[Из песочницы] Как я базу в GIT закачивал
В других и вовсе приводит к засорению базы мусором с других площадок и к ошибкам после «простейшего мержа».
Знакомых с такими ситуациями, критиков и знающих точно, что я изобрел велосипед — приглашаю под кат.
Подобный принцип хранения я встречал в нескольких компаниях, но по какой-то причине его описание в сети прячется от меня отсутствует.
Общая концепция проста как апельсин и принимает 2 установки:
1. База данных должна знать о том, какие патчи к ней применялись.
2. При создании связей в записях ни в коем случае не используются значения идентификаторов. (кроме полученных в вычислениях).
При соблюдении этих условий продукт должен работать на любой машине с объективно стабильным результатом. Да, может показаться, что второе условие не выполнимо, но если все базы создаются одним и тем же скриптом — расхождение идентификаторов будет уже аномалией.
Итерации в процессе создания схемы.
«Кто создал столбец?»
«Что здесь должно храниться? Откуда эти числа? Пишите хотя бы комментарии!»
«Мы это уже 100 лет как не используем. Откуда оно здесь?»
Знакомо? Многие принимают как аксиому тот факт, что база данных существует «как есть». Это письмо Дяди Федора, которое не имеет своего автора. Но все так лишь отчасти. У каждого изменения, столбца и записи есть свой автор. Все изменения делаются на линии времени. Похоже на git / svn, etc.? Мы все активно пользуемся системами версионного контроля и прекрасно сдружились со всеми бонусами, которые дарит нам такой подход. Давайте попробуем применить его здесь.
Итак, переходим к практике и взглянем на несметное количество папок в проекте.
Для начала создадим таблицу с информацией о патчах. Здесь мы запомним какие патчи у нас отработали (name + type) и с каким результатом (result)
CREATE TABLE IF NOT EXISTS dc (
id INTEGER(11) AUTO_INCREMENT NOT NULL,
code VARCHAR(100) NOT NULL,
type VARCHAR(50) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
result VARCHAR(1) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
m_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
Чтобы все работало так, как мы хотим, добавим самый главный файл с оригинальным названием.
#!/usr/bin/env bash username="habr" password="habr" database="mydatabase" cd COMMON mysql --user ${username} --password=${password} -D${database} < dc.sql if [ $? -eq "1" ]; then exit $? fi echo '' echo '>>> TABLES' echo '' cd TABLE FILES=* for f in ${FILES} do scriptName=`expr "$f" : '\([a-z_]*\)'` var=$(mysql --user ${username} --password=${password} -D${database} <<< "select count(*) from information_schema.tables as t where t.TABLE_NAME='${scriptName}'" -s) if [ ${var} -ne '1' ]; then echo "Processing $f file..." mysql --user ${username} --password=${password} -D${database} < ${f} mysql --user ${username} --password=${password} -D${database}<<<"INSERT INTO dc (code, type, result) values ('${f}', 'CREATE TABLE', "$?")" if [ $? -ne 0 ]; then exit $? fi else echo '--- Skip '${f}' ---' fi done echo '' echo '>>> FOREIGN KEYS' echo '' cd ../F_KEY FILES=* for f in ${FILES} do scriptName=`expr "$f" : '\([a-z_A-Z]*\)'` var=$(mysql --user ${username} --password=${password} -D${database} <<< "select count(*) from information_schema.table_constraints as t where t.constraint_name='${scriptName}'" -s) if [ ${var} -ne '1' ]; then echo "Processing $f file..." mysql --user ${username} --password=${password} -D${database} < ${f} mysql --user ${username} --password=${password} -D${database}<<<"INSERT INTO dc (code, type, result) values ('${f}', 'CREATE FK', "$?")" if [ $? -ne 0 ]; then exit $? fi else echo '--- Skip '${f}' ---' fi done echo '' echo '' echo '>>> LOAD DATA SCRIPTS' echo '' cd ../DATA FILES=* for f in ${FILES} do scriptName=`expr "$f" : '\([a-z0-9]*\)'` var=$(mysql --user ${username} --password=${password} -D${database} <<< "select count(*) from ${database}.dc as t where t.code='${f}' and result='0'" -s) if [ ${var} -ne '1' ]; then echo "Processing $f file..." mysql --user ${username} --password=${password} -D${database} < ${f} mysql --user ${username} --password=${password} -D${database}<<<"INSERT INTO dc (code, type, result) values ('${f}', 'LOAD DATA', "$?")" if [ $? -ne 0 ]; then exit $? fi else echo '--- Skip '${f}' ---' fi done echo '' echo '' echo '>>> LOAD TRIGGERS' echo '' cd ../TRIGGER FILES=* for f in ${FILES} do scriptName=`expr "$f" : '\([a-z_0-9]*\)'` var=$(mysql --user ${username} --password=${password} -D${database} <<< "select count(*) from information_schema.triggers as t where t.trigger_name='${f}'" -s) if [ ${var} -ne '1' ]; then echo "Processing $f file..." mysql --user ${username} --password=${password} -D${database} < ${f} mysql --user ${username} --password=${password} -D${database}<<<"INSERT INTO dc (code, type, result) values ('${f}', 'LOAD TRIGGER', "$?")" if [ $? -ne 0 ]; then exit $? fi else echo '--- Skip '${f}' ---' fi done echo '' exit $?
Принцип был взят как самый очевидный — имя патча оно же имя файла. Для триггеров, таблиц и внешних ключей проверяется наличие объектов с такими именами через information_schema. Если результат отрицательный — выполняется прогон соответствующего скрипта.
Никаких коллизий идентификаторов
Гораздо интереснее наполнение каталогов данными. Оно проводится через исполнения go-скриптов в каталоге /COMMON/DATA.
Факт отработки скрипта фиксируется в нашей таблице dc (data containers). Если все было ок — при следующем прогоне файл пропускаем.
Исполнение идет в алфавитном порядке, так что наиболее универсальным является использование timestamp в имени.
В самом простом исполнении это обычные INSERT-запросы со светлой надеждой на стабильность и предсказуемость автоинкремента.
-- добавление базовых ресурсов
INSERT INTO ds.reso_type (name, description) VALUES ('Пища', 'Основной ресурс, обеспечивающий жизнь');
INSERT INTO ds.reso_type (name, description) VALUES ('Дерево', 'Основной ресурс, обеспечивающий строительство');
INSERT INTO ds.reso_type (name, description) VALUES ('Камень', 'Основной ресурс, обеспечивающий строительство');
# noinspection SqlResolve
INSERT INTO ds.human_type (name, description) VALUES ("Чернь", "Кто все эти люди?");
А как же быть с более сложными данными? Используем вложенные запросы.
INSERT INTO reso_speed (resoId, popId, speed) VALUES (
(SELECT ht.id
FROM human_type ht
WHERE ht.name = 'Чернь'),
(SELECT rt.id
FROM reso_type rt
WHERE rt.name = 'Пища'),
30);
Или же напишем вспомогательные функции.
#создание tasktype
DELIMITER //
CREATE FUNCTION installTaskType(name VARCHAR(50), title VARCHAR(100), groupId INT(11), description VARCHAR(1000))
RETURNS int(11)
BEGIN
SELECT count(id) into @c from tasktype as t where t.name = name COLLATE 'utf8_unicode_ci';
if (@c = 0) THEN
insert into `tasktype`(`name`,`title`,`group_id`,`description`)
VALUES (name, title, groupId, description);
RETURN LAST_INSERT_ID();
END IF;
SELECT id INTO @taskTypeId FROM tasktype as t where t.name = name COLLATE 'utf8_unicode_ci';
RETURN @taskTypeId;
END //
DELIMITER ;
# создание новых taskgroup
DELIMITER //
CREATE FUNCTION installTaskGroup(name VARCHAR(255))
RETURNS INT(11)
BEGIN
SELECT count(id) into @c FROM taskgroup as t where t.name = name COLLATE 'utf8_unicode_ci';
if (@c = 0) THEN
INSERT INTO taskgroup (`name`) VALUES (name);
RETURN LAST_INSERT_ID();
END IF;
SELECT id INTO @groupId FROM taskgroup as t WHERE t.name = name COLLATE 'utf8_unicode_ci';
RETURN @groupId;
END //
DELIMITER ;
В go-скрипт пишем:
SELECT installTaskGroup('TEST_GROUP') into @groupId;
SELECT installTaskType('TEST_TASK', 'Это тестовая задача отправки статьи на HABR', @groupId, '');
Итак, мы получили подход, который позволяет выработать единую систему управления базой данных на примере git + mysql.
В качестве профита получили:
— авторство + в некоторых случаях привязка задач таск-трекера;
— последовательность в накате данных;
— новый проект на jenkins;
— спокойные нервы.
Искренне благодарю всех дочитавших за внимание. Приветствую и жду критику). Согласен, что продукт местами получился сырой, но для домашних нужд подходит отлично.
Тапками сильно не закидывайте — это первая попытка опубликоваться на Хабре (девушкам можно).
Комментарии (2)
2 января 2017 в 15:18 (комментарий был изменён)
+4↑
↓
Это называется «миграции схемы бд». Есть куча различных реализаций на куче разных языков.2 января 2017 в 15:24
0↑
↓
Не знаю, что вы в сети искали, но принципы построения подобных продуктов точно никто от вас не прячет.Можно например взглянуть на liquibase, или на flyway (это лишь два первых пришедших в голову, и далеко не единственные), где подобные (и еще кое-какие другие полезные) концепции давно реализованы. Идее хранить список примененных патчей в самой базе — наверное сто лет в обед.
И кстати, вынужден вас огорчить — на ваши же три вопроса:
«Кто создал столбец?»
«Что здесь должно храниться? Откуда эти числа? Пишите хотя бы комментарии!»
«Мы это уже 100 лет как не используем. Откуда оно здесь?»данный велосипед ответов все равно не дает. Во всяком случае — простых и полных ответов.