Изменение схемы данных с помощью space:format()¶
В этом руководстве рассказано, как разработать типовое приложение в Tarantool DB и изменить в нем схему данных, используя метод space:format(). В качестве примера используется база данных для системы управления проектами. Для работы используются модули CRUD и vshard.
Руководство включает следующие шаги:
Пререквизиты¶
Для выполнения примера требуются:
установленный Docker-образ Tarantool DB;
приложение Docker Compose;
утилита tt CLI;
исходные файлы примера
migrations
.Примечание
Есть два способа получить исходные файлы примера:
Архив с полной документацией Tarantool DB, полученный по почте или скачанный в личном кабинете tarantool.io. Пример архива:
tarantooldb-documentation-2.0.0.tar.gz
. Примерmigrations
расположен в таком архиве в директории./doc/examples/migrations/
.Отдельный архив migrations.tar.gz, скачанный c сайта Tarantool.
Схема данных¶
В качестве примера приведена база данных для системы управления проектами, которая состоит из трех спейсов: projects
(проекты), tasks
(задачи),
users
(пользователи).
Схема этой базы данных выглядит так:
Здесь:
Спейсы
projects
иtasks
имеют одинаковый ключ шардированияproject_id
и находятся на одном экземпляре.Спейс
users
имеет ключ шардированияuser_id
.
Особенности базы данных:
Задач на проекте больше, чем пользователей.
При удалении проекта нужно удалить и связанные с ним задачи. Это удобно делать, если все записи находятся на одном экземпляре.
Примечание
При выборе ключа шардирования учитывайте предметную область и предполагаемое API.
Для работы с данными в примере используются методы модуля CRUD. Дополнительно будет реализовано следующее API:
app.delete_user(user_id)
– удаление пользователя. У всех задач, связанных с этим пользователем, в полеassigned_user_id
должен быть выставленbox.NULL
;app.delete_project(project_id)
– удаление проекта и все связанных с ним задач;app.get_project_data(project_id)
– получение проекта и всех связанных с ним задач и пользователей.
Используемые файлы¶
В руководстве используются следующие файлы примера migrations
:
cluster/
– директория c файлами для запуска кластера Tarantool DB:config.yml
– конфигурация и топология кластера;docker-compose.yml
– описание узлов кластера Tarantool DB;migrations/scenario/
иmigration_next/
– директории, содержащие файлы с описанием миграций;
tools/
– директория с файлами для запуска кластера etcd и TCM:docker-compose.yml
– описание узлов кластера etcd;tcm.yml
– конфигурация для запуска Tarantool Cluster Manager.
Запуск стенда¶
Для успешного запуска должны быть свободны следующие порты:
3301–3304
8081
Перейдите в директорию примера migrations
:
cd ./doc/examples/migrations/
Запустите стенд:
make start
Команда развернет стенд, состоящий из:
кластера Tarantool DB:
2 роутера;
2 набора реплик по 3 хранилища;
1 Tarantool Cluster Manager (TCM);
кластера etcd из 3 узлов.
После запуска должны работать все контейнеры, кроме init_host.
Также после запуска кластера становится доступен веб-интерфейс TCM. Для входа в TCM откройте в браузере адрес http://localhost:8081. Логин и пароль для входа:
Username:
admin
Password:
secret
В TCM откройте вкладку Stateboard.
Выберите в наборе реплик router-msk
узел router-msk
и в открывшемся окне перейдите на вкладку Terminal.
Во вкладке Terminal введите следующую команду:
box.space
Проверьте, что в выводе есть спейсы projects
, tasks
и users
– эти спейсы создаются при запуске кластера.
Кроме того, проверьте, что в запущенном кластере созданы функции app.delete_user(user_id)
и app.get_project_data(project_id)
.
Загрузка данных¶
Исходный код миграции приведен в файле 001_test.lua
в директории ./cluster/migrations/scenario/
примера migrations
.
Загрузить тестовые данные в спейсы можно с помощью утилиты tt CLI:
tt crud import \
admin:secret-cluster-cookie@localhost:3301 \
examples_data_projects.csv:projects \
--header
tt crud import \
admin:secret-cluster-cookie@localhost:3301 \
examples_data_users.csv:users \
--header
tt crud import \
admin:secret-cluster-cookie@localhost:3301 \
examples_data_tasks.csv:tasks \
--header
Проверка загруженных данных¶
Чтобы начать работу с базой данных через интерактивную консоль Tarantool, нужно подключиться к узлу кластера. Сделать это можно двумя способами:
в веб-интерфейсе TCM;
в терминале с помощью утилиты tt CLI:
tt connect admin:secret-cluster-cookie@localhost:3301
Подключитесь к роутеру router-msk
, используя первый способ – через TCM. Для этого:
Перейдите на вкладку Stateboard.
Нажмите на набор реплик
router-msk
.Выберите узел
router-msk
и в открывшемся окне перейдите на вкладку Terminal.
Чтобы проверить загруженные данные, выполните в TCM во вкладке Terminal несколько базовых операций в спейсе users
, используя модуль CRUD:
tarantool-router-msk:3301> crud.select('users')
---
- metadata: [{'name': 'user_id', 'type': 'uuid'}, {'name': 'bucket_id', 'type': 'unsigned'},
{'name': 'name', 'type': 'string'}, {'type': 'string', 'name': 'email', 'is_nullable': true}]
rows:
- [cccccccc-0000-0000-0000-000000000001, 24745, 'John Doe', 'john.doe@example.com']
- [cccccccc-0000-0000-0000-000000000002, 24093, 'Jane Smith', 'jane.smith@example.com']
- [cccccccc-0000-0000-0000-000000000003, 19072, 'Michael Brown', 'michael.brown@example.com']
- [cccccccc-0000-0000-0000-000000000004, 13957, 'Emily Davis', 'emily.davis@example.com']
- [cccccccc-0000-0000-0000-000000000005, 22248, 'David Wilson', 'david.wilson@example.com']
- [cccccccc-0000-0000-0000-000000000006, 16356, 'Emma Johnson', 'emma.johnson@example.com']
- [cccccccc-0000-0000-0000-000000000007, 24657, 'James Martinez', 'james.martinez@example.com']
- [cccccccc-0000-0000-0000-000000000008, 4437, 'Olivia Garcia', 'olivia.garcia@example.com']
- [cccccccc-0000-0000-0000-000000000009, 23784, 'Robert Rodriguez', 'robert.rodriguez@example.com']
- [cccccccc-0000-0000-0000-00000000000a, 23932, 'Ava Martinez', 'ava.martinez@example.com']
- null
...
tarantool-router-msk:3301> crud.select('users', {{"==", "name", "John Doe"}})
---
- metadata: [{'name': 'user_id', 'type': 'uuid'}, {'name': 'bucket_id', 'type': 'unsigned'},
{'name': 'name', 'type': 'string'}, {'type': 'string', 'name': 'email', 'is_nullable': true}]
rows:
- [cccccccc-0000-0000-0000-000000000001, 24745, 'John Doe', 'john.doe@example.com']
- null
...
tarantool-router-msk:3301> crud.update('users', require('uuid').fromstr('04e7f
6a2-2979-46e4-8d71-e80217e3aac3'), {{'=', 'name', "John Doevelyn"}})
---
- rows: []
metadata: [{'name': 'user_id', 'type': 'uuid'}, {'name': 'bucket_id', 'type': 'unsigned'},
{'name': 'name', 'type': 'string'}, {'type': 'string', 'name': 'email', 'is_nullable': true}]
- null
...
tarantool-router-msk:3301> crud.update('users', require('uuid').fromstr('ccccc
ccc-0000-0000-0000-000000000001'), {{'=', 'name', "John Doevelyn"}})
---
- rows:
- [cccccccc-0000-0000-0000-000000000001, 24745, 'John Doevelyn', 'john.doe@example.com']
metadata: [{'name': 'user_id', 'type': 'uuid'}, {'name': 'bucket_id', 'type': 'unsigned'},
{'name': 'name', 'type': 'string'}, {'type': 'string', 'name': 'email', 'is_nullable': true}]
- null
...
tarantool-router-msk:3301> crud.get('users', require('uuid').fromstr('cccccccc
-0000-0000-0000-000000000001'))
---
- rows:
- [cccccccc-0000-0000-0000-000000000001, 24745, 'John Doevelyn', 'john.doe@example.com']
metadata: [{'name': 'user_id', 'type': 'uuid'}, {'name': 'bucket_id', 'type': 'unsigned'},
{'name': 'name', 'type': 'string'}, {'type': 'string', 'name': 'email', 'is_nullable': true}]
- null
...
tarantool-router-msk:3301> crud.delete('users', require('uuid').fromstr('ccccc
ccc-0000-0000-0000-000000000001'))
---
- rows:
- [cccccccc-0000-0000-0000-000000000001, 24745, 'John Doevelyn', 'john.doe@example.com']
metadata: [{'name': 'user_id', 'type': 'uuid'}, {'name': 'bucket_id', 'type': 'unsigned'},
{'name': 'name', 'type': 'string'}, {'type': 'string', 'name': 'email', 'is_nullable': true}]
- null
...
Проверка пользовательских функций базы данных¶
Модуль CRUD упрощает работу с шардированными данными – выполнение простых операций
чтения и записи таких данных прозрачно для пользователя.
Тем не менее, для задач, реализующих функции базы данных (app.get_project_data(id)
, app.delete_user(id)
и app.delete_project(id)
),
модуля CRUD недостаточно.
Это связано с тем, что эти функции работают с несколькими спейсами и нестандартными операциями чтения и записи.
Получение данных проекта¶
В TCM во вкладке Terminal вызовите функцию и оцените результат:
localhost:3301> box.schema.func.call('app.get_project_data', require('uuid').fromstr('aaaaaaaa-0000-0000-0000-000000000001'))
---
- res:
tasks:
- status: In Progress
user:
name: Jane Smith
email: jane.smith@example.com
name: Optimize Database
description: Optimize the database to improve performance.
- status: Not Started
user:
name: Michael Brown
email: michael.brown@example.com
name: Create New Logo
description: Design a new logo for the website.
name: Task Management
description: Development of a task management system
err: null
В выводе видны данные проекта Task Management
и двух задач из этого проекта.
Функция app.get_project_data(project_id)
выполняет join
из всех спейсов, используются crud.get
, crud.pairs
.
Замена пользователя¶
Предположим, что во всех задачах, связанных с некоторым пользователем, нужно присвоить полю assigned_user_id
другого пользователя.
Для этой цели на всех хранилищах объявлена функция tasks.replace_user
.
Функция задает новое значение в поле assigned_user_id
для всех задач, у которых assigned_user_id == user_id
, где
user_id
– аргумент функции.
Код функции:
function(current_user_id, new_user_id)
local fiber = require('fiber')
local every_100 = 0
for _, t in box.space.tasks.index.assigned_user_id:pairs({current_user_id}, 'EQ') do
box.space.tasks:update(t.task_id, {{'=', 'assigned_user_id', new_user_id}})
every_100 = every_100 + 1
if every_100 == 100 then
every_100 = 0
fiber.yield()
end
end
return true
end
Особенность такой замены состоит в том, что спейсы tasks
и users
шардируются по разным ключам.
В общем случае связанные задачи и пользователи будут находиться на разных наборах реплик.
Это значит, что нет узла, на котором бы было известно, на каких наборах реплик будут задачи, связанные с удаляемым пользователем.
Вызывать функцию tasks.replace_user
нужно на каждом мастере набора реплик, потому что такие задачи будут на всех наборах реплик.
Для вызова функции на всех наборах реплик используется модуль для горизонтального масштабирования vshard.
В примере ниже функция tasks.replace_user
заменяет пользователя Jane Smith
на Ava Martinez
:
vshard.router.map_callrw('tasks.replace_user', {
require('uuid').fromstr('cccccccc-0000-0000-0000-000000000002'),
require('uuid').fromstr('cccccccc-0000-0000-0000-00000000000a')
})
В TCM во вкладке Terminal просмотрите ещё раз данные проекта Task Management
:
localhost:3301> box.schema.func.call('app.get_project_data', require('uuid').fromstr('aaaaaaaa-0000-0000-0000-000000000001'))
---
- res:
tasks:
- status: In Progress
user:
name: Ava Martinez
email: ava.martinez@example.com
name: Optimize Database
description: Optimize the database to improve performance.
- status: Not Started
user:
name: Michael Brown
email: michael.brown@example.com
name: Create New Logo
description: Design a new logo for the website.
name: Task Management
description: Development of a task management system
err: null
Видно, что в проекте на задачах пользователь Jane Smith
изменен на Ava Martinez
.
Полный исходный код функции tasks.replace_user
приведен в файле миграции ./cluster/migrations/scenario/001_test.lua
примера migrations
.
Удаление проекта¶
Удалите проект Task Management
:
tarantool-router-msk:3301> box.schema.func.call('app.delete_project',
require('uuid').fromstr('aaaaaaaa-0000-0000-0000-000000000001')
)
---
- res: true
err: null
...
Просмотрите содержимое спейса projects
.
Видно, что проект Task Management
был удален вместе со всеми задачами:
tarantool-router-msk:3301> box.schema.func.call('app.get_project_data',
require('uuid').fromstr('aaaaaaaa-0000-0000-0000-000000000001')
)
---
- res: []
err: null
...
Спейсы projects
и tasks
шардируются по одинаковым значениям.
Это означает, что связанные между собой проект и задача находятся на одном экземпляре.
Такой подход позволяет транзакционно удалить данные из projects
и tasks
.
Для этого на хранилищах реализована API-функция projects.delete_project
.
Код функции:
function(project_id)
local proj = box.space.projects:get(project_id)
if proj == nil then
return false
end
box.atomic(function() -- атомарно удаляем и из projects и из tasks
box.space.projects:delete(project_id)
for _, t in box.space.tasks.index.project_id:pairs({project_id}, 'EQ') do
box.space.tasks:delete(t.id)
end
end)
return true
end
Для вызова функции на конкретном мастере используется модуль vshard
.
Вызов функции projects.delete_project
выглядит так:
local vshard_router = require('vshard.router')
local bucket_id = vshard_router.bucket_id_strcrc32(id)
local _, err = vshard_router.callrw(bucket_id, 'projects.delete_project', {id})
Полный исходный код функции projects.delete_project
приведен в файле миграции ./cluster/migrations/scenario/001_test.lua
примера migrations
.
Изменение схемы данных¶
Предположим, что теперь нужно изменить схему данных, добавив в нее новые поля:
deadline (datetime)
в спейсprojects
;due_date (datetime)
в спейсtasks
;role (string)
в спейсusers
.
По умолчанию в полях projects.deadline
и due_date(datetime)
должно быть значение 2999-12-31T00:00:00Z
, а в поле
users.role
– значение not set
.
Функцию app.get_project_data
нужно также переписать, чтобы отображались новые поля.
Новая схема данных будет выглядеть так:
Код миграции приведен в файле ./cluster/migration_next/002_test.lua
примера migrations
.
Миграции выполняются в лексикографическом порядке, поэтому им даны нумерованные названия: (0001_my_migr.lua
, 2023_12_24_migr.lua
).
Выполнение миграции¶
Выполнить миграцию можно с помощью утилиты tt CLI. Для этого:
В терминале поместите файлы из папки
migration_next
с кодом миграций002_test.lua
и002_test_upgrade.lua
в папку./cluster/migrations/scenario/
:cd cluster cp -a migration_next/* migrations/scenario/
Загрузите миграции в централизованное хранилище:
tt migrations publish http://admin:secret-cluster-cookie@localhost:2379/tdb/ migrations
Узнать больше о командах
tt migrations
можно в документации Tarantool.Примените миграции:
docker compose exec tarantool-router-msk tt migrations apply http://etcd1:2379/tdb --tarantool-username=admin --tarantool-password=secret-cluster-cookie cd ..
В случае успеха вывод будет выглядеть так:
• router-msk: • 001_test.lua: skipped, already applied • 002_test.lua: successfully applied • 002_test_upgrade.lua: successfully applied • router-spb: • 001_test.lua: skipped, already applied • 002_test.lua: successfully applied • 002_test_upgrade.lua: successfully applied • storage-1: • 001_test.lua: skipped, already applied • 002_test.lua: successfully applied • 002_test_upgrade.lua: successfully applied • storage-2: • 001_test.lua: skipped, already applied • 002_test.lua: successfully applied • 002_test_upgrade.lua: successfully applied
Проверьте, что миграция прошла успешно. Для этого в TCM во вкладке Terminal выполните функцию
app.get_project_data
. В функции должны появиться новые поля в ответе:tarantool-router-msk:3301> box.schema.func.call('app.get_project_data', require('uuid').fromstr('aaaaaaaa-0000-0000-0000-000000000002')) --- - res: tasks: - due_date: 2999-12-31T00:00:00Z status: In Progress user: email: not set name: Emily Davis role: emily.davis@example.com name: Develop User Authentication description: Implement user authentication for the mobile app. - due_date: 2999-12-31T00:00:00Z status: Not Started user: email: not set name: David Wilson role: david.wilson@example.com name: Design Marketing Materials description: Create brochures and flyers for the campaign. deadline: 2999-12-31T00:00:00Z name: Website Update description: Making changes to the website design and functionality err: null ...
Обратите внимание, что в миграции происходит обновление функции
app.get_project_data
. Для этого транзакционно удаляется старая функция и добавляется новая. Такой подход гарантирует, что не произойдет ситуации, когда функцииapp.get_project_data
не существует:box.atomic(function() box.schema.func.drop('app.get_project_data') -- удаляется старый вариант box.schema.func.create('app.get_project_data', { language = 'LUA', if_not_exists = true, body = [[ ... ]] }) -- добавляется новый вариант end)
Узнать подробнее о том, как хранятся персистентные функции, можно в спейсе box.space._func.
Остановка стенда¶
Чтобы остановить стенд, выполните в локальном терминале следующую команду:
make stop