Часто бывает, при ближайшем рассмотрении некоторая проблема выявляет более глубокую, погружаясь в решение которой находишь для себя много интересного.
О такой ситуации на одном из наших проектов и пойдет речь.
Предыстория, описание проявления изначальной проблемы
Прежде всего, с чего все началось. Есть веб-проект, backend часть которого написана с использованием фреймворка Laravel (8.80.0 версии). В качестве админки — Laravel Nova (3.27.0 версия). Используемая СУБД — PostgreSQL. Развертка в production осуществляется посредством Kubernetes. Однажды, в силу определенных причин, было принято решение архитектурно перейти с PostgreSQL на распределенную систему управления базами данных CockroachDB. Процесс миграции был успешен.
Заметим, что изначально пытались подобрать сторонний драйвер для подключения ORM к CockroachDB. Однако выяснили, что это совершенно не требуется и стандартный драйвер pgsql прекрасно работает.
Изначальная проблема не была связана с СУБД в явном виде, а проявилась таким образом: один из ресурсов Nova использует поле со связью BelongsToManyField, предоставляемое пакетом benjacho/belongs-to-many-field, но не суть. В админке, при выборке в данном поле связанной сущности, технически данные связи должны быть записаны в pivot таблицу связи, проще говоря пара первичных ключей, связанных между собой таблиц.
Однако "сырое" значение первичного ключа таблицы, получаемое из базы данных, имеющее примерно такой вид 782610923664375809, округлялось JavaScript на стороне фронтовой части пакета до 782610923664375800. И далее в момент записи в pivot таблицу мы получили ошибку "violates foreign key constraint"
local.ERROR: SQLSTATE[23503]: Foreign key violation: 7 ERROR: insert on table "******" violates foreign key constraint "********_id_foreign"
DETAIL: Key (*****_id)=(782610923664375800) is not present in table "******". (SQL: insert into "******" ("***_id", "***_id") values (782611156327727105, 782610923664375800))
Что и понятно, так как такого ключа в ней не могло быть. В браузере это выглядело вот так:
Установленная основная причина
Данная проблема точности таких больших чисел в JavaScript, обусловленная природой, плавающей запятой двойной точности IEEE-754, имеет место быть сама по себе. Вопрос в том, как ее обойти. Возможно хорошим вариантом было бы на стороне фронта перед использованием полученного значения поля преобразовывать его в строку, но мы от этого отказались по причине того, что невозможно дорабатывать все пакеты вендоров.
Итак, если размерность значений первичных ключей, определяемая CockroachDB столь велика, возможно ли уменьшить её изначально на стороне СУБД при создании таблицы?
И тут началось интересное
У CockroachDB числовой тип данных представлен как INT. Однако, привычная нам размерность Integer, BigInteger есть не что иное, как алиасы на все тот же тип INT, и по сути своей одно и то же. Вот страница официальной документации на этот счет.
Этот факт подвел нас к тому, что попытка уменьшить размерность первичного ключа в создаваемой таблице путем строгого приведения его типа к unsignedInteger привела к созданию такового в базе все с тем же типом int8. Т.е. с чего начали, к тому и пришли.
Внутреннее поведение CockroachDB
В принципе, разработчикам CockroachDB все это известно, очевидно архитектура ими выбрана не просто так и способы решения данной ситуации представлены тут.
Все сводится к двум вариантам:
Установить сессионную переменную default_int_size в значение 4, что соответствует размерности числового типа Integer (int4) и позволит создавать именно эти поля.
Установить sql.defaults.default_int_size, но уже для всего кластера
Против последнего варианта выступили наши DevOps специалисты, так как в кластере мы можем быть не одни со своей БД.
Однако на практике было обнаружено следующее: значение default_int_size учитывается при создании таблицы исключительно для обычных полей. Если поле создается нами в коде миграции как первичный ключ (с присущими ограничениями автоинкремента и уникальности), то CockroachDB принудительно определяет его с типом int8. И повлиять на это мы никак не смогли.
Способы решения проблемы
Всего этого можно было бы избежать, если использовать uuid изначально. Вариант перехода на него мы отклонили по следующим причинам. В первую очередь пришлось бы серьезно перерабатывать 90% проектов. Также наряду с преимуществом существуют и недостатки uuid, например, большее потребление памяти, более низкая скорость работы, ограничения сортировки по первичному ключу.
Вот несколько мнений на этот счет:
Что лучше и правильнее использовать в качестве первичного ключа: автоинкремент или UUID?
Про uuid-ы, первичные ключи и базы данных
Идею решения вопроса мы почерпнули опять же из официальной документации в той ее части, где объясняется конструкция первичного ключа.
При создании таблицы, в случае, когда первичный ключ не был определен, CockroachDB самостоятельно его создает в виде скрытого "rowid" поля (разумеется все с тем же int8 типом).
Тогда вполне возможно наряду с rowid создать отдельным порядком поле id, необходимого нам unsignedInteger типа (int4), с использованием сессионной переменной default_int_size. И далее добавить ему обычные ограничения первичного ключа, такие как автоинкремент и уникальность. Именно это поле id и будет в последствии использоваться ORM в связях таблиц. Вся суть именно в раздельной операции создания поля и последующего наделения его необходимыми свойствами.
Данное решение рабочее, в итоге мы получили id идентификаторы таблиц приемлемой размерности, которые не округляются JavaScript на стороне фронта, с минимальными издержками в виде правок миграций. Примеры кода ORM миграций:
// Создание поля id, разделение операций обязательно. Как уже сказал, без определения первичного ключа при создании таблицы, CockroachDB создаст скрытое rowid поле этого ключа
Schema::create('my_table', function (Blueprint $table) {
$table->unsignedInteger('id');
//...
});
Schema::table('my_table', function (Blueprint $table) {
$table->unsignedInteger('id')->unique()->autoIncrement()->change();
});
// В методе down() отката миграции, после создания таблицы, также необходимо удалить ее sequence структуру. Без этого повторная миграция на создание таблицы выдаст ошибку
public function down()
{
Schema::dropIfExists('my_table');
// Драйвер 'cockroach' - это простое копирование в config/database драйвера 'pgsql', один в один
if (config('database.default') === 'cockroach') {
$pdo = DB::getPdo();
try {
$pdo->query('drop sequence my_table_id_seq');
} catch (Exception $e) {
Log::channel('stderr')->info('sequence my_table_id_seq not exist');
}
}
}
// Создание pivot таблицы для связи многие-ко-многим, используемое полем BelongsToManyField в Nova
Schema::create('my_table1_my_table2', function (Blueprint $table) {
// Здесь тип идентификатора не важен, т.к. он не участвует непосредственно в связи
$table->id();
$table->unsignedInteger('my_table1_id')->nullable();
$table->foreign('my_table1_id')->references('id')->on('my_table1')->onDelete('cascade');
$table->unsignedInteger('my_table2_id')->nullable();
$table->foreign('my_table2_id')->references('id')->on('my_table2')->onDelete('cascade');
});
Остался вопрос переопределения PostgresConnector через дополнительный сервис провайдер с целью установки сессионной переменной default_int_size. Напомним, что мы подключаемся к CockroachDB через PostgeSQL драйвер.
Реализовали это вот так:
// app/Database/PostgresConnector
<?php
namespace App\Database;
use Illuminate\Database\Connectors\PostgresConnector as BaseConnector;
class PostgresConnector extends BaseConnector
{
public function connect(array $config)
{
$pdo = parent::connect($config);
if (config('database.default') === 'cockroach') {
$pdo->query('set default_int_size = 4');
}
return $pdo;
}
}
// app/Providers/DatabaseConnectorProvider
<?php
namespace App\Providers;
use App\Database\PostgresConnector;
use Illuminate\Support\ServiceProvider;
class DatabaseConnectorProvider extends ServiceProvider
{
public function register()
{
$this->app->bind('db.connector.pgsql', PostgresConnector::class);
}
}
// Регистрируем провайдера в config/app
'providers' => [
//...
App\Providers\DatabaseConnectorProvider::class,
],
Вот таким опытом мы поделились с сообществом, надеемся, кому-нибудь пригодится.
Спасибо за внимание! :)
Комментарии (2)
antarx
03.09.2022 17:30+1Вы проводили нагрузочное тестирование? Сколько TPS обрабатывает кокроач в вашей конфигурации? Несколько лет назад там были очень печальные числа.
akhkmed
Какие были причины?