Archive for Декабрь 2010
MongoDB и официальный cssharp-driver
Для MongoDb существует много драйверов, но вот, не так недавно появился официальный. Насколько я понял он разрабатывается компанией http://www.10gen.com и скачать его можно здесь . В этом посте я не буду рассуждать что лучше и зачем вообще нужен MongoDb, я просто пройдусь по основным действиям и моментам, которые показались мне неочевидными при использовании драйвера.
Идентификаторы
У каждого объекта должен быть идентификатор. К нему предъявляется несколько требований: не должен быть null, не должен быть значение по умолчанию (например 0 для int). Также идентификатор может быть структурой, например:
public struct Pair { public int Low{get;set;} public int High{get;set;} }
В базе это будет выглядеть так:
Теоретически идентификатором может быть любой объект который может быть сериализован в 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