MongoDB и C#. Новые возможности и неочевидные проблемы

в 4:32, , рубрики: .net, mongodb, nosql, метки: , , ,

Введение

В начале июля вышла очередная версия(1.5) официального драйвера MongoDB для C#. Среди нововведений стоит отметить поддержку типизированных запросов. Теперь появилась возможность использовать лямбда-функции в связке с Expression.
В этой статье я покажу примеры нового синтаксиса, который мне очень нравится(а мне вообще Expression в C# очень нравится), а также продемонстрирую примеры запросов, где, увы, Expression нам ничем не поможет и придется вернуться к привычным строкам. Также я порассуждаю, почему оно так, и будет ли когда-нибудь всё прекрасно в С# при работе с MongoDB.

О хорошем

Да, теперь вместо:

ObjectId articleId = new ObjectId("dgdfg343ddfg");
IMongoQuery query = Query.EQ("_id", articleId);

можно написать так:

ObjectId articleId = new ObjectId("dgdfg343ddfg");
IMongoQuery query = Query<Article>.EQ(item => item.Id, articleId);

при наличии, разумеется, класса Article, представляющего схему документа. Стоит отметить что все методы, связанные с выборкой у QueryBuilder<T> полностью аналогичны таковым в QueryBuilder. Правда для объединения запросов всё равно нужно использовать Query.And или Query.Or. Ну это и понятно, так как все методы возвращают тот же QueryBuilder. По факту их можно комбинировать как угодно.
Для UpdateBuilder также появился UpdateBuilder<T> с соответствующими методами.
Как было раньше:

Article article = new Article();
IMongoUpdate update = Update.PushWrapped("Articles", article);

Как можно теперь:

Article article = new Article();
IMongoUpdate update = Update<Article>.Push(item => item.Articles, article);

По-моему, гораздо лучше(если говорить о красоте и контроле). Вообще, деревья выражений очень мощная и красивая вещь. Здесь только самая вершина айсберга, но я много где их успешно использовал. Во-многом, это Reflection с человеческим лицом.
Ну да ладно. Здесь вроде бы всё просто. Перейдем к менее красивым вещам.

О грустном

Простые запросы вроде поиска по значению проходят на ура. Чуть посложнее, кстати, тоже. Например, поиск элемента в массиве:

IMongoQuery query = Query<Article>.ElemMatch<Comment>(item => item.Comments, builder => builder.EQ(item => item.Id, comment.Id));

Но напряжение уже чувствуется.
Теперь давайте подбросим угля. Есть документ такого содержания:

_id : "s3d4f5d6sf",
array1 : [{
            _id : "cv434lfgd45",
            array2 : [{
                        _id : "df4gd45g43f4",
                        name : "Logic" 
                      },
                      {
                         ...
                      }] 
          },
          {
             ...
          }]

И вот у нас стоит задача добавить добавить ещё один элемент в массив array2. В программе:

1) главный документ у нас будет описываться классом модели Doc
2) array1 превратим в List<Item1>, где Item1 — класс модели для каждого элемента array1
3) array2 превратим в List<Item2>, где Item2 — класс модели для каждого элемента array2

(Я сознательно обезличил документ, чтобы не было претензий к структуре данных)

С поиском проблем само собой нет:

IMongoQuery query = Query.And(
                       Query<Doc>.EQ(item => item.Id, new ObjectId("s3d4f5d6sf")),
                       Query<Doc>.ElemMatch<Item1>(item => item.Array2, builder => builder.EQ(item => item.Id, new ObjectId("df4gd45g43f4")));

А вот с запросом на обновление сложности возникли(я сознательно не рассматриваю вариант с выборкой документа, добавлении в него данных, и последующей записью в базу. Ибо вариант слишком неоптимален. Плюс хотелось бы в таких случае обновлять данные атомарной операцией, а не каскадом выборок/записей). Необходимо добраться до вложенного массива array2. Если пользоваться стандартными средствами MongoDB, то можно сделать так:

IMongoUpdate update = Update.PushWrapped<Item2>("array1.$.array2", new Item2());

Придумать, как обойти магические строки я так и не смог, и чем дольше я смотрел на выражение «array1.$.array2», тем больше подозрительных мыслей у меня возникало.

О высоком

Начнем издалека.

Есть «статические» языки. Например C#. В них структуры элементов известны до компиляции(чаще всего). И именно этими структурами мы и оперируем. А конкретнее мы оперируем классами, этакими схемами. Ну а со стороны данных у нас есть объекты — экземпляры классов.

Есть «динамические» языки. Например, Javascript. В них обычной практикой является формирование структуры данных по ходу исполнения. В общем случае мы оперируем «пустым» объектом и добавляем/удаляем методы и поля. Структур как таковых не существует вообще. Есть только начальная точка(прототип), от которой мы отталкиваемся.

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

А вот теперь я буду фантазировать и приводить более грубые аналогии.
Проблема в составлении запроса с помощью Expression для случая «array1.$.array2» состоит в том, что в C# мы оперируем структурами(классами) для работы с документами(объектами). Масла в огонь добавляет и «мощь» запроса. Если в запросе выборки мы оставим только первое условие, то при наличии желания(установки флага в multiupdate) мы сможем добавить элемент в каждый из массивов array2. Ведь по сути наш красивый запрос(пусть даже мы сможем его написать) все равно будет преобразован в «array1.$.array2». Для более разветвленного документа с глубоким уровнем вложенности ситуация только усугубится.

Я не утверждаю, что невозможно придумать Expression-синтаксис для подобной задачи, просто мне кажется, что этот синтаксис потеряет понятность. То есть связь синтаксической и семантической части(интуитивная, понятное дело) будет таять. Я не уверен, что именно этого хочу в своих проектах.
Для меня выводы довольно очевидны: красивый синтаксис в относительно простых случаях — это здорово и круто, однако в более сложных ситуациях можно прийти к тому, что Expression не упрощает работу, а только множит неясности.

Заключение

У меня нет большого опыта работы с MongoDB, и я с удовольствием включу в статью решение описанной проблемы запроса, а также аргументированные рассуждения/спор на тему построения архитектуры и кода в C# для MongoDB.

Автор: Alvaro

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js