Применение Tarantool: хранимые процедуры

image


Перевод статьи с 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-код.


24g-tqoiw_sncacpmfniiukpzlg.png


Первые шаги


Мы рассмотрим создание 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.


Регистрация по почте


Теперь напишем наш первый скрипт для создания пространства, в котором будут храниться все пользователи. Оно аналогично таблице в реляционной БД. Сами данные хранятся в кортежах (массивах, содержащих записи). Каждое пространство должно иметь один первичный индекс и может иметь несколько вторичных индексов. Индексы могут быть определены по одному или нескольким полям. Вот схема пространства нашего сервиса аутентификации:


s2fy2xf4pyilnsatx0o7vuqceb4.png


Мы используем индексы двух типов: 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


Продолжение следует

© Habrahabr.ru