Alexey Suvorov dev blog

Мой разработческий блог

Archive for Декабрь 2010

MongoDB и официальный cssharp-driver

2 комментария

Для MongoDb существует много драйверов, но вот, не так недавно появился официальный. Насколько я понял он разрабатывается компанией http://www.10gen.com и скачать его можно здесь . В этом посте я не буду рассуждать что лучше и зачем вообще нужен MongoDb, я просто пройдусь по основным действиям и моментам, которые показались мне неочевидными при использовании драйвера.

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

У каждого объекта должен быть идентификатор. К нему предъявляется несколько требований: не должен быть null, не должен быть значение по умолчанию (например 0 для int).  Также идентификатор может быть структурой, например:

public struct Pair
{
public int Low{get;set;}
public int High{get;set;}
}

В базе это будет выглядеть так:
MongoDb csharp-driver

Теоретически идентификатором может быть любой объект который может быть сериализован в BsonValue (а это практически любой объект), но в этом случае перед тем как искать по такому объекту его нужно секонвертировать в BsonDocument или BsonValue. Так же есть ObjectId специальный тип в библиотеке MongoDb.Bson, хранит в себе временную метку, идентификатор компьютера на котором создан и какой то pid (возможно идентификатор процесса), если всё равно что использовать в качестве идентификатора, то стоит рассмотреть использование этого типа, потому что сериализуется он на ура и монго сам присвоит ему уникальное значение при сохранении в базу.

Поле идентификатора выбирается для энтити в соответствии с конвенцией или атрибутом. По умолчанию идентификатором будет поле с именем Id если ни одно другое поле не помечено атрибутом [BsonId]. Подробнее про conventions и атрибуты сериализации можно почитать тут

Поиск объекта по идентификатору:

Поиск по простому идентификатору

public class CustomIdEntity 
{
  public string Id { get; set; }
  public string Name { get; set; }
}

var result = demoCollection.FindOneByIdAs<CustomIdEntity>("alexey.suvorov");

Поиск по составному идентификатору

public struct Pair 
{
  public int Low { get; set; }
  public int High { get; set; }
}

public class CustomIdEntity 
{
  [BsonId]
  public Pair ComplexId { get; set; }
  public string Name { get; set; }
  public string Id { get; set; }
}

var result = demoCollection.FindOneByIdAs<CustomIdEntity>((new Pair() { High = 1, Low = 2 }).ToBsonDocument());

Ещё одна особенность MongoDb — как бы не было названо свойство в энтити в базе оно всегда называется _id, возможно это можно изменить с помощью переопределения маппинга, но по умолчанию следующий код работать не будет:

public class DemoEntity 
{
  [BsonId]
  public ObjectId Id { get; set; }
  public string Name { get; set; }
}

[TestMethod]
public void TestInsertDelete() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  var demoCollection = db.GetCollection<DemoEntity>(typeof(DemoEntity).FullName);
  const string ConstName = "Some name";

  demoCollection.RemoveAll();
  var de1 = new DemoEntity();
  de1.Name = ConstName;
  demoCollection.Insert<DemoEntity>(de1);
  Assert.IsFalse(de1.Id == ObjectId.Empty);
  //тут всё хорошо, потому что драйвер знает как правильно искать по id 
  var objFromDb = demoCollection.FindOneByIdAs<DemoEntity>(de1.Id);
  Assert.IsNotNull(objFromDb);
  Assert.AreEqual(ConstName, objFromDb.Name);
  //а вот тут вернётся null, не смотря на то, что запрос логически безупречен
  var searchResult = demoCollection.FindOneAs<DemoEntity>(Query.EQ("Id", de1.Id));
  //этот Assert не проходит
  Assert.IsNotNull(searchResult);
}

На самом деле невелика беда, но если нужен метод  RemoveById, то писать его нужно как то так

public static class MongoExtensions 
{
  private const string ID_COLUMN_NAME = "_id";
  public static void RemoveById(this MongoCollection collection, BsonValue val) {
    collection.Remove(Query.EQ(ID_COLUMN_NAME, val));
  }
}

Запросы

Данный драйвер не поддерживает LINQ, либо авторы не почувствовали в себе силы реализовать эту функцию, либо они просто сочли что LINQ «не ложиться» на идею документо ориентированной БД и не вписывается в DDD, вопрос спорный, но я тоже не вижу как и зачем может быть реализован join для документо ориентированной базы данных. Классы запросов находятся в пространстве имён MongoDB.Driver.Builders и я успел попробовать не все классы, но уже кое что потребовало копания в сорцах, чтобы выяснить как заставить это работать.

Начнём с поиска на основе данных во вложенных объектах. В mongo действует dot notation, подробнее тут , пример:

public class ChildChild 
{
  public int SomeVal { get; set; }
}

public class Child 
{
  public int Age { get; set; }
  public ChildChild ChildChildInstance { get; set; }
}

public class Parent 
{
  public ObjectId Id { get; set; }
  public string Name { get; set; }
  public Child ChildInstance { get; set; }
}

[TestMethod]
public void TestQueryToChild() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  ClearCollection<Parent>(db);
  const string NameTemplate = "Parent {0}";
  
  var parentCollection = db.GetCollection<Parent>(typeof(Parent).FullName);

  for (int i = 0; i < 10; i++) 
  {
    var cc = new ChildChild() { SomeVal = i };
    var c = new Child() { Age = i, ChildChildInstance = cc};
    var p = new Parent() {
      Name = string.Format(NameTemplate, i),
      ChildInstance = c
    };
    parentCollection.Insert(p);
  }

  //запрос через dot notation
  var resColl = parentCollection.FindAs<Parent>(Query.EQ("ChildInstance.Age", 5));
  Assert.AreEqual(1, resColl.Count());
  Assert.AreEqual(string.Format(NameTemplate, 5), resColl.First().Name);

  //запрос через совпадение объекта
  var childChildDoc = (new ChildChild() { SomeVal = 5 }).ToBsonDocument();
  var resultColl = parentCollection.FindAs<Parent>(Query.EQ("ChildInstance.ChiъldChildInstance", childChildDoc));

  Assert.AreEqual(string.Format(NameTemplate, 5), resultColl.First().Name);
}

Тут есть принципиальная разница между запросами. Запрос через dot notation подразумевает частичное совпадение объекта, т.е. конкретного поля или полей, а вот запрос на основе объекта подразумевает полное совпадение объекта по которому ищем (совпадение бинарных данных после сериализации в BSON).

Соответственно Query.EQ (равно, если кто ещё не понял) возвращает нам CompletedQuery, т.е. не подразумевающий добавления дальнейших условий, тогда как  Query.GT (строго больше) или Query.LTE (меньше равно) возвращают QueryConditionList к которому условия могут быть добавлены, что для меня было не очень очевидно и я сначала пытался сделать проверку на вхождения значения в интервал через Query.And. Правильно сделать:

//постарше, но в рамках 😉
collection.FindAs<Girl>(Query.GT("Age", 25).LT(35));

Вернёмся к LINQ. Все разнообразные Find* методы возвращают MongoCursor;

подробнее о нём тут. По сути это IEnumerable, и к нему подходят стандартные методы LINQtoObject, но они не будут транслированы в запросы на сервере, простой тест:

[TestMethod]
public void TestLinq() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  int nCount = 1000000;
  ClearCollection<DemoPerson>(db);

  var collection = db.GetCollection<DemoPerson>(typeof(DemoPerson).FullName);

  for (int i = 0; i < nCount; i++) 
  {
    collection.Insert(new DemoPerson() { Age = i, Name = string.Format("Name {0}",i) });
  }
  var start = DateTime.Now;
  var res = collection.Find(Query.Null).Where(x => x.Age < 99440 && x.Age < 99450);
  Assert.AreEqual(9, res.ToList().Count);
  var end = DateTime.Now;
  
  var start2 = DateTime.Now;
  var res2 = collection.Find(Query.GT("Age", 99440).LT(99450));
  Assert.AreEqual(9, res2.ToList().Count);
  var end2 = DateTime.Now;
  
  //много секунд и памяти
  var diff = end - start;
  //мало секунд и памяти
  var diff2 = end2 - start2;
}

Так что в запросах разбираться всё таки придётся и тут немного примеров, правда они не на C#.

Заключение

За бортом остались индексы, частичная загрузка объектов и map reduce, но я сам пока до этого не добрался. Хочу так же отметить, что Mongo оставляет более приятное впечатление чем тот же RavenDB от которого веет Nhibernate в каждой строчке. Кода не будет, потому что у приведённых примеров нет общей идеи и целосности, если что то не очень понятно я постараюсь оперативно отвечать в комментариях.

Использованные версии
MongoDb: 1.6.5
Driver: 0.9.0.3992

Written by alexeysuvorov

20.12.2010 at 3:12 дп

Опубликовано в .net, C#, MongoDb