Недавно я искал систему хранения для моей программы. Она представляет собой desktop-приложение, которому нужно хранить множество объектов и осуществлять поиск текста в них. И я подумал: "Почему бы не попробовать что-то новое". Вместо SQL базы данных типа SqLite я мог бы использовать документную базу данных. Но мне хотелось бы, чтобы для неё не требовался отдельный сервер, чтобы она работала с простым файлом. Поиск в Интернет подобных систем для .NET приложений быстро вывел меня на LiteDB. Здесь я хочу поделиться тем, что я нашёл в процессе работы с этой базой данных.


Наследование

Специфика моей программы такова. Я хочу сохранять в базу простые объекты типа:

internal class Item
{
    public string Title { get; set; }

    public string Description { get; set; }

    public List<Field> Fields { get; set; } = new List<Field>();
}

Но вот класс Field у меня абстрактный, имеющий массу наследников:

internal abstract class Field
{
}

internal sealed class TextField : Field
{
    public string Text { get; set; }
}

internal sealed class PasswordField : Field
{
    public string Password { get; set; }
}

internal sealed class DescriptionField : Field
{
    public string Description { get; set; }
}

...

При работе с SQL базами данных мне приходилось настраивать сохранение различных наследников класса Field. Я полагал, что в LiteDB мне придётся писать собственный механизм BSON-сериализации, благо такая возможность предоставляется. Но LiteDB меня приятно удивил. Никаких усилий с моей стороны не потребовалось. Сохранение и восстановление различных типов происходит совершенно без моего участия. Вы создаёте нужные объекты:

var items = new Item[]
{
    new Item
    {
        Title = "item1",
        Description = "description1",
        Fields =
        {
            new TextField
            {
                Text = "text1"
            },
            new PasswordField
            {
                Password = "123"
            }
        }
    },
    new Item
    {
        Title = "item2",
        Description = "description2",
        Fields =
        {
            new TextField
            {
                Text = "text2"
            },
            new DescriptionField
            {
                Description = "description2"
            }
        }
    }
};

... и вставляете их в базу данных:

using (var db = new LiteDatabase(connectionString))
{
    var collection = db.GetCollection<Item>();

    collection.InsertBulk(items);
}

Вот и всё. LiteDB поставляется с удобной программой LiteDB.Studio, которая позволяет вам исследовать содержимое вашей базы данных. Давайте посмотрим, как хранятся наши объекты:

{
  "_id": {"$oid": "62bf12ce12a00b0f966e9afa"},
  "Title": "item1",
  "Description": "description1",
  "Fields":
  [
    {
      "_type": "LiteDBSearching.TextField, LiteDBSearching",
      "Text": "text1"
    },
    {
      "_type": "LiteDBSearching.PasswordField, LiteDBSearching",
      "Password": "123"
    }
  ]
}

Оказывается, что для каждого объекта в поле _type сохраняется его тип, что и позволяет правильно восстановить объект при чтении из базы.

Что ж, с сохранением разобрались. Давайте перейдём к чтению.

Поиск текста

Как я уже сказал, мне необходимо искать объекты Item в свойствах Title и Description которых, а так же в свойствах их полей (свойство Fields) содержится определённый текст.

С поиском внутри свойств Title и Description всё ясно. Документация содержит понятные примеры:

var items = collection.Query()
    .Where(i => i.Title.Contains("1") || i.Description.Contains("1"))
    .ToArray();

Но с поиском в полях есть проблема. Дело в том, что абстрактный класс Field не определяет никаких свойств. Поэтому я не могу на них сослаться здесь. К счастью LiteDB позволяет использовать строковую запись запросов:

var items = collection.Query()
    .Where("$.Title LIKE '%1%' OR $.Description LIKE '%1%'")
    .ToArray();

Но как с её помощью искать внутри полей? Документация даёт подсказку, что выражение должно выглядеть примерно так:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR $.Fields[@.Text] LIKE '%1%' OR $.Fields[@.Description] LIKE '%1%' OR $.Fields[@.Password] LIKE '%1%'

Но такая запись приводит к ошибке:

Left expression `$.Fields[@.Text]` returns more than one result. Try use ANY or ALL before operant.

Действительно, использование функции ANY решает проблему:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR ANY($.Fields[@.Text LIKE '%1%']) OR ANY($.Fields[@.Description LIKE '%1%']) OR ANY($.Fields[@.Password LIKE '%1%'])

По поводу этого выражения хочу сделать несколько замечаний. Во-первых, из него кажется, что можно в качестве фильтра использовать выражение вида:

ANY($.Fields[@.Text LIKE '%1%'])

Но это не так. Попытка отфильтровать записи по этому выражению приводит к ошибке:

Expression 'ANY($.Fields[@.Text LIKE "%1%"])' are not supported as predicate expression.

Странно, не правда ли. Оказывается нужно писать так:

ANY($.Fields[@.Text LIKE '%1%']) = true

Сразу на ум приходят 1 и 0, используемые в предикатах в SQL Server. Ну да Бог им судья.

Во-вторых, меня несколько смущала фраза Try use ANY or ALL before operant. Как-то она не согласовывалась у меня с вызовом функции. Оказывается, что LiteDB поддерживает и следующий синтаксис:

$.Fields[*].Text ANY LIKE '%1%'

К большому сожалению, он не описан в документации, я натолкнулся на него, просматривая исходных код тестов LiteDB на GitHib. Он, в отличии от функции ANY нормально работает в качестве предиката без всяких сравнений с true.

В итоге поисковое выражение можно переписать так:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR ($.Fields[*].Text ANY LIKE '%1%') OR ($.Fields[*].Description ANY LIKE '%1%') OR ($.Fields[*].Password ANY LIKE '%1%')

Здесь меня смущали две вещи. Во-первых, для каждого нового типа поля мне могло потребоваться дописывать это выражение, если оно использовало новое имя текстового свойства, в котором мне нужно было осуществлять поиск. Можно ли что-то сделать по этому поводу? Ну, можно.

LiteDB поддерживает атрибут BsonField, который позволяет задавать имя поля базы данных, в котором будет храниться данное свойство. Можно использовать его так:

internal sealed class TextField : Field
{
    [BsonField("TextField")]
    public string Text { get; set; }
}

internal sealed class PasswordField : Field
{
    [BsonField("TextField")]
    public string Password { get; set; }
}

internal sealed class DescriptionField : Field
{
    [BsonField("TextField")]
    public string Description { get; set; }
}

Теперь можно использовать одно поисковое выражение для любых объектов Field:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR $.Fields[*].TextField ANY LIKE '%1%'

Добавляя нового наследника класса Field, я могу просто пометить его свойство атрибутом [BsonField("TextField")]. Тогда мне не придётся вносить никаких изменений в поисковое выражение.

К сожалению этот способ не совсем решает все наши проблемы. Дело в том, что у наследника Field может быть произвольное число свойств, в которых нужно осуществлять поиск. Поэтому может оказаться, что их нельзя привести к уже имеющимся в базе полям.

По этой причине я пока остановлюсь на старой форме запроса:

$.Title LIKE '%1%' OR $.Description LIKE '%1%' OR ($.Fields[*].Text ANY LIKE '%1%') OR ($.Fields[*].Description ANY LIKE '%1%') OR ($.Fields[*].Password ANY LIKE '%1%')

У неё есть ещё одна проблема. Мне несколько раз пришлось писать искомую строку '%1%'. Да и SQL Injection ещё никто не отменял (хотя не уверен, применимо ли здесь использовать слово SQL). Короче говоря, я веду к тому, что неплохо было бы использовать параметры запроса. И действительно, API позволяет это делать:

Параметры запроса
Параметры запроса

Но как конкретно мне сослаться на параметр в тексте запроса? К сожалению, документация опять подвела меня. Пришлось лезть в код тестов для LiteDB и искать там, как следует использовать параметры:

var items = collection.Query()
    .Where("$.Title LIKE @0 OR $.Description LIKE @0 OR ($.Fields[*].Text ANY LIKE @0) OR ($.Fields[*].Description ANY LIKE @0) OR ($.Fields[*].Password ANY LIKE @0)", "%1%")
    .ToArray();

Что ж, с поиском разобрались. Но насколько быстро он осуществляется?

Индексы

LiteDB поддерживает индексы. Конечно, моё приложение будет хранить не такой большой объём данных, чтобы это было критически важно. Но всё же было бы хорошо, чтобы мои запросы использовали индексы и выполнялись быстро.

Во-первых, как нам узнать, использует ли данный запрос индекс или нет. Для этого в LiteDB есть команда EXPLAIN. В LiteDB.Studio я выполню свой запрос так:

EXPLAIN
SELECT $ FROM Item
WHERE $.Title LIKE '%1%'
    OR $.Description LIKE '%1%'
    OR ($.Fields[*].Text ANY LIKE '%1%')
    OR ($.Fields[*].Description ANY LIKE '%1%')
    OR ($.Fields[*].Password ANY LIKE '%1%')

Результат выполнения этой команды содержит следующую информацию об используемом индексе:

"index":
  {
    "name": "_id",
    "expr": "$._id",
    "order": 1,
    "mode": "FULL INDEX SCAN(_id)",
    "cost": 100
  },

Как видите, сейчас применяется полный просмотр всех данных. Хотелось бы добиться лучшего результата.

Документация прямо говорит, что возможно создать индекс на основе свойств типа массив. При этом можно будет осуществлять поиск любого входящего в такие массивы значения. Например, вот так можно создать индекс для поиска информации в свойствах Text моих полей:

collection.EnsureIndex("TextIndex", "$.Fields[*].Text");

Теперь этот индекс можно использовать в поиске:

var items = collection.Query()
    .Where("$.Fields[*].Text ANY LIKE @0", "%1%")
    .ToArray();

Команда EXPLAIN в LiteDB.Studio показывает, что этот запрос действительно использует созданный нами индекс:

"index":
  {
    "name": "TextIndex",
    "expr": "MAP($.Fields[*]=>@.Text)",
    "order": 1,
    "mode": "FULL INDEX SCAN(TextIndex LIKE \"%1%\")",
    "cost": 100
  },

Но как нам объединить в один индекс все наши свойства? На помощь приходит команда CONCAT. Она объединяет в один массив несколько свойств. Вот как будет выглядеть создание полного индекса:

collection.EnsureIndex("ItemsIndex", @"CONCAT($.Title,
            CONCAT($.Description,
                CONCAT($.Fields[*].Text,
                    CONCAT($.Fields[*].Password,
                            $.Fields[*].Description
                    )
                )
            )
        )");

Чтобы искать по нему, нам придётся переписать наше поисковое выражение:

var items = collection.Query()
    .Where(
        @"CONCAT($.Title,
            CONCAT($.Description,
                CONCAT($.Fields[*].Text,
                    CONCAT($.Fields[*].Password,
                            $.Fields[*].Description
                    )
                )
            )
        ) ANY LIKE @0",
        "%1%")
    .ToArray();

Теперь наш поиск действительно использует индекс:

"index":
  {
    "name": "ItemsIndex",
    "expr": "CONCAT($.Title,CONCAT($.Description,CONCAT(MAP($.Fields[*]=>@.Text),CONCAT(MAP($.Fields[*]=>@.Password),MAP($.Fields[*]=>@.Description)))))",
    "order": 1,
    "mode": "FULL INDEX SCAN(ItemsIndex LIKE \"%3%\")",
    "cost": 100
  },

К сожалению, оператор LIKE всё равно приводит к FULL INDEX SCAN. Остаётся надеяться, что индекс всё же даёт некоторый выигрыш. Хотя, зачем нам надеяться. Мы же можем всё измерить. У нас же есть BenchmarkDotNet.

Я написал вот такой класс для проведения тестов быстродействия:

[SimpleJob(RuntimeMoniker.Net60)]
public class LiteDBSearchComparison
{
    private LiteDatabase _database;
    private ILiteCollection<Item> _collection;

    [GlobalSetup]
    public void Setup()
    {
        if (File.Exists("compare.dat"))
            File.Delete("compare.dat");

        _database = new LiteDatabase("Filename=compare.dat");

        _collection = _database.GetCollection<Item>();

        _collection.EnsureIndex("ItemIndex", @"CONCAT($.Title,
            CONCAT($.Description,
                CONCAT($.Fields[*].Text,
                    CONCAT($.Fields[*].Password,
                            $.Fields[*].Description
                    )
                )
            )
        )");

        for (int i = 0; i < 100; i++)
        {
            var item = new Item
            {
                Title = "t",
                Description = "d",
                Fields =
                {
                    new TextField { Text = "te" },
                    new PasswordField { Password = "p" },
                    new DescriptionField { Description = "de" }
                }
            };

            _collection.Insert(item);
        }
    }

    [GlobalCleanup]
    public void Cleanup()
    {
        _database.Dispose();
    }

    [Benchmark(Baseline = true)]
    public void WithoutIndex()
    {
        _ = _collection.Query()
            .Where("$.Title LIKE @0 OR $.Description LIKE @0 OR ($.Fields[*].Text ANY LIKE @0) OR ($.Fields[*].Description ANY LIKE @0) OR ($.Fields[*].Password ANY LIKE @0)",
                "%1%")
            .ToArray();
    }

    [Benchmark]
    public void WithIndex()
    {
        _ = _collection.Query()
            .Where(@"CONCAT($.Title,
                        CONCAT($.Description,
                            CONCAT($.Fields[*].Text,
                                CONCAT($.Fields[*].Password,
                                        $.Fields[*].Description
                                )
                            )
                        )
                    ) ANY LIKE @0",
                "%1%")
            .ToArray();
    }
}

Результаты, которые я получил для него, следующие:

Method

Mean

Error

StdDev

Ratio

WithoutIndex

752.7 us

14.71 us

21.56 us

1.00

WithIndex

277.5 us

4.30 us

4.02 us

0.37

Как видите, индекс действительно может давать существенное преимущество в быстродействии.

Заключение

Вот и всё, что я хотел сказать сегодня. В общем у меня осталось довольно приятное впечатление о LiteDB, я бы использовал её в качестве документного хранилища для небольших проектов. К сожалению, документация оставляет желать лучшего, на мой взгляд.

Надеюсь, приведённая информация будет вам полезна. Удачи!

Комментарии (21)


  1. Laserson
    12.07.2022 19:39
    +1

    Асинхронное апи так и не завезли?


    1. iakimov Автор
      12.07.2022 19:44

      Похоже, нет.


  1. apro
    12.07.2022 20:22

    Вместо SQL базы данных типа SqLite я мог бы использовать документную базу данных

    Хотелось бы уточнить, современная sqlite имеет поддержку https://www.sqlite.org/json1.html , чего-то не хватает чтобы ее использовать ее как "документную базу данных"?


    1. DEugene
      13.07.2022 10:10

      Если у вас приложение на .NET, то преимущество LiteDB в том что нет необходимости создавать прослойку в виде SQL запросов или подключении ORM. Вы работаете сразу напрямую с вашими POCO делая запросы к коллекциям документов с Linq.


    1. iakimov Автор
      13.07.2022 10:24

      Нет никаких проблем с использованием SqLite. Просто хотелось чего-нибудь нового.


  1. shai_hulud
    12.07.2022 20:57

    Я так понимаю, данные автора помещаются в память. Тогда логичный вопрос, зачем искать какой-то движек/либа БД, когда самый быстрый и удобный доступ к объектам CLR предоставляет сам C#?


    1. xXxVano
      12.07.2022 21:49

      Вероятно автор хочет что бы приложение переживало завершение приложения без потери данных. Собственно возможность работы DB без полной загрузки в память, это лишь малая часть её функциональности.


      1. Kerman
        12.07.2022 21:53

        На такие случаи придумали сериализацию


      1. shai_hulud
        12.07.2022 22:56

        Сложно себе представить мобильное/десктопное приложение, которому требуется оперировать бОльшим количеством структурированных данных, чем есть доступной памяти.

        Встроенные БД не дают каких-либо волшебных гарантий сохранения данных. Всё тоже самое достигается сериализацией в файл.


        1. apro
          12.07.2022 23:42
          +1

          Встроенные БД не дают каких-либо волшебных гарантий сохранения данных. Всё тоже самое достигается сериализацией в файл.

          Ну так про любую библиотеку можно сказать - "это библиотека ничего особого не делает, ее код можно написать самому в своей программе".

          Если например файловая система журналируемая, но транзакции позволяет делать только для метаданных, то нужно заморачиваться с двумя файлами, в одном данные в другом последние изменения, и нужно вызвать в правильном порядке нужные системные вызовы, а главное отработать все возможные ошибки. Например можно почитать про "I/O Error Testing" для sqlite: https://www.sqlite.org/testing.html Все это конечно можно сделать самому, но стоит ли это того?


        1. xXxVano
          13.07.2022 00:33
          +4

          Но почему обязательно не помещается в память то. БД не обязательно про огромные размеры, а про удобство использование и уже собранные проблемы при реализации функционала сохранения.


          Например обычные заметки. Их можно редактировать и сохранять. А так же они могут хотеть синхронизироваться из облака.


          Можно конечно всё написать самому, а потом героически чинить множество проблем и мелочей, которые возникнут в процессе. А можно же взять готовую бд и получить тот же функционал из коробки.


          Что она даст? Например возможность не думать о:


          • Синхронизации потоков. Может прилететь апдейт из облака в другом потоке и бд это разрулит сама;
          • Синхронизации использования файла на диске. Кто в него пишет, кто читает, всё целиком или частями;
          • Мерже изменений, если можно менять стейт асинхронно или параллельно;
          • Роллбек изменений, когда что то пошло не так;
          • Поиск по значениям полей или даже полнотекстовый поиск (не знаю умеет ли в него конкретно LiteDB);
          • О сохранности данных на диске в конце концов. Я знаю что если транзакция закоммичена, то даже если приложение крашнёт, то я его переоткрою и данные не потеряются. В случае с файлом есть много ньюансов, типа flush забыли позвать.

          А если это десктопное приложение начинает уметь обрабатывать запросы от кого либо извне, то все эти проблемы увеличиваются многократно


          1. shai_hulud
            13.07.2022 10:05
            -1

            Можно конечно всё написать самому, а потом героически чинить множество проблем и мелочей, которые возникнут в процессе. А можно же взять готовую бд и получить тот же функционал из коробки.

            Если взять встроенную БД придётся аналогично чинить множество проблем, только уже чужих.
            Ну и под написать самому, то тут есть нуансы. Каждый перечисленный пункт делается одной двумя строками кода либо дизайном.

            Синхронизации потоков. Может прилететь апдейт из облака в другом потоке и бд это разрулит сама;

            Мерже изменений, если можно менять стейт асинхронно или параллельно

            Роллбек изменений, когда что то пошло не так

            Решается immutable структурами данных. Хочешь менять, делай copy-on-write.
            Хочешь применить изменения, удостоверься что ты менял последнюю версию. Для пессимистов есть блокировки. Если не копипастить все данные каждый раз, то проблемы большого мемори трафика нет.

            Синхронизации использования файла на диске. Кто в него пишет, кто читает, всё целиком или частями;

            Читать/писать целиком. Писать в новый файл, старый заменять атомарным реплейсом. Читать естественно один раз на старте, дальше работать из памяти.

            Поиск по значениям полей или даже полнотекстовый поиск (не знаю умеет ли в него конкретно LiteDB)

            Аналогично любой встроенной БД надо будет делать разметку тех полей что индексируются. Можно сделать очень дешево, можно подороже и воткнуть полнотекстовый поиск любой сложности, а не то что "есть" в либе.

            О сохранности данных на диске в конце концов.


            Как выше указано, проблемы нет т.к. нет работы с файлами. Пишут и читают из памяти. На файловой системе остается только последняя копия, которая не может быть сломанной.

            А теперь из плюсов:

            • Нет сторонних компонентов.

            • Непревзойденный перфоманс.

            • Знакомый всем разработчикам API работы с коллекциями и объектами.

            • Формат хранения данных, который может читаться человеком. Удобно отлаживать.

            • Схема данных объявлена там, где используется, а не в 2 местах.

            • Можно запрофилировать проблемы производительности и решить их.
              Можно найти баг и починить. В чужом компоненте это из разряда фантастики.


            Как я писал выше, есть только две проблемы для которой подходят БД:

            • данные не помещаются в память.

            • данные надо обслуживать (бекапы, аудиты, интеграции), хотя это уже СУБД


            1. pfffffffffffff
              13.07.2022 10:25

              Сколько с бд работал не встречал баги. И все же бд это про удобство и скорость разработки.


            1. xXxVano
              13.07.2022 14:54

              Если взять встроенную БД придётся аналогично чинить множество проблем, только уже чужих.

              Я прекрасно понимаю желание написать функционал бд самому. Но я не понимаю желания на каждом проекте это делать по новой. Почему нельзя это оформить в nuget и переиспользовать? А если так, то вот и получается своя бд.


              Решается immutable структурами данных. Хочешь менять, делай copy-on-write

              Такой подход работает только в пределах небольших по объёму структур, но никак не в пределах всей бд, а тут как раз и начинаются проблемы.
              Вот есть структура из статьи:


              class Item
              {
                  public string Title { get; set; }
                  public string Description { get; set; }
                  public List<Field> Fields { get; set; } = new List<Field>();
              }

              И сама она лежит в каком-нибудь:


              class Table
              {
                  public List<Item> Fields { get; set; } = new List<Item>();
              }

              И вся эта табличка весит 1-2GB. Что вы тут будете менять copy-on-wirte, когда вам нужно будет новый item добавить? Всю таблицу копировать? Если да, то непревзойдённого перфоманса у вас не будет, если нет, то синхронизацию потоков это не обеспечит.


              Читать/писать целиком. Писать в новый файл, старый заменять атомарным реплейсом. Читать естественно один раз на старте, дальше работать из памяти.

              Я правильно понимаю, что если у меня приложение заметок с сохранёнными данными на 1-2Gb, то при каждом открытии/закрытии оно должно целиком весь файл перечитывать/перезаписывать (а по хорошему вообще на каждый набранный символ тригерить сейв в фоне)? Тут тоже возникают небольшие сомнения относительно скорости работы.


              Как выше указано, проблемы нет т.к. нет работы с файлами. Пишут и читают из памяти. На файловой системе остается только последняя копия, которая не может быть сломанной.

              Тут как раз возникает та проблема, про которую я говорил в начале. Когда я нажал кнопку добавить/сохранить и приложение сказало что оно всё сделало, то я хочу что бы данные уже на диске были, а не в памяти продолжали висеть. Иначе при любом краше я всё потеряю.


              В общем мой поинт по прежнему в том что если в приложении не нужно ни возможность менять данные из разных потоков, ни изменение данных частями, ни гарантированность сохранности данных при краше, то конечно достаточно в 1 строку открыть файл и сериализовать в него json/xml. Но это ни разу не сравнимо по функционалу с тем что даёт нормальная встраиваемая база данных.


              1. shai_hulud
                14.07.2022 10:49

                И вся эта табличка весит 1-2GB. Что вы тут будете менять copy-on-wirte, когда вам нужно будет новый item добавить? Всю таблицу копировать?

                Конечно нет. Т.к. оригинал не изменяемый, то достаточно сделать новую неизменяемую List подобную коллекцию, где будут "запатчены" измененные элементы, в остальном она будет ссылаться на старую. В итоге будет занято немного новой памяти. Любой программист выше джуниора может накатать этот код за рабочий день.

                Я правильно понимаю, что если у меня приложение заметок с сохранёнными данными на 1-2Gb, то при каждом открытии/закрытии

                Что за заметки на 1-2Gb? Десктопное приложение обычно оперирует пользовательскими данными, с большим натягом пользователь сгенерирует 1-2Mb данных. Это передёргивание.

                Но это ни разу не сравнимо по функционалу с тем что даёт нормальная встраиваемая база данных.

                В булочную через дорогу можно ездить на авто. Это даже удобно, но не практично. У каждой проблемы есть оптимальное решение.


                1. xXxVano
                  14.07.2022 11:13

                  Т.к. оригинал не изменяемый, то достаточно сделать новую неизменяемую List подобную коллекцию, где будут "запатчены" измененные элементы, в остальном она будет ссылаться на старую.

                  А как об этом новом list узнает остальной код программы? Как помёржить изменения, если 2 разных потока сделали 2 RO копии одновременно? Если там создаются новые листы то всё равно надо всё будет блокировкой оборачивать, т.к. при оптимистичном подходе вы можете какие то операции по нескольку раз делать, что далеко не всегда приемлемо.


                  Что за заметки на 1-2Gb? Десктопное приложение обычно оперирует пользовательскими данными, с большим натягом пользователь сгенерирует 1-2Mb данных. Это передёргивание.

                  Обычные заметки, как в существующих приложениях. Там же не только текст, но и картинки и видосики вполне успешно сохраняются. Ни разу не передёргивание. 1-2Gb успешно влазят в память любого PC даже 10 летней давности и вполне подходят под ваше определение.


                  Но даже если и 1-2Мб. Добавить nuget пакет на любую популярную db и в 2-3 строчки начать её использовать получается на порядок проще и удобнее чем сделать сериализацию в файлик, а потом по ходу развития проекта постоянно подпирать в нём всё новые проблемные случаи.


                  Конечно есть случае когда бд не нужна и не незачем тащить мидлвар, но далеко не все случае такие.


                  1. shai_hulud
                    14.07.2022 17:18

                    А как об этом новом list узнает остальной код программы? Как помёржить изменения, если 2 разных потока сделали 2 RO копии одновременно?

                    Узнают по тому что теперь новая ссылка на list. Как узнать о конфликте? Это по завершению изменений тот list с которым начали изменения не соответствует актуальному. Можно заново начинать работу. Либо оптимистичный подход, который будет работать с редкими изменениями. Либо пессимистичный, если высокая конкуренция. Но высокая конкуренция от каких асинхронных событий? Один источник это user input от одного человека/устройства, он редкий, второй это всё что приходит с сервера, частота разная. Ну так.

                    Обычные заметки, как в существующих приложениях. Там же не только текст, но и картинки и видосики вполне успешно сохраняются. Ни разу не передёргивание.

                    Хранить BLOB'ы в БД? Есть такая штука, придуманная в 80-х, для хранения BLOB'ов.

                    Но даже если и 1-2Мб. Добавить nuget пакет на любую популярную db и в 2-3 строчки начать её использовать

                    Взять качественное и проверенное решение и делать схемы, мапперы и транслировать запросы в SQL. Потом возиться с платфомами и архитектурами.
                    Либо взять nuget пакет с 100 звездами и модной in-memory DB и терять данные из за неправильной синхронизации с ФС :) Альтернативы ИМХО хуже, чем своё написать.


                    1. xXxVano
                      14.07.2022 22:19

                      Узнают по тому что теперь новая ссылка на list. Как узнать о конфликте? Это по завершению изменений тот list с которым начали изменения не соответствует актуальному.

                      Это и есть один из способов работы с многопоточным кодом. И вот этим нужно заниматься самому, вместо того что бы просто вызывать нужный метод, который всё это инкапсулирует внутри и уже кем то написан и проверен.


                      Взять качественное и проверенное решение и делать схемы, мапперы и транслировать запросы в SQL. Потом возиться с платфомами и архитектурами.
                      Либо взять nuget пакет с 100 звездами и модной in-memory DB и терять данные из за неправильной синхронизации с ФС :) Альтернативы ИМХО хуже, чем своё написать.

                      LiteDB как раз тут третья альтернатива. Не требует таких усилий как SQL базы, но и не in-memory и не 100 звёздочек. Хочется больше звёздочек — можно взять BoltDB или RocksDB. Там звёздочек больше чем в dotnet/runtime.


                      Ну и если у вас есть уже хотя бы раз написанное решение, которое по вашему не обладает таким количеством недостатков и багов, то почему него не выложить его на GitHub, что бы и другие тоже могли его использовать?


                      За лично мой опыт работы с LiteDB она данные не теряла ни разу и позволила потратить пол дня на то что бы полностью закрыть вопрос с хранением всех данных в проекте.


                      Но она конечно далека от идеала. Например нет async методов. Да и индексы объявляются достаточно громоздко. Но делать db руками, это явно не то сравнимая по времени написания/поддержки альтернатива.


        1. Fulborg
          13.07.2022 16:40
          +1

          Когда мы выбирали механизм хранения локальных данных для нашего ПО, чтобы уйти от локального SQL - мы проверяли и Sqlite и LiteDB и сериализацию с сохранением в файлы.

          К удивлению, LiteDB с существенным отрывом обошла ручное сохранение файлов (не помню точных цифр, давно это было, но она была в 2-3 раза). Экспериментировали и с сжатием, и без сжатия, и просто записью бинарного потока данных, чтобы избавиться от накладных расходов на сериализацию.

          Там достаточно много низкоуровневых оптимизаций работы с файловой системой, которые позволяют этого добиться. Повторить все это самим конечно можно, но надо ли если кто-то уже это сделал?

          Ложка дегтя - как выяснилось в эксплуатации в production - её хранение данных не слишком надежно. На наших масштабах (~10000 установок, 3-5 RPM на запись в среднем) - мы ловили по несколько сломавшихся БД в месяц. Попытки достучаться до разработчика и раскопать в чем проблема - не приводили ни к чему, игнор, потом "обновитесь на последнюю версию", потом повторение ситуации. Сложный конечно вопрос, кто виноват, LiteDB или проблемы с файловой системой у клиентов, но SQL (LocalDB) это переживал нормально.

          В итоге остановились на том, что в LiteDB перенесли большие, часто меняемые, но не слишком критичные данные и относились к нему как к персистентному кешу. Критичные к потере данные - оставили в SQL


  1. BareDreamer
    13.07.2022 10:05

    По-моему, вы излишне усложнили структуру классов. Или в статье неудачный пример. Я бы сделал проще:

    internal class Field
    {
      public string Text { get; set; }
    }
    
    internal sealed class TextField : Field {  }
    internal sealed class PasswordField : Field {  }
    internal sealed class DescriptionField : Field {  }

    Description и Password – это тоже текстовое значение. Если Text нельзя перенести в класс Field, лучше всё равно использовать одно имя свойства, это упростит условие поиска

    ($.Fields[*].Text ANY LIKE @0) OR ($.Fields[*].Description ANY LIKE @0) OR ($.Fields[*].Password ANY LIKE @0)


    1. iakimov Автор
      13.07.2022 10:30

      Вероятно, вы правы, и пример не из лучших. Но целью статьи было рассмотрение возможностей LiteDb. Кроме того, не хотелось бы подгонять структуру классов под структуру хранения. Имя свойства Password на мой взгляд более говорящее, чем общее Text. Поэтому такого выноса свойств в базовый класс я не люблю.