В предыдущей статье мы узнали, как 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") используется для доступа к привязке переменной. Разбивая это, вот что делают основные части:

  1. Let создает привязки к переменным latest и counter. Переменная Counter в реальном приложении будет значением счетчика бухгалтерской книги, которое передается клиентом в запрос.
  2. Затем Let выполняет запрос «Если», чтобы проверить, что counter и latest являются одним и тем же значением.
  3. Если counter и latest имеют одно и то же значение, оператор if будет оцениваться как истина, создавать экземпляр класса и возвращать counter.
  4. Если counter и latest не совпадают, возвращается ошибка.

В этом примере запись была сохранена, поэтому запрос возвращает флаг saved и новое значение counter, чтобы указать, что запись книги успешно сохранена.

Резюме

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

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

Особая благодарность моему коллеге Бену Эдвардсу за помощь в написании и корректуре этой статьи.

Автор:Райан Найт
Дата:
25 июля 2018 г.
Первоначально опубликовано в blog. фауна.com.