Violet Tape некоторые мысли о разработке на платформе .Net

26Сен/110

Eloquera V

краткое описание / немного истории / установка / режимы работы базы: service, inmemory, desktop / восстановление базы
конфигурация / программная конфигурация / простейшие операции CRUD / insert / update / refreshMode / delete
построение запросов / joins / параметры / массивы / шаблонные контейнеры / order by / доступные функции / almos
хранимые процедуры
родные и динамические объекты / работа с массивами / контейнеры / фильтрация типов, свойств и полей / backup and restore / эволюция типов / fulltext search / индексация / под капотом

«Родные» и динамические объекты

Под родными объектами в данном случае подразумеваются любые типы принадлежащие CLR или созданные вами. Eloquera может на лету (в runtime) конвертировать динамические объекты в «родные» и обратно.

Внимание!

Для использования этого функционала должно быть открыто соединение к базе.

Основные методы для конвертации:

  • ToNative<ConcretType>()
  • FromNative(variable)

Пример для уже открытой базы по созданию динамического объекта:

 Book book = new Book { DbID = 12345,  Title = "Title Mitle" }
 Dynamic dynObj = new Dynamic(book);
 db.Store(dynObj);
 Book newBook = dynObj.ToNative();

При обновлении данных применяются следующие схемы работы:

  • Обновление через родной тип

0    Динамический тип переводим в «родной» тип

0    Изменяем «родной» тип

0    Обновляем (синхронизируем) динамический тип с помощью FromNative

  • Обновление через динамический тип

0    Существует переменная конкретного типа с данными динамического

0    Изменяем динамический тип

0    Обновляем (синхронизируем) «родной» тип с помощью ToNative<T>(variable)

Пример на первую схему:

@do = db.Query().Where(x => x["Name"] == "John").First();

// переводим в «родной» тип
var user = @do.ToNative();

// обновляем
user.Name = "Frank";

// синхронизируем
@do.FromNative(user);

// сохраняем в базу
db.Store(@do);

Пример на вторую схему:

var @do = new Dynamic();
@do["Name"] = "Frank";
db.Store(@do);

// получаем объект обратно
@do = db.Query().Where(x => x["Name"] == "Frank").First();

// обновляем существующую переменную user «родного» типа
@do.ToNative(user);

 

Работа с массивами

Работа с массивами является одной из фич базы Eloquera.

Основная рекомендация, которую вы можете принять за правило, звучит так: сохранять и получать объекты следует с использованием Dynamic , всю остальную работу делать с оригинальными типами. Другой хорошей рекомендацией является указание от разработчиков использовать именно массивы, а не List<>, Dictionary<,>  -  так как это всего лишь синтаксический сахар.

Лучше всего это продемонстрировать на примерах.  Сначала я покажу, как создавать и сохранять массивы, потом как добавлять новые элементы и обновлять существующие записи.

Создание массивов, никаких неожиданностей:

Dynamic datamix = new Dynamic();
datamix["Number"] = 46;
datamix["Int array"] = new int[] {1442, 7557, 9938, 3988, 16833, int.MaxValue, int.MinValue};
datamix["Object array"] = new Book[] {
               new Book() {
Title = "Atilla",
  Parent = new ParentBook() { Publisher ="Hun" } },
               null,
               new Book() {
Title = "Gengis Khan",
  Parent = new ParentBook() { Publisher ="Europe"}
              }
           };

Перед каким бы то ни было использованием объектов их надо сохранить в базе. И не забываем выставлять режим обновления базы.

db.RefreshMode = ObjectRefreshMode.AlwaysReturnUpdatedValues;
db.Store(datamix);

Как я уже упомянул, при работе с List<T> рекомендуется перевести его в массив перед сохранением.

List<Book> list = new List<Book>();
list.Add(new Book() { Title = "Unseen Academicals", Parent = new ParentBook() { Publisher = "Europe" } });

datamix["Generics"] = list.ToArray();
db.Store(datamix);

В реальных приложениях опять же постоянно приходится добавлять новые элементы в массивы. Вот как это можно проделать с динамическими типами, обратите внимание, что конкретный тип при создании оборачивается в динамический:

Dynamic[] array = datamix["Generics"];
// список используется для простоты работы
List nativeList = new List(array);

// добавляем новый элемент – внимание!
nativeList.Add(new Dynamic(new Book() { Title = "Atilla", Parent = new ParentBook() { Publisher = "Hun" } }));

// сохранение. Переводим список обратно в массив.
datamix["Generics"] = nativeList.ToArray();
db.Store(datamix);

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

array = datamix["Generics"];
nativeList = new List(array);

Dynamic @do = nativeList[0]; // только для примера взят нулевой элемент

// получаем «родной» объект
Book book = @do.ToNative();

// меняем значение полей
book.Title = "Joel on software";
book.Parent.Publisher = "Hun";

// переводим обратно в динамический тип
@do.FromNative(book);

// сохраняем
db.Store(@do);

Так как массивы суть ссылочные объекты и структура массива не изменилась, то можно просто сохранить измененный объект.  Eloquera корректно его опознает в своей структуре и обновит. Если одновременно с изменением были добавлены новые элементы, надо будет передать на сохранение весь массив.

Контейнеры

В базе данных есть поддержка полностью изолированных хранилищ для динамических объектов – контейнеры. Контейнеры позволяют хранить объекты по логическим группам, к примеру, «еда», «книги», «игры».  Еще раз сделаю акцент на том, что контейнеры изолированы, т.е. если один и тот же объект поместить в 2 контейнера, то изменение в объекте в первом контейнере не повлечет за собой изменения объекта во втором контейнере, он останется неизменным.

Сохранение вложенных объектов так же будет сделано по контейнерам и они будут независимыми.

Пример:

const string db_name = "Test";
DB db = new DB("server=(local);password=pwd;options=none;");
db.DeleteDatabase(db_name, true);
db.CreateDatabase(db_name);
db.OpenDatabase(db_name);
db.RefreshMode = ObjectRefreshMode.AlwaysReturnUpdatedValues;

db.Containers.CreateContainer("Electronics");
var electronics = db.Containers["Electronics"];

Dynamic tv = new Dynamic();
tv["Price"] = 649;//dollars
tv["Model"] = "Samsung LA40C650";
tv["Screen"] = 6.40;//inch

electronics.Store(tv);

db.Containers.CreateContainer("Furniture");
var furniture = db.Containers["Furniture"];

Dynamic bed = new Dynamic();
bed["Price"] = 700;//dollars
bed["Type"] = "King";

furniture.Store(bed);

var beds = furniture.ExecuteQuery("SELECT Dynamic WHERE Price > 600");

var tvs = from Dynamic d in electronics where d["Model"] == "Samsung LA40C650" select d;

Естественно, что присутствует возможность получить все контейнеры, созданные в системе. Метод для получения этого списка соответствует принципу наименьшего удивления и носит имя ListContainers.


string[] containerList = db.Containers.ListContainers();

Extras

Движок Eloquera индексирует все поля динамических объектов, однако порой в объекте хранятся сырые данные представляющие собой картинки, зашифрованные данные, сериализованные данные. Эти объекты с вероятностью 99,9% никогда не будут участвовать в запросах. В таких случаях массивы могут быть объявлены как дополнения – они не будут индексированы для увеличения скорости обработки (сохранения) данных.

Для создания дополнения надо добавить знак решетки перед именем.

message = new Dynamic();
message["Type"] = "string";
message["ExpireTime"] = DateTime.UtcNow;

byte[] payload = new byte[512];
r.NextBytes(payload);
message["#Payload"] = payload;//array-attachment

 

Дополнительные возможности

Фильтрация типов

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

Пример использования:

DB db = new DB(connectionString);

db.TypeRules.IgnoreType(typeof(DebugBook)); // Этот тип будет проигнорирован. Используйте до создания базы. 

db.CreateDatabase("Sample");
db.OpenDatabase("Sample");

db.RegisterType(typeof(Book));

db.Close();

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

Важно!

Правила для игнорирования типов надо объявлять до создания базы, иначе вам этот код надо будет прописывать всегда.

Фильтрация свойств и полей

Кроме того, что вы можете указать типы для игнорирования, существует возможность указать конкретные поля, которые не будут сохраняться в базу данных. Свойства указываются в виде лямбда-выражений вида ()=>myClassInstance.MyPropertyToBeIgnored, в качестве параметра для метода IgnoreProperty. После этого все проверки будут проводиться движком базы.

DB db = new DB("server=(local);password=;");

TestTypeDefsOnly test = null;

// Create all rules to be saved before calling CreateDatabase.
db.TypeRules
   .IgnoreProperty(() => test.IntProperty)
   .IgnoreProperty(() => test.DecimalProperty)
   .IgnoreProperty(() => 1 == 0); // Так делать не стоит, хотя исключений и не будет. 

db.CreateDatabase("IgnorePropertyPersistence");

Тестовый класс может выглядеть так:
internal sealed class TestTypeDefsOnly
{
   // Эти свойства будут проигнорированы
   public int IntProperty { get { Assert.Fail(); throw new NotImplementedException(); } }
   public DateTime DateProperty { get { Assert.Fail(); throw newNotImplementedException(); } }

   // Эти свойства будут сохранены
   public string Name { get; set; }
   public int Age { get; set; }
}

 

Backup and restore

Функция резервного сохранения позволяет сделать копию базы без выключения сервера, так же с помощью данной функции можно перемещать данные между разными версиями базы данных. Данные методы актуальны для работы базы Eloquera в серверном режиме.

Пример создания резервной копии:

db = new DB("server=localhost:43963;password=pwd;options=none;");
db.Backup("Backup");

И пример восстановления данных:

db = new DB("server=localhost:43963;password=pwd;options=none;");
//create a new database
db.CreateDatabase("Restored");
//open it
db.OpenDatabase("Restored");
//restore from backup file
db.Restore("Backup");

 

Регистрирование типов

При работе в режиме настольной базы, заблаговременное регистрирование типов  может использоваться для увеличения скорости сохранения данных. Идея состоит в том, чтобы создать базу данных в памяти, с возможностью сохранения на диск, зарегистрировать нужные типы, после чего заново открыть базу  в стандартном режиме. Данный подход может дать прирост производительности в 5-20 раз.

// создаем соединение. Обращаем внимание на options
DB db = new DB("server=(local);password=pwd;options=inmemory,persist;");

// создание базы
db.DeleteDatabase(DBName, true);
db.CreateDatabase(DBName);

db.OpenDatabase(DBName);

// регистрация типов
db.RegisterType(typeof(A1));
db.RegisterType(typeof(A2));
db.RegisterType(typeof(A3));
db.RegisterType(typeof(A4));
db.RegisterType(typeof(A5));
db.RegisterType(typeof(A6));
db.RegisterType(typeof(A7));
//...
db.Close();

// открытие созданной базы в нормальном режиме
db = new DB("server=(local);password=pwd;options=;");
db.OpenDatabase(DBName);

var listOfObjects = db.ExecuteQuery("SELECT A1 WHERE value = 0").Cast().ToList();

 

Эволюция типов

При применении итеративной разработки и поставки продукта не редкостью является ситуация, когда меняются основные типы данных. Такой сценарий часто представляется головной болью для разработчиков и не добавляет радости при работе.

В описанной ситуации на помощь могут прийти динамические объекты. Перевести «родные» объекты в динамические в промежуточной базе. Затем составить в новый тип и записать обратно. Модификация данных в одной и той же базе может быть слишком сложной и громоздкой.

Как пример обновления типов:

class User1{
    public string Username { get; set; }
}

class Content1{
    public string Text { get; set; }
    public User1 Author { get; set; }
    public long AuthorId { get; set; }
}

class User2{
    public string Username { get; set; }
    public string JobTitle { get; set; }
    public int Age { get; set; }
}

class Content2{
    public string Text { get; set; }
    public User2 Author { get; set; }
    public long AuthorId { get; set; }
}

    db.DeleteDatabase(dbName, true);
    db.CreateDatabase(dbName);
    db.OpenDatabase(dbName);

    var u1 = new User1 { Username = "Marcus" };
    var c1 = new Content1 { Text = "Hello, world", Author = u1, AuthorId = 1 };
    var @do = new Dynamic();
    @do.FromNative(c1);  //сохраняем топовый объект
    db.Store(@do);//объект @do должен быть сохранен в базе для правильной работы
    var c2 = @do.ToNative<Content2&rt;(); //получение нового типа
    Assert.IsNotNull(c2);
    Assert.IsNotNull(c2.Author);
    Assert.AreEqual("Marcus", c2.Author.Username);

Full text search

Данная возможность может рассматриваться как немного магии над свойствами объекта.

Идея в том, что можно делать множество трансформаций в свойствах объекта и полученный результат использовать в запросе. Допустим, у нас есть куча товаров, которые описываются километрами текста. По всем товарам можно запустить полнотекстовый поиск следующим образом:

internal string description; 

public string[] Words {
    get {
        return description.Split();
    }
}

Запрос:


SELECT Products WHERE Words CONTAINS 'Jam'

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

Еще пример на работу со свойствами:

double[] prices;

double min_price{get {...}}

select Foo where min_price < 10

Увеличение производительности

Индексация

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

Для того чтобы свойство или поле стало индексируемым необходимо применить к нему атрибут Index.

public class Movie{
   [Index]
   public string Title {get; set;}

   [Index]
   public string StudioName {get; set;}

   [Index]
   public DateTime DateReleased {get; set; }

   public double Budget{get; set;}
}

Если скорости индексов кажется мало, то их можно настроить более детально. Для строковых полей можно указать чувствительность к регистру, культуру, максимальный размер ключа.

[Index(IsCaseSensitive = true, Culture = "en-US", MaxKeySize = 20)]
public string Title {get; set;}

Если указать в качестве культуры null, то можно добиться еще прироста в производительности.

Индексация будет применяться только к вновь сохраненным объектам.

Для динамических объектов индексы создаются всегда для всех полей, кроме тех, которые помечены как «дополнения» (начинаются с решетки).

message = new Dynamic();
message["Type"] = "string";
message["ExpireTime"] = DateTime.UtcNow;

byte[] payload = new byte[512];
r.NextBytes(payload);
message["#Payload"] = payload;// не будет проиндексировано

 

Настройки в конфигурации

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

  • Множество строковых полей в объекте – рекомендуется увеличить в конфигурации параметр VarSectorSize
  • Множество индексируемых полей – увеличить значение поля IndexCacheSize, попробовать поиграть с настройками IndexNodeSize в сторону уменьшения значения.
  • Быстрый жесткий диск – попробуйте уменьшить MemoryFootPrint. Значение более 100 будет считаться за размер в KB, менее 100 будет считаться за проценты.
  • Множество запросов к непроиндексировнным данным – будет полезно увеличить ShallowReadAhead и уменьшить значение ShallowReadThreshold

 

Под капотом

На данный момент база данных Eloquera свободно распространяемый продукт с закрытым исходным кодом. Дизайн базы изначально проектировался для корпоративного (Enterprise) использования, т.е. для баз от 100ГБ и более. Это явно не для встроенного использования, что не отменяет локального использования конечно.

Внутреннее устройство, API, и прочие детали не раскрываются из-за коммерческой тайны, однако общее внутреннее устройство базы известно и показано ниже. Однако следует помнить о том, что это лишь отображение общей идеи, в то время как база данных очень сложный продукт и невозможно указать на диаграмме связи и значимые типы так, чтобы сохранить ясность и хоть сколько-нибудь свободного места на листе формата А3.

Тем не менее, вот как все выглядит внутри:

После примера на эволюцию данных у вас может сложиться впечатление, что внутренне представление всех типов будет  Dynamic с какой-то более строгой проверкой типов, однако это не так. Для хранения «родных» объектов используется специальная мета-таблица для работы с данными.

Это весьма сложная таблица, так как она описывать зависимости между типами, наследие, типы полей, их layout в специальных записях для доступа и т.д.

Мета также используется для сборки и разборки объектов (сборка и разборка написана на MSIL, стандартная сериализация не используется по причине крайней медленности).

Когда новый тип сохраняется, клиент генерирует MSIL код на лету, компилирует его и использует откомпилированный код для сборки и разборки объектов. Получается в сотни раз быстрее, чем стандартная сериализация в .NET. Вот что фактически происходит внутри.

То есть, клиент сам себя наращивает кодом для работы с данным типом.

Комментарии (0) Пинги (0)

Пока нет комментариев.


Оставить комментарий


Нет обратных ссылок на эту запись.