Статьи

Назад в будущее с Datomic

В начале марта Рич Хики и его команда выпустили Datomic . Datomic — это новая система распределенных баз данных, разработанная для обеспечения масштабируемых, гибких и интеллектуальных приложений, работающих на облачных архитектурах следующего поколения. Его запуск был окружен довольно жужжанием и скептицизмом , главным образом связанным с его довольно разрушительным архитектурным предложением. Вместо того, чтобы пытаться подытожить различные плюсы и минусы его архитектурного подхода, я попытаюсь сосредоточиться на другом нововведении, которое оно представляет, а именно на его мощной модели данных (основанной на концепции Datoms ) и его выразительном языке запросов (основанном на концепции из Datalog). В оставшейся части этой статьи будет описано, как хранить факты и запрашивать их с помощью выражений и правил Datalog. Кроме того, я покажу, как Datomic вводит явное понятие времени , которое позволяет выполнять запросы как к предыдущему, так и к будущему состоянию базы данных. В качестве примера я буду использовать очень простую модель данных, способную описывать генеалогическую информацию. Как всегда, полный исходный код можно найти в общедоступном GitHub-хранилище Datablend .

 

1. Datomic модель данных

Datomic хранит факты (т. Е. Ваши данные) в виде данных . Datom представляет сложение (или сокращение) отношения между сущностью , атрибутом , значением и транзакцией . Концепция Datom тесно связана с концепцией RDF-тройки , где каждая тройка является утверждением о конкретном ресурсе в форме выражения субъект-предикат-объект . Datomic добавляет понятие времени , явно помечая данные с помощью идентификатора транзакции (то есть точного момента времени, в который факт был сохранен в базе данных Datomic). Это позволяет Datomic продвигать неизменность данных: обновления не изменяют ваши существующие факты; они просто создают новые данные, помеченные более поздней транзакцией. Следовательно, система отслеживает все факты, навсегда.

Datomic не применяет явную схему сущности ; Пользователь должен решить, какой тип атрибутов он / она хочет сохранить для конкретной сущности. Атрибуты являются частью метамодели Datomic, которая определяет характеристики (то есть атрибуты) самих атрибутов. Наш генеалогический пример модели данных хранит информацию о людях и их предках. Для этого нам потребуются два атрибута: имя и родитель . Атрибут в основном сущность, выраженная в терминах встроенной системы атрибутов , такие как мощность , тип значения и описание атрибутов .

// Open a connection to the database
String uri = "datomic:mem://test";
Peer.createDatabase(uri);
Connection conn = Peer.connect(uri);
// Declare attribute schema
List tx = new ArrayList();
tx.add(Util.map(":db/id", Peer.tempid(":db.part/db"),
                ":db/ident", ":person/name",
                ":db/valueType", ":db.type/string",
                ":db/cardinality", ":db.cardinality/one",
                ":db/doc", "A person's name",
                ":db.install/_attribute", ":db.part/db"));
tx.add(Util.map(":db/id", Peer.tempid(":db.part/db"),
                ":db/ident", ":person/parent",
                ":db/valueType", ":db.type/ref",
                ":db/cardinality", ":db.cardinality/many",
                ":db/doc", "A person's parent",
                ":db.install/_attribute", ":db.part/db"));
// Store it
conn.transact(tx).get();

Все сущности в базе данных Datomic должны иметь внутренний ключ, называемый идентификатором сущности . В нашем случае мы генерируем временный идентификатор с помощью метода утилиты tempid. Все объекты хранятся в определенном разделе базы данных, который группирует логически связанные объекты. Определения атрибутов должны находиться в разделе: db.part / db, выделенном системном разделе, используемом исключительно для хранения системных объектов и определений схемы. : person / name является однозначным атрибутом строки типа значения. : person / parent — многозначный атрибут типа значения ref. Значение ссылочного атрибута указывает на (id) другого объекта, хранящегося в базе данных Datomic. Как только наша схема атрибутов будет сохранена, мы можем начать заполнять нашу базу данных конкретными сущностями человека.

// Define person entities
List tx = new ArrayList();
Object edmond = Peer.tempid(":db.part/user");
tx.add(Util.map(":db/id", edmond,
                ":person/name", "Edmond Suvee"));
Object gilbert = Peer.tempid(":db.part/user");
tx.add(Util.map(":db/id", gilbert,
                ":person/name", "Gilbert Suvee",
                ":person/parent", edmond));
Object davy = Peer.tempid(":db.part/user");
tx.add(Util.map(":db/id", davy,
                ":person/name", "Davy Suvee",
                ":person/parent", gilbert));
// Store them
conn.transact(tx).get();

Мы создадим три конкретных человека: я , мой папа Гилберт Суви и мой дедушка Эдмонд Суви . Подобно определению атрибутов, мы снова используем метод служебной программы tempid для получения временных идентификаторов для наших вновь созданных объектов. Однако на этот раз мы храним наших сотрудников в разделе базы данных: db.part / user, который является разделом по умолчанию для хранения сущностей приложения. Каждому человеку дают имя (через атрибут: person / name) и родителя (через атрибут: person / parent). При вызове метода транзакции каждая сущность переводится в набор отдельных данных, которые вместе описывают сущность. После сохранения Datomic гарантирует, что временные идентификаторы будут заменены их окончательными аналогами.

 

2. Datomic язык запросов

Модель запросов Datomic — это расширенная форма Datalog . Datalog — это система дедуктивных запросов, которая будет хорошо знакома людям, имеющим опыт работы с SPARQL и / или Prolog . В декларативном языке запросов используется механизм сопоставления с образцом для поиска всех комбинаций значений (т. Е. Фактов), которые удовлетворяют определенному набору условий, выраженных в виде предложений. Давайте посмотрим на несколько примеров запросов:

// Find all persons
System.out.println(Peer.q("[:find ?name " +
                           ":where [?person :person/name ?name] ]", conn.db()));
// Find the parents of all persons
System.out.println(Peer.q("[:find ?name ?parentname " +
                           ":where [?person :person/name ?name] " +
                                  "[?person :person/parent ?parent] " +
                                  "[?parent :person/name ?parentname] ]" , conn.db()));
// Find the grandparent of all persons
System.out.println(Peer.q("[:find ?name ?grandparentname " +
                           ":where [?person :person/name ?name] " +
                                  "[?person :person/parent ?parent] " +
                                  "[?parent :person/parent ?grandparent] " +
                                  "[?grandparent :person/name ?grandparentname] ]" , conn.db()));

Мы считаем, что сущности имеют тип
person, если они владеют атрибутом: person / name. Часть: where-часть первого запроса, целью которого является поиск всех лиц, хранящихся в базе данных Datomic, определяет следующее «условное» предложение: [? Person: person / name? Name]. «person» и «name» являются переменными, которые действуют как заполнители. Механизм запросов Datalog извлекает все факты (т.е. данные), которые соответствуют этому предложению. Часть: find-части запроса указывает «значения», которые должны быть возвращены в результате запроса.

     
Result query 1: [["Davy Suvee"], ["Edmond Suvee"], ["Gilbert Suvee"]]

Второй и третий запросы направлены на поиск родителей, бабушек и дедушек всех лиц, хранящихся в базе данных Datomic. Эти запросы задают несколько предложений, которые
решаются с помощью
унификации : когда имя переменной используется более одного раза, оно должно представлять одно и то же значение в каждом предложении, чтобы удовлетворять общему набору предложений. Как и следовало ожидать, только
Дэви Suvee был идентифицирован как имеющие прародитель, так как необходимые факты , чтобы удовлетворить этот запрос не доступны ни для
Gilbert Suvee и
Эдмон Suvee .

Result query 2: [["Gilbert Suvee" "Edmond Suvee"], ["Davy Suvee" "Gilbert Suvee"]]
Result query 3: [["Davy Suvee" "Edmond Suvee"]]

Если несколько запросов требуют этого понятия «прародителя», можно определить правило многократного использования,
которое инкапсулирует требуемые предложения. Правила можно гибко комбинировать с предложениями (и другими правилами) в части запроса: where. Наш третий запрос может быть переписан с использованием следующих правил и положений:

String grandparentrule = "[ [ (grandparent ?person ?grandparent) [?person :person/parent ?parent] " +
                                                                "[?parent :person/parent ?grandparent] ] ]";
System.out.println(Peer.q("[:find ?name ?grandparentname " +
                           ":in $ % " +
                           ":where [?person :person/name ?name] " +
                                  "(grandparent ?person ?grandparent) " +
                                  "[?grandparent :person/name ?grandparentname] ]" , conn.db(), grandparentrule));

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

String ancestorrule = "[ [ (ancestor ?person ?ancestor) [?person :person/parent ?ancestor] ] " +
                        "[ (ancestor ?person ?ancestor) [?person :person/parent ?parent] " +
                                                       "(ancestor ?parent ?ancestor) ] ] ]";
System.out.println(Peer.q("[:find ?name ?ancestorname " +
                           ":in $ % " +
                           ":where [?person :person/name ?name] " +
                                  "[ancestor ?person ?ancestor] " +
                                  "[?ancestor :person/name ?ancestorname] ]" , conn.db(), ancestorrule));

Result query 4: [["Gilbert Suvee" "Edmond Suvee"], ["Davy Suvee" "Edmond Suvee"], ["Davy Suvee" "Gilbert Suvee"]]

 

3. Назад в будущее я

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

System.out.println(Peer.q("[:find ?name ?parentname " +
                           ":where [?person :person/name ?name] " +
                                  "[?person :person/parent ?parent] " +
                                  "[?parent :person/name ?parentname] ]", conn.db().asOf(getCheckPoint(checkpoint))));

     
Result query 2: [["Gilbert Suvee" "Edmond Suvee"]]

 

4. Назад в будущее II

Datomic также позволяет прогнозировать будущее. Ну, вроде… Как и в методе asOf, можно использовать метод with для получения версии базы данных, которая расширяется за счет списка еще не совершенных транзакций. Это позволяет выполнять запросы к будущим состояниям базы данных и наблюдать за последствиями, если эти новые факты будут добавлены.

List tx = new ArrayList();
tx.add(Util.map(":db/id", Peer.tempid(":db.part/user"),
                ":person/name", "FutureChild Suvee",
                ":person/parent", Peer.q("[:find ?person :where [?person :person/name \"Davy Suvee\"] ]", conn.db()).iterator().next().get(0)));
        
System.out.println(Peer.q("[:find ?name ?ancestorname " +
                           ":in $ % " +
                           ":where [?person :person/name ?name] " +
                                  "[ancestor ?person ?ancestor] " +
                                  "[?ancestor :person/name ?ancestorname] ]" , conn.db().with(tx), ancestorrule));

Result query 4: [["FutureChild Suvee" "Edmond Suvee"], ["FutureChild Suvee" "Gilbert Suvee"],
                 ["Gilbert Suvee" "Edmond Suvee"], ["Davy Suvee" "Edmond Suvee"],
                 ["Davy Suvee" "Gilbert Suvee"], ["FutureChild Suvee" "Davy Suvee"]]

 

5. Заключение

Использование Datoms и Datalog позволяет вам выражать простые, но мощные запросы. В этой статье представлена ​​лишь часть функций, предлагаемых Datomic. Чтобы лучше познакомиться с различными функциями Datomic, я реализовал Tinkerpop Blueprints API поверх Datomic. Поступая таким образом, вы в основном получаете распределенную , временную базу данных графа , который, насколько я знаю, уникальный в экосистеме базы данных Graph. Исходный код этой реализации Blueprints в настоящее время можно найти в общедоступном GitHub-хранилище Datablend, и вскоре он будет объединен с проектом Tinkerpop.