В предыдущей статье мы узнали, как FaunaDB обеспечивает идеальную платформу для программирования, управляемого событиями, предоставляя критически важные ACID-транзакции в глобальном масштабе. Эта статья поможет вам начать использовать FaunaDB для таких транзакций. Если вы новичок в FaunaDB, мы рекомендуем прочитать Документацию FaunaDB CRUD для получения информации о языке запросов FaunaDB, но вам это не нужно, чтобы следовать дальше. Мы рассмотрим только основные запросы FaunaDB из командной строки. (Но следите за обновлениями для следующего учебника в этой серии о том, как запрашивать FaunaDB через полноценное приложение Java.)
В этом руководстве мы рассмотрим добавление записей в книгу вручную, а затем запрос к книге. Если вам нужен только необработанный код, он доступен на странице Event-Sourcing with FaunaDB gist.
Предпосылки
Если вы еще этого не сделали, зарегистрируйте бесплатную учетную запись Fauna.
Затем установите Fauna Shell:
$ npm install -g fauna-shell
После установки скажите Фауне, что хотите войти.
$ fauna cloud-login
Введите учетные данные Fauna, когда будет предложено.
Email: [email protected] Password: **********
Нажмите Ввод. После того, как вы вошли в систему на машине, вам не нужно снова входить в систему. Затем создайте базу данных, в которой будет жить наша книга:
$ fauna create-database ‘main_ledger’
Теперь откройте Fauna Shell в новой базе данных:
$ fauna shell main_ledger
Мы готовы к работе!
Настройка схемы
Схема нашей базы данных состоит из одного класса, называемого бухгалтерская книга. Этот класс содержит уникальный реестр для каждого клиента. В следующем примере показана схема записи бухгалтерской книги:
'{"clientId":50,"counter":10,"type":"DEPOSIT","description": "NEW DEPOSIT", "amount":42.11}'
Сначала создайте класс леджера. Для простоты все данные будут храниться в одном классе леджера:
faunadb> CreateClass({ name: "ledger" })
{
"ref": Class("ledger"),
"ts": 1532019955672424,
"history_days": 30,
"name": "ledger"
}
Наряду с уникальным идентификатором клиента есть счетчик, который является уникальным идентификатором только для записи в книге этого клиента. Комбинация clientId
+ counter
гарантирует, что каждая запись в реестре уникальна.
Далее нам нужно создать два отдельных индекса. Скопируйте и вставьте в оболочку следующее:
faunadb> CreateIndex(
{
name: "UNIQUE_ENTRY_CONSTRAINT",
source: Class("ledger"),
terms: [{ field: ["data", "clientId"] }],
values: [{ field: ["data", "counter"] }],
unique: true
})
Первый индекс применяет ограничение уникальности в реестре с термином плюс значения clientId
и counter
. Мы не можем добавить ссылку на класс в список значений, потому что это сделало бы ограничение уникальности clientId
+ counter
+ ссылка на класс. Этот метод позволит дублировать записи с clientId
+ counter
.
Затем введите в оболочке следующее:
faunadb> CreateIndex(
{
name: "ledger_client_id",
source: Class("ledger"),
terms: [{ field: ["data", "clientId"] }],
values: [{ field: ["data", "counter"], reverse:true }, { field:
["ref"] }],
unique: false,
serialized: true
})
Этот индекс обеспечивает поиск и ссылку на все записи для конкретного клиента, отсортированные по counter
в обратном порядке. При сортировке книги по counter
в обратном порядке мы можем легко найти последнюю запись в книге.
Вы можете убедиться, что индексы были созданы правильно, выполнив запрос, чтобы найти индекс:
faunadb> Get(Index("ledger_client_id"))
{
"ref": Index("ledger_client_id"),
"ts": 1531245138484000,
"active": true,
"partitions": 1,
"name": "ledger_client_id",
"source": Class("ledger"),
"terms": [
{
"field": [
"data",
"clientId"
]
}
],
"values": [
{
"field": [
"data",
"counter"
],
"reverse": true
},
{
"field": [
"ref"
]
}
],
"unique": false,
"serialized": true
}
Добавление записей в книгу
Запросы FaunaDB строятся с использованием одного или нескольких вложенных выражений. Каждое выражение возвращает выражение, поэтому они могут быть вложены друг в друга. В этом примере показано, как построить эти вложенные выражения.
Предположим, что клиент выполняет вызов для вставки записи в книгу для clientId
50, а последняя запись в книге имеет counter
из 20.
Основным выражением для вставки события реестра будет выражение «создать класс». Данные — это значение экземпляра класса в формате json:
Create(Class("ledger"),
{ data:
{"clientId":50,"counter":21,"type":"DEPOSIT","description":
"NEW DEPOSIT", "amount":28.19} })
Добавьте несколько записей в бухгалтерскую книгу в качестве отправной точки, обновляя счетчик для каждой из них.
faunadb> Create(Class("ledger"),
{ data: {"clientId":50,"counter":0,"type":"DEPOSIT",
"description":"NEW DEPOSIT", "amount":28.19} })
{
"ref": Ref(Class("ledger"), "205271124881179148"),
"ts": 1532020649624717,
"data": {
"clientId": 50,
"counter": 0,
"type": "DEPOSIT",
"description": "NEW DEPOSIT",
"amount": 28.19
}
}
Обратите внимание, что если мы попытаемся ввести повторяющиеся записи, «создать» не удастся:
faunadb> Create(Class("ledger"),
{ data: {"clientId":50,"counter":0,"type":"DEPOSIT",
"description":"NEW DEPOSIT", "amount":28.19} })
Error: instance not unique
Поскольку выражения возвращают структуру данных, все запросы FaunaDB могут быть вложены для выбора значений из возвращаемых выражений. В этом случае мы хотим, чтобы counter
было сохранено, поэтому мы можем использовать выбор, чтобы вернуть только counter
из результатов с помощью выбора. По сути, выбор говорит «выберите элемент данных массива, а затем из этого массива выберите значение counter
»:
faunadb> Select(["data", "counter"], Create(Class("ledger"),{
data:
{"clientId":50,"counter":5,"type":"DEPOSIT","description":"NEW DEPOSIT", "amount":28.19} }))
5
Поиск последней записи в бухгалтерской книге
Мы можем использовать это основное выражение для создания запроса, который вставляет запись в книгу только в том случае, если counter
равно единице плюс последняя запись. Это гарантирует, что у клиента есть последние counter
, а события вставлены по порядку.
Теперь давайте создадим запрос, который получает последнее значение counter
из индекса ledger_client_id
. Первой частью этого запроса будет чтение первой страницы записей из индекса с разбиением на страницы. Это возвращает вложенный массив индексированных записей вместе со ссылкой на экземпляры класса, например:
faunadb> Paginate(Match(Index("ledger_client_id"), 50))
{
"data": [
[
5,
Ref(Class("ledger"), "205271417149719052")
],
[
0,
Ref(Class("ledger"), "205271124881179148")
]
]
}
Обратите внимание, что записи возвращаются в порядке, обратном значению counter
. Это связано с тем, что индекс был создан с флагом реверса, установленным в значение true. Это сохраняет значения, отсортированные в обратном порядке.
Затем выберите counter
из первой записи из этого двумерного массива. Вы можете сделать это, сославшись на нужную запись в значении вложенного массива, аналогично тому, как это делается в других языках программирования. Последний параметр 0 является значением по умолчанию:
faunadb> Select([0,0],
Paginate(Match(Index("ledger_client_id"), 50)), 0
)
Это возвращает счетчик последней записи в бухгалтерской книге или 0, например:
5
Теперь добавьте к этому значению единицу и сохраните ее во временной переменной latest
, которая будет использоваться позже в запросе. Это можно сделать с помощью выражений Add
и Let
. Let
связывает значения с переменными для ссылки в последующих частях запроса:
faunadb> Let(
{latest: Add(
Select([0,0],
Paginate(Match(Index("ledger_client_id"), 50)),0
),1)
},
Var("latest")
)
6
Второй параметр Let
— это выражение, которое запускается после связывания переменных. В этом случае мы просто возвращаем привязку переменной. Выполнение только этого запроса установит переменную latest
, а затем вернет переменную latest
, которая равна 5, поскольку 4 является последним counter
(4+1).
Создание условной записи в книге
При вставке записи убедитесь, что вставляемый counter
правильный. Это можно сделать с помощью выражения «если». Упрощенный пример:
faunadb> If(Equals(20, 20),
["saved", 7],
["not_saved",9]
)
[ 'saved', 7 ]
Это возвращает флаг, указывающий, была ли запись сохранена, и используемый counter
; или, в случае ошибки, counter
, который должен был быть сохранен.
[ 'not_saved', 9 ]
Из этого массива можно вернуть любое значение, и в данном случае мы хотим вернуть сохраненное counter
:
faunadb> If(
Equals(5, 5),
["saved",
Select(["data", "counter"], Create(Class("ledger"),
{ data: {"clientId":50,"counter":5,"type":"DEPOSIT",
"description":"NEW DEPOSIT", "amount":28.19} }))
],
["not_saved",6]
)
В случае успеха вы увидите:
[ 'saved', 5 ]
Или, если это не удалось:
[ 'not_saved', 6 ]
Собираем все вместе
Наконец, давайте объединим все это в одном запросе. Мощная функция FaunaDB заключается в том, что все это можно объединить в один запрос, который выполняется как одна атомарная операция. Что он делает, так это получает последнюю запись бухгалтерской книги counter
и проверяет, является ли добавляемое нами counter
ожидаемым значением. Только потом делаем вставку:
faunadb> Let(
{latest: Add(
Select([0,0],
Paginate(Match(Index("ledger_client_id"), 50)),0
),1),
counter: 7
},
If(Equals(Var("counter"), Var("latest")),
["saved",
Select(["data", "counter"], Create(Class("ledger"),
{ data: {"clientId":50,"counter":Var("counter"),"type":
"DEPOSIT","description":"NEW DEPOSIT", "amount":28.19} }))
],
["not_saved",Var("latest")]
)
)
[ 'saved', 7]
Это много для обработки, но держитесь и повторите это пару раз! Обратите внимание, что выражение Var("latest")
используется для доступа к привязке переменной. Разбивая это, вот что делают основные части:
Let
создает привязки к переменнымlatest
иcounter
. ПеременнаяCounter
в реальном приложении будет значением счетчика бухгалтерской книги, которое передается клиентом в запрос.- Затем
Let
выполняет запрос «Если», чтобы проверить, чтоcounter
иlatest
являются одним и тем же значением. - Если
counter
иlatest
имеют одно и то же значение, оператор if будет оцениваться как истина, создавать экземпляр класса и возвращатьcounter
. - Если
counter
иlatest
не совпадают, возвращается ошибка.
В этом примере запись была сохранена, поэтому запрос возвращает флаг saved
и новое значение counter
, чтобы указать, что запись книги успешно сохранена.
Резюме
Хотя эти запросы FaunaDB на первый взгляд кажутся довольно сложными, они инкапсулируют множество бизнес-логики в одну транзакцию, которая была бы значительно сложнее на уровне приложений. Большинство баз данных не поддерживают вложенные запросы, позволяющие совмещать чтение и запись в одной транзакции. Таким образом, выполнение одной и той же бизнес-логики на уровне приложений потребует нескольких запросов к базе данных и некоторого типа блокировки, чтобы гарантировать, что счетчик не обновится после считывания значения.
В этом руководстве мы обрисовали в общих чертах ядро модели источника событий, необходимой для создания полного приложения для поиска событий с помощью FaunaDB. В следующем уроке мы возьмем эти значения и используем их в полном приложении Java. Для краткого обзора взгляните на этот репозиторий.
Особая благодарность моему коллеге Бену Эдвардсу за помощь в написании и корректуре этой статьи.
Автор:Райан Найт
Дата:25 июля 2018 г.
Первоначально опубликовано в blog. фауна.com.