Применение Tarantool: хранимые процедуры
Перевод статьи с DZone. Оригинал: https://dzone.com/articles/applications-for-tarantool-part-1-stored-procedure.
Я хочу поделиться своим опытом создания приложений для Tarantool, и сегодня мы поговорим об установке этой СУБД, о хранении данных и об обращении к ним, а также о записи хранимых процедур.
Tarantool — это NoSQL/NewSQL-база данных, которая хранит данные в оперативной памяти, но может использовать диск и обеспечивает согласованность с помощью тщательно спроектированного механизма под названием «журнал упреждающей записи» (write-ahead log, WAL). Также Tarantool может похвастаться встроенным LuaJIT-компилятором (JIT — just-in-time), который позволяет выполнять Lua-код.
Первые шаги
Мы рассмотрим создание Tarantool-приложения, реализующего API для регистрации и аутентификации пользователей. Его возможности:
- Регистрация и аутентификация по почте за три этапа: создание аккаунта, подтверждение регистрации и задание пароля.
- Регистрация с помощью учётных данных соцсетей (FB, Google+, ВКонтакте и т. д.).
- Восстановление пароля.
За примером хранимой процедуры для Tarantool мы обратимся к первому этапу, точнее к получению кода подтверждения регистрации. Можете зайти в GitHub-репозиторий и выполнять все действия по ходу повествования.
Устанавливаем Tarantool
В сети есть подробные инструкции по установке под различные ОС. К примеру, для установки Tarantool под Ubuntu вставьте в консоль и выполните этот скрипт:
curl http://download.tarantool.org/tarantool/1.9/gpgkey | sudo apt-key add -
release=`lsb_release -c -s`
sudo apt-get -y install apt-transport-https
sudo rm -f /etc/apt/sources.list.d/*tarantool*.list
sudo tee /etc/apt/sources.list.d/tarantool_1_9.list <<- EOF
deb http://download.tarantool.org/tarantool/1.9/ubuntu/ $release main
deb-src http://download.tarantool.org/tarantool/1.9/ubuntu/ $release main
EOF
sudo apt-get update
sudo apt-get -y install tarantool
Проверим успешность установки, введя tarantool
и войдя в интерактивную консоль администратора.
$ tarantool
version 1.9.0-4-g195d446
type 'help' for interactive help
tarantool>
Здесь вы уже можете попробовать программировать на Lua. Если не знакомы с этим языком, то вот короткое руководство для начала: http://tylerneylon.com/a/learn-lua.
Регистрация по почте
Теперь напишем наш первый скрипт для создания пространства, в котором будут храниться все пользователи. Оно аналогично таблице в реляционной БД. Сами данные хранятся в кортежах (массивах, содержащих записи). Каждое пространство должно иметь один первичный индекс и может иметь несколько вторичных индексов. Индексы могут быть определены по одному или нескольким полям. Вот схема пространства нашего сервиса аутентификации:
Мы используем индексы двух типов: HASH
и TREE
. Индекс HASH
позволяет искать кортежи с помощью полного совпадения первичного ключа, который должен быть уникальным. Индекс TREE
поддерживает неуникальные ключи, позволяет искать по началу составного индекса и организовывать сортировку ключей, поскольку их значения упорядочены внутри индекса.
Пространство session
содержит специальный ключ (session_secret
), используемый для подписывания куков сессии. Хранение ключей сессии позволяет при необходимости разлогинивать пользователей на стороне сервера. Также у сессии есть опциональная ссылка на пространство social
. Это нужно для проверки сессий тех пользователей, которые входят по учётным данным соцсетей (проверяем валидность хранимого токена OAuth 2).
Пишем приложение
Прежде чем начать писать само приложение, давайте взглянем на структуру проекта:
tarantool-authman
├── authman
│ ├── model
│ │ ├── password.lua
│ │ ├── password_token.lua
│ │ ├── session.lua
│ │ ├── social.lua
│ │ └── user.lua
│ ├── utils
│ │ ├── http.lua
│ │ └── utils.lua
│ ├── db.lua
│ ├── error.lua
│ ├── init.lua
│ ├── response.lua
│ └── validator.lua
└── test
├── case
│ ├── auth.lua
│ └── registration.lua
├── authman.test.lua
└── config.lua
Пути, определённые в переменной package.path
, используются для импорта Lua-пакетов. В нашем случае пакеты импортируются относительно текущей директории tarantool-authman
. Но если нужно, то пути импорта легко можно расширить:
-- Prepending a new path with the highest priority
package.path = "/some/other/path/?.lua;” .. package.path
Перед созданием первого пространства давайте положим все необходимые константы в отдельные модели. Нужно задать имя каждому пространству и индексу. Также необходимо определить порядок полей в кортеже. К примеру, так выглядит модель authman/model/user.lua
:
-- Our package is a Lua table
local user = {}
-- The package has the only function — model — that returns a table
-- with the model’s fields and methods
-- The function receives configuration in the form of a Lua table
function user.model(config)
local model = {}
-- Space and index names
model.SPACE_NAME = ‘auth_user’
model.PRIMARY_INDEX = ‘primary’
model.EMAIL_INDEX = ‘email_index’
-- Assigning numbers to tuple fields
-- Note thatLua uses one-based indexing!
model.ID = 1
model.EMAIL = 2
model.TYPE = 3
model.IS_ACTIVE = 4
-- User types: registered via email or with social network
-- credentials
model.COMMON_TYPE = 1
model.SOCIAL_TYPE = 2
return model
end
-- Returning the package
return user
При обработке пользователей нам понадобятся два индекса: уникальный по ID и неуникальный по адресу почты. Когда два разных пользователя регистрируются с учётными данными соцсетей, они могут указать одинаковые адреса или вообще их не указать. А для пользователей, регистрирующихся обычным способом, приложение проверит уникальность почтовых адресов.
Пакет authman/db.lua
содержит метод для создания пространств:
local db = {}
-- Importing the package and calling the model function
-- The config parameter is assigned a nil (empty) value
local user = require(‘authman.model.user’).model()
-- The db package’s method for creating spaces and indexes
function db.create_database()
local user_space = box.schema.space.create(user.SPACE_NAME, {
if_not_exists = true
})
user_space:create_index(user.PRIMARY_INDEX, {
type = ‘hash’,
parts = {user.ID, ‘string’},
if_not_exists = true
})
user_space:create_index(user.EMAIL_INDEX, {
type = ‘tree’,
unique = false,
parts = {user.EMAIL, ‘string’, user.TYPE, ‘unsigned’},
if_not_exists = true
})
end
return db
UUID будет выступать в роли ID пользователя, и для поиска с полным совпадением мы станем использовать индекс HASH
. Индекс для поиска по почте будет состоять из двух частей: (user.EMAIL, ‘string’
) — пользовательский адрес почты, (user.TYPE, ‘unsigned’
) — тип пользователя. Напомню, что типы определяются чуть раньше в модели. Составной индекс позволяет искать не только по всем полям, но и по первой части индекса. Так что мы можем искать только по адресу почты (без типа пользователя).
Войдём в консоль администратора и используем пакет authman/db.lua
.
$ tarantool
version 1.9.0-4-g195d446
type ‘help’ for interactive help
tarantool> db = require(‘authman.db’)
tarantool> box.cfg({})
tarantool> db.create_database()
Отлично, мы только что создали первое пространство. Не забывайте: прежде чем вызывать box.schema.space.create
, нужно с помощью метода box.cfg
сконфигурировать и запустить сервер. Теперь можно внутри созданного пространства выполнять какие-то простые действия:
-- Creating users
tarantool> box.space.auth_user:insert({‘user_id_1’, ‘example_1@mail.ru’, 1})
— -
- [‘user_id_1’, ‘example_1@mail.ru’, 1]
…
tarantool> box.space.auth_user:insert({‘user_id_2’, ‘example_2@mail.ru’, 1})
— -
- [‘user_id_2’, ‘example_2@mail.ru’, 1]
…
-- Getting a Lua table (array) with all the users
tarantool> box.space.auth_user:select()
— -
- — [‘user_id_2’, ‘example_2@mail.ru’, 1]
— [‘user_id_1’, ‘example_1@mail.ru’, 1]
…
-- Getting a user by the primary key
tarantool> box.space.auth_user:get({‘user_id_1’})
— -
- [‘user_id_1’, ‘example_1@mail.ru’, 1]
…
-- Getting a user by the composite key
tarantool> box.space.auth_user.index.email_index:select({‘example_2@mail.ru’, 1})
— -
- — [‘user_id_2’, ‘example_2@mail.ru’, 1]
…
-- Changing the data in the second field
tarantool> box.space.auth_user:update(‘user_id_1’, {{‘=’, 2, ‘new_email@mail.ru’}, })
— -
- [‘user_id_1’, ‘new_email@mail.ru’, 1]
…
Уникальные индексы не позволяют вводить неуникальные значения. Если нужно создать записи, которые уже могут находиться в пространстве, используйте операцию upsert
(update/insert). Полный список доступных методов приведён в официальной документации: https://tarantool.org/doc/1.9/book/box/box_space.html.
Давайте расширим пользовательскую модель возможностью регистрации пользователей:
function model.get_space()
return box.space[model.SPACE_NAME]
end
function model.get_by_email(email, type)
if validator.not_empty_string(email) then
return model.get_space().index[model.EMAIL_INDEX]:select({email, type})[1]
end
end
-- Creating a user
-- Fields that are not part of the unique index are not mandatory
function model.create(user_tuple)
local user_id = uuid.str()
local email = validator.string(user_tuple[model.EMAIL]) and
user_tuple[model.EMAIL] or ‘’
return model.get_space():insert{
user_id,
email,
user_tuple[model.TYPE],
user_tuple[model.IS_ACTIVE],
user_tuple[model.PROFILE]
}
end
-- Generating a confirmation code sent via email and used for
-- account activation
-- Usually, this code is embedded into a link as a GET parameter
-- activation_secret — one of the configurable parameters when
-- initializing the application
function model.generate_activation_code(user_id)
return digest.md5_hex(string.format(‘%s.%s’,
config.activation_secret, user_id))
end
В нижеприведённом коде использованы два стандартных пакета Tarantool — uuid
и digest
— и один созданный пользователем — validator
. Но сначала их нужно импортировать:
-- standard Tarantool packages
local digest = require(‘digest’)
local uuid = require(‘uuid’)
-- Our application’s package (handles data validation)
local validator = require(‘authman.validator’)
При определении переменных мы используем оператор local
, ограничивающий их область видимости текущим блоком. Если так не делать, переменные будут глобальными, а нам этого нужно избегать из-за возможных конфликтов имён.
Теперь создадим основной пакет authman/init.lua
, в котором будут храниться все API-методы:
local auth = {}
local response = require(‘authman.response’)
local error = require(‘authman.error’)
local validator = require(‘authman.validator’)
local db = require(‘authman.db’)
local utils = require(‘authman.utils.utils’)
-- The package returns the only function — api — that configures and
-- returns the application
function auth.api(config)
local api = {}
-- The validator package contains checks for various value types
-- This package sets the default values as well
config = validator.config(config)
-- Importing the models for working with data
local user = require(‘authman.model.user’).model(config)
-- Creating a space
db.create_database()
-- The api method creates a non-active user with a specified email
-- address
function api.registration(email)
-- Preprocessing the email address — making it all lowercase
email = utils.lower(email)
if not validator.email(email) then
return response.error(error.INVALID_PARAMS)
end
-- Checking if a user already exists with a given email
-- address
local user_tuple = user.get_by_email(email, user.COMMON_TYPE)
if user_tuple ~= nil then
if user_tuple[user.IS_ACTIVE] then
return response.error(error.USER_ALREADY_EXISTS)
else
local code = user.generate_activation_code(user_tuple[user.ID])
return response.ok(code)
end
end
-- Writing data to the space
user_tuple = user.create({
[user.EMAIL] = email,
[user.TYPE] = user.COMMON_TYPE,
[user.IS_ACTIVE] = false,
})
local code = user.generate_activation_code(user_tuple[user.ID])
return response.ok(code)
end
return api
end
return auth
Отлично! Теперь пользователи могут создавать аккаунты.
tarantool> auth = require(‘authman’).api(config)
-- Using the api to get a registration confirmation code
tarantool> ok, code = auth.registration(‘example@mail.ru’)
-- This code needs to be sent to a user’s email address so that they
-- can activate their account
tarantool> code
022c1ff1f0b171e51cb6c6e32aefd6ab
Продолжение следует