Введение
В начале июля вышла очередная версия(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.