Alexey Suvorov dev blog

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

Archive for Октябрь 2011

RavenDB в реальной жизни

6 комментариев

Intro

Хочу поделиться опытом полученным при внедрении nosql в небольшой реальный проект. Взаимодействие с базой в этом проекте ограничивалось тремя операциями : быстрое создание большого количества записей без контента, проход по записям и загрузка контента, изменение полей большого количества записей отвечающих условиям.

Столкнувшись с невозможностью продолжать разработку на mongodb из за глобального лока, который тот ставит при записи на всю базу (при активной записи база практически недоступна на чтение) выбор пал на ravendb. Вначале Ravendb производит ощущение сырого продукта: LINQ поддерживается только для совсем простых случаев, из инструментов мониторинга – веб страничка с silverlight приложением, очень строгие лимиты на количество выбираемых за раз объектов и чтений/записей в рамках сессии, информацию приходится собирать из конференций и половина решений уже устарела, но потом всё встаёт на свои места.

Выборка коллекции документов по коллекции ID

Есть кое что, что nosql умеет лучше всего – операция вставки. В моём случае даже если есть разинца с mongodb по скорости, то она незаметна. Задача была обрабатывать массив данных из внешнего хранилища, соответственно входящие данные могли уже быть в базе и это нужно было верифицировать. Тут появилась первая проблема: ravendb не поддерживает .Contains() в LINQ. Решение нашлось достаточно быстро:

string req = string.Join(" OR ", items.Select(x => x.ToString()).ToArray());
string query = string.Format("PropertyName:{0}",req);
IDocumentStore store = ...//resolve initialized doc store
using(var s = store.OpenSession()){
    IEnumerable<SomeEntity> items = s.Advanced.LuceneQuery<SomeEntity>().Where(query);
}

Размер порции ID который приходил не верификацию позволял делать запрос через форматирование строки, но данное решение далеко не универсально. Сразу немного строгой типизации:

public static string CreateQueryString<T, U>(Expression<Func<T, U>> action, U par) 
where T : class 
{
    var expression = (MemberExpression)action.Body;
    string name = expression.Member.Name;
    return string.Format("{0}:{1}",name, par);
}
public static string CreateQueryString<T, U>(Expression<Func<T, U>> action, IEnumerable< U> par) 
where T : class 
{
    var expression = (MemberExpression)action.Body;
    string name = expression.Member.Name;
    string req = string.Join(" OR ", par.Select(x => x.ToString()).ToArray());
    return string.Format("{0}:({1})",name, req);
}
//somewhere
string query = CreateQueryString((SomeEntity o) => o.PropertyName, tmpId);
IEnumerable<SomeEntity> items = s.Advanced.LuceneQuery<SomeEntity>().Where(query);

Выборка большого количества записей

Особенность ravndb – ограничение на количество возвращаемых объёктов. Ограничение понятное и разумное, особенно для web приложений, но для задачи заполнения пустых объектов контентом загружаемым отдельно это неудобно, поэтому я использовал ленивый Enumerable чтобы затягивать записи по мере необходимости. CollectionEmumerable принимает функцию, которая возвращает данные и количество объектов возвращаемых за раз. Код:

public class CollectionEmumerable<T> : IEnumerable<T>
{
    private readonly Func<int, int, IEnumerable<T>> _f;
    private readonly int _size;
    public CollectionEmumerable(Func<int, int, IEnumerable<T>> f, int size = 50)
    {
        _f = f;
        _size = size;
    }
    public IEnumerator<T> GetEnumerator()
    {
        return new CollectionEmumerator<T>(_f, _size);
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
public class CollectionEmumerator< U> : IEnumerator< U> {
    private readonly Func<int, int, IEnumerable< U>> _f;
    private List< U> _items;
    private int _skip;
    private readonly int _take;
    private int _curr;
    public CollectionEmumerator(Func<int, int, IEnumerable< U>> f, int take) {
        _f = f;
        _take = take;
        LoadNext();
    }
    private void LoadNext() {
        _items = _f(_skip, _take).ToList();
        _skip += _take;
        _curr = -1;
    }
    public void Dispose() { }
    public bool MoveNext() {
        _curr++;
        if (_curr == _take) { 
            LoadNext();
            _curr = 0;
        }
        return _items.Count > 0 && _curr < _items.Count;
    }
    public void Reset() {
        _skip = 0;
        LoadNext();
    }
    public U Current {
        get { return _items[_curr]; }
    }
    object IEnumerator.Current {
        get { return Current; }
    }
}

Использование:

IDocumentStore store = ...//resolve initialized doc store
var f = new Func<int, int, IEnumerable<SomeEntity>>((skip, take) => {
    using(var s = store.OpenSession()){
        IEnumerable<SomeEntity> res = s.Query<SomeEntity>()
                                       .Where(x=>x.SomeProperty == "123")
                                       .Skip(skip)
                                       .Take(take);
        return res;
    }
});
IEnumerable<SomeEntity> items = new CollectionEmumerable<EstateType>(f);

Открывать сессию на каждую выборку необходимо из за ограничения в 30 roundtrip(буду признателен если кто то напишет в комментариях как грамотно перевести на русский это слово) в рамках одной сессии. В моём случае порядок был неважен, потому что мне нужны все данные соответствующие условию, но если важен порядок, то выборку нужно делать по индексу.

Удаление большого количества записей

Тут действуют те же правила – не больше 30 удалений в сессию, при этом до того как удалить объекты их ещё нужно загрузить в память. Неудобно. Медленно. Удалить большое количество объектов можно с использованием индекса по этому объекту. Тут я бы хотел сделать небольшое отступление по поводу индексов.

В ravendb индекс на выборку нужно использовать всегда. Если нет индекса соответствующего выборке – база создаст его. Если не вдаваться в детали, то ravendb удобно представлять в ванильных инженерных мечтах как 2 независимых хранилища: непосредственно документы и хранилище с индексами. Индексы можно создавать через клиента:

var def = new IndexDefinitionBuilder<SomeEntity> 
          { 
              Map = docs => from doc in docs select new { doc.SomeProperty } 
          };
ds.DatabaseCommands.PutIndex("some/indexname",def);

Я не считаю создание индексов в коде приложения хорошей практикой, они должны быть известны на момент разработки и код их создания должен быть вынесен в отдельную утилиту, так что я использую динамическое создание индексов в целях тестирования. Как проверять наличие индекса правильно я так и не нашёл, так что опять же для тестов я проверяю так:

if (!ds.DatabaseCommands.GetIndexNames(0, 1000).Contains("some/indexname")) { 
   //then create index 
}

Вернёмся к удалению

var ds = ...//resolve initialized doc store
using (var s = ds.OpenSession())
{
    IndexQuery query = new IndexQuery(); //all
    s.Advanced.DatabaseCommands.DeleteByIndex("Aggregator/Status",query);
    s.SaveChanges();
}

Или удаление с использованием условия:

var ds = ...//resolve initialized doc store
using (var s = ds.OpenSession())
{
    //delete entity with id  1 and 2 and 3
    IndexQuery query = CreateQuery((SomeEntity t)=>t.Id, new []{1,2,3}); 
    s.Advanced.DatabaseCommands.DeleteByIndex("SomeEntity/allEntityIndex",query);
    s.SaveChanges();
}
//Где то в коде
public static IndexQuery CreateQuery<T, U>(Expression<Func<T, U>> action, U par) 
    where T : class 
{
    return new IndexQuery { Query = CreateQueryString(action, par) };
}
public static IndexQuery CreateQuery<T, U>(Expression<Func<T, U>> action, IEnumerable< U> par) 
    where T : class 
{
    return new IndexQuery { Query = CreateQueryString(action, par) };
}

Ещё одна особенность работы с индексами – индексы не пересчитываются мгновенно. Для уверенности что выберутся актуальные данные есть набор функций WaitForNonStaleResults*. В тестах для уверенности что мой индекс по которому я удаляю/модифицирую коллекцию готов я использую следующий код:

var ds = ...//resolve initialized doc store
using (var s = ds.OpenSession()) {
    //will wait 1h for index           
    var tmp = s.Advanced.LuceneQuery<SomeEntity>().WaitForNonStaleResults(TimeSpan.FromHours(1d)).First();
    //TestEntity index is ready
}

Обновление большого количества документов

Тут те же проблемы что и с удалением. Загружать документы для модификации долго, за сессию не больше 30 документов. В ravendb есть специальный механизм для таких случаев:

//using (var s = ds.OpenSession()) e.t.c.
//where SomeEntity.PropertyName == 1
IndexQuery q = CreateQuery((SomeEntity t) => t.PropertyName, 1);
PatchRequest[] p = new [] { 
                           //SomeEntity.AnotherProperty = "new value"
                           CreatePatch((SomeEntity t) => t.AnotherProperty, "new value")
                          };
s.Advanced.DatabaseCommands.UpdateByIndex("some/indexName", q, p, false);
s.SaveChanges();
//Где то в коде
public static PatchRequest CreatePatch<T, U>(Expression<Func<T, U>> action, U par) 
    where T : class 
{
    var expression = (MemberExpression)action.Body;
    string name = expression.Member.Name;
    var res = new PatchRequest {
        Type = PatchCommandType.Set,
        Name = name,
        Value = RavenJToken.FromObject(par)
    };
    return res;
}

Итого

Помоему разработчикам ravendb просто нехватает времени, чтобы имплементировать нормальную поддержку LINQ и интерес к этой БД cущественно ниже чем к mongodb из за моноплотформенности, хотя я считаю что идеи реализованные в ravendb очень грамотные и со своей стороны буду продолжать использовать именно ravendb в собственных проектах (по крайней мере до тех пор пока mongodb не избавится от невменяемого глобального лока)

Written by alexeysuvorov

21.10.2011 at 10:47 дп

Опубликовано в Uncategorized