Изменение схемы данных с помощью space:format()¶
В этом руководстве рассказано, как разработать типовое приложение в Tarantool DB и изменить в нем схему данных, используя метод space:format(). В качестве примера используется база данных для системы управления проектами. Для работы используются модули CRUD и vshard.
Руководство включает следующие шаги:
Пререквизиты¶
Для выполнения примера требуются:
- установленный Docker-образ Tarantool DB; 
- приложение Docker Compose; 
- утилита tt CLI; 
- исходные файлы примера - migrations.- Примечание - Есть два способа получить исходные файлы примера: - Архив с полной документацией Tarantool DB, полученный по почте или скачанный в личном кабинете tarantool.io. Пример архива: - tarantooldb-documentation-3.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.
 
Запуск стенда¶
Для успешного запуска должны быть свободны следующие порты:
- 2379 
- 3301–3308 
- 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.
Выберите в наборе реплик узел storage-1-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('04e7f6a2-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('cccccccc-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('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
...
Проверка пользовательских функций базы данных¶
Модуль 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 через модуль vshard в коде app.delete_project выглядит так:
local vshard_router = require('vshard.router')
local bucket_id = vshard_router.bucket_id_strcrc32(project_id)
local _, err = vshard_router.callrw(bucket_id, 'projects.delete_project', {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