MongoDB и C# driver от 10gen, неочевидные моменты


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

    После первого поста про mongodb я на несколько недель переключался на f# и вернувшись понял, что всё забыто. Пришлось перечитывать собственный пост, и видимо не одному приходится разбирать монго, раз даже Dmitry Nesteruk влепил мне like. Лирическое отступление закончилось. Сразу оговорюсь, в данной статье как и в предыдущей речь пойдёт о драйвере, ссылка на который размещена на сайте монго, это драйвер от 10gen.

    Частичная загрузка объектов


    Начав переводить один из реальных проектов на Монго я сразу столкнулся с необходимостью загружать объекты без вложенных коллекций, которые он содержит. Надо заметить, что рассматриваемый драйвер работает очень просто. Через классы хелперов и визардов он просто складывает строки чтобы получить работающий запрос на родном для mongo javascript. Чтобы не загружать какое то поле, достаточно в запросе указать field_name:0

      db.users.find({}, {thumbnail:0});<br/>

    В драйвере всё не так очевидно, но после некоторого копания удалось получить вот такой код:

    MongoCollection coll = GetCollection();<br/>
    FieldsBuilder fbExclude = Fields.Exclude(new string[]{“thumbnail”});<br/>
    //can be FindAllAs<TEntity>()
    MongoCursor result = coll.FindAll().SetFields(fbExclude);<br/>

    Все результаты в курсоре будут без поля thumbnail, проверено, работает замечательно. Все не загруженные поля будут инициализированы значениями по умолчанию, для коллекций null. Тут есть одна тонкость, если вы не загрузите какое либо поле, а потом запишете этот объект, то он совершенно законно перетрёт старый в базе и вот такой тест упадёт на последнем Assert-е:

    public class Data<br/>
    {<br/>
      [BsonId]<br/>
      public ObjectId Id {get;set;}<br/>
      public int Area {get;set;}<br/>
    }<br/>
    Data x = new Data();<br/>
    x.Area = 20;<br/>
    var db = GetDb();<br/>
    var coll = db.GetCollection<Data>(typeof(Data).FullName);<br/>
    db.ClearColl<Data>();<br/>
    coll.Save(x);<br/>
    Data y = coll.FindAllAs<Data>().SetFields(Fields.Exclude(new string[]{"Area"})).FirstOrDefault();<br/>
    Assert.AreEqual(0, y.Area);<br/>
    coll.Save(y);<br/>
    Data z = coll.FindAllAs<Data>().SetFields(
    Fields.Exclude(new string[] { "Area" })
    ).FirstOrDefault();<br/>
    //fail of course
    Assert.AreEqual(20, z.Area);<br/>

    Я думаю это как то решается через Find and modify, но пока такой потребности не стоит и всё что нужно менять грузится полностью.

    Выбор по элементу вложенного массива


    В моём случае была задача выбрать регион на основе почтового кода одной европейской страны. Т.к. по историческим причинам общая логика работала не во всех случаях, то регион должен был определятся по следующему алгоритму: если есть точное совпадение, то выбираем найденный регион, если совпадения нет, то оставляем первые 2 цифры и делаем предположение о регионе на основе диапазона почтовых кодов в регионе, причём самих диапазонов может быть много. Объект выглядит как то так:

    public class HighLevelCodeInterval <br/>
    {<br/>
      public HighLevelCodeInterval() { }<br/>
        <br/>
      public HighLevelCodeInterval(int mn, int mx) <br/>
      {<br/>
        Min = mn; Max = mx;<br/>
      }<br/>
      public int Min { get; set; }<br/>
      public int Max { get; set; }<br/>
    }<br/>
    public class RegionObject <br/>
    {<br/>
      public RegionObject() <br/>
      {<br/>
        PostalCodes = new List<int>();<br/>
      }<br/>
     <br/>
      [BsonId]<br/>
      public int Id { get; set; }<br/>
      public string Name { get; set; }<br/>
      public List<HighLevelCodeInterval> HighLevelCodes { get; set; }<br/>
      public List<int> PostalCodes { get; set; }<br/>
    }<br/>

    Тогда запрос выборки по конкретному коду будет:

    QueryComplete q = Query.EQ("PostalCodes", postCode);<br/>
    MongoCollection coll = GetCollection();<br/>
    RegionObject result = coll.FindOneAs<RegionObject>();<br/>

    Я бы сказал неожиданно просто. Для поиска по диапазонам приведу только сам запрос:

    int nCodeValue = …;<br/>
    Query.And(     <br/>
           Query.LTE("HighLevelCodes.Min", nCodeValue),<br/>
                      Query.GTE("HighLevelCodes.Max", nCodeValue)<br/>
    )<br/>

    Надо признать, что поиска по вложенным массивам в монго предстален очень достойно.

    Идентификаторы


    Драйвер поддерживает 2 встроенных генератора идентификаторов: для Guid и ObjectId типов, т.е. достаточно декорировать свойства одного из этих типов аттрибутом BsonId и дальше всё сделается само. Но идентификатор может быть любым, у меня отлично работают целочисленные идентификаторы, но за их уникальностью следит внешняя по отношению к монго программа. Так же утверждают, есть возможность самом написать генератор:

    public class EmployeeIdGenerator : IIdGenerator <br/>
    {<br/>
      object GenerateId(){ ⋮ }<br/>
      bool IsEmpty(object id) { ⋮ }<br/>
    }<br/>
    public class Employee <br/>
    { <br/>
      [BsonId(IdGenerator = typeof(EmployeeIdGenerator)]<br/>
      public int Id { get; set; }<br/>
      // other fields or properties
    }<br/>

    Сам я правда не пробовал.

    Заключение


    Документо ориентированные базы данных подходят далеко не для всех проектов. Если не получается красиво, то может просто DDD неприменим. Я нашёл что один из моих проектов идеально укладывается в DDD и несказанно рад исчезновению огромного числа таблиц, если всё будет хорошо, то скоро будут тесты производительности в духе MondoDB vs MSSQL на реально работающем приложении.

    PS: Хочу упомянуть утилиту, которая очень помогает посмотреть что записалось в базу в результате выполненных операций – это Mongo Vue

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      0
      «Я бы сказал неожиданно просто.»

      А вы не используете NoRM? Вот именно с него я офигел, как просто с Монго работать из .NET :) Плюс, NoRM не несет никакого over-kill'а в производительности, в отличие от ORM традиционных RDBMS.
        0
        NoRM отстает от официального драйвера по фичам — там до сих пор невозможно работать с шаред инстансами. Очень огорчает, что NoRM развивает только один человек, те же решения для Ruby комитят несколько человек.
          0
          Зато в NoRM есть реально работающий Linq.
            0
            Реализация Linq есть и для стандартного драйвера https://github.com/craiggwilson/fluent-mongo
              0
              Спасибо, не знал. Но как-то странно использовать «довесок к драйверу».
                0
                Думаю что в стандартном драйвере LINQ не имплементирован потому что не может быть имплементирован полность (например join).
                0
                Спасибо, хотел сам писать :)
                0
                NoRM всё равно порядочно допиливать надо, в первую очередь с области поддержки маппинга.
                Не знаю насчёт допиливания стандартного драйвера, надо на него посмотреть.

                Как-то и то и то по сравнению с Mongoid, скажем, очень печально.
                  0
                  Чем же печально то?
                  Помоему active record — это печально (взято с mongoid.org/docs/persistence/):
                  Mongoid::Document
                  class Person
                  include Mongoid::Document
                  field :first_name
                  field :middle_initial
                  field :last_name
                  embeds_one :email
                  embeds_many :addresses
                  end

                  person = Person.where(:first_name => «Dudley»).first
                  person.last_name = «Moore»
                  person.save
                    0
                    Ну LINQ нет, это понятно — у них его никогда не было. Я скорее про
                    1. mongoid.org/docs/associations/ — Relational Associations
                    2. mongoid.org/docs/extras/

                    Особенно Relational Associations, я бы даже сказал, поддержка частичной денормализации.

                    В принципе если я решу использовать Norm и дальше, мы его допилим так или иначе.
                      0
                      Ну если честно не вижу таких прямо невероятных бенефитов, ну есть у меня ссылка на связанный объект в другой коллекции… да и в рамках представленных примеров выглядит это надуманно. Про денормализацию www.mongodb.org/display/DOCS/MongoDB+Data+Modeling+and+Rails#MongoDBDataModelingandRails-ANoteonDenormalization
                        0
                        Ну у меня довольно стандартный пример, у объекта X есть Author, который User.
                        В Norm это делается не очень просто.
                        Плюс по поводу денормализации (пример за пределами способностей даже mongoid): если я храню имя Author'а прямо в объекте X, то хотелось бы, чтобы получившийся объект User мог выдать имя без какого-либо lazy load.

                        Но в принципе в таком виде это тривиально прикрутить, и сделаем.
                0
                Почему 1? Я вижу 80 forks и 6 pull requests.
                А если хочется что-то добавить, почему бы и не добавить?
                0
                А NoRM поддерживает «частичную загрузку документов» (см. 1 часть поста)?
                0
                Интересно, спасибо.
                Чем больше работаю с sql, тем больше заглядываюсь на документо ориентированные базы данных )
                  0
                  Если вы хотите обновить объект, тогда лучше использовать update, а не save целого объекта.
                  Это будет гораздо быстрее.

                  Кстати, интересно, как сработает Query.And если в нем будет несколько условий по одному и тому же полю?
                  Например:
                  Query.And(
                  Query.LTE(«HighLevelCodes.Min», nCodeValue),
                  Query.GTE(«HighLevelCodes.Min», nCodeValue-100)
                  )
                    0
                    Это неправильно, я не помню точно выдаст он ошибку или просто выбенет неправильно, но в данном случае правильный запрос будет
                    var q =Query.LTE(«HighLevelCodes.Min», nCodeValue).GTE(«HighLevelCodes.Min», nCodeValue-100);
                      0
                      Поспешил отправить, правильно:
                      var q =Query.LTE(«HighLevelCodes.Min», nCodeValue).GTE(nCodeValue-100);
                        0
                        Я клоню к тому, что Монга не может построить запрос по одному и тому же полю с условием AND
                        Это пока не реализованно, а данный синтаксис такое написать позволяет.
                    0
                    Есть еще один хитрый момент. Если через js в mongo shell сохранить документ, одно из полей которого целое число, то в bson оно сохранится как double из-за особенностей javascript при работе с числами. Соответственно при попытке получить этот объект из C# получаем эксепшин.

                    Only users with full accounts can post comments. Log in, please.