
Перевод статьи с DZone. Оригинал.
Я хочу поделиться своим опытом создания приложений для 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). Полный список доступных методов приведён в официальной документации.
Давайте расширим пользовательскую модель возможностью регистрации пользователей:
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
Продолжение следует

