Як я базу в GIT закачував

День добрий, хабровчане. Більшість продуктів, з якими стикається розробник, зазвичай вимагають розгортання на декількох машинах, які працюють незалежно один від одного. Це породжує одну з типових проблем — розбіжність бази даних на різних серверах, невідповідність ідентифікаторів у таблицях-довідниках і зрозуміло неоднорідність чинності неуважність і пропущених патчів при оновленні БД на конкретній машині. У деяких випадках це виливається в дикі (на мій наївний погляд) концепції типу «ми стовпці ніколи не видаляємо — тільки додаємо».

В інших і зовсім приводить до засмічення бази сміттям з інших майданчиків і до помилок після «найпростішого мержа».

Знайомих з такими ситуаціями, критиків і знають точно, що я винайшов велосипед — запрошую під кат.

Подібний принцип зберігання я зустрічав в декількох компаніях, але з якоїсь причини його опис в мережі ховається від мене відсутній.

Загальна концепція проста як апельсин і приймає 2 установки:

1. База даних повинна знати про те, які патчі до неї застосовувалися.
2. При створенні зв'язків у записах ні в якому разі не використовуються значення ідентифікаторів. (крім отриманих в обчисленнях).

При дотриманні цих умов продукт повинен працювати на будь-якій машині з об'єктивно стабільним результатом. Так, може здатися, що друга умова не виконується, але якщо всі бази створюються одним і тим же скриптом — розбіжність ідентифікаторів буде вже аномалією.

Ітерації в процесі створення схеми.
«Хто створив стовпець?»
«Що тут має зберігатися? Звідки ці числа? Пишіть хоча б коментарі!»
«Ми це вже 100 років як не використовуємо. Звідки воно тут?»

Знайоме? Багато приймають як аксіому той факт, що база даних існує «як є». Це лист Дядька Федора, яке не має свого автора. Але так лише частково. У кожної зміни, стовпця і запису є свій автор. Всі зміни робляться на лінії часу. Схоже на git / svn, etc.? Ми всі активно користуємося системами версійного контролю і чудово здружилися з усіма бонусами, які дарує нам такий підхід. Давайте спробуємо застосувати його тут.

Отже, переходимо до практики і поглянемо на незліченну кількість папок у проекті.

image
Для початку створимо таблицю з інформацією про латках. Тут ми запам'ятаємо які патчі у нас відпрацювали (name + type) і з яким результатом (result)

dc.sql
CREATE TABLE IF NOT EXISTS dc (
id INTEGER(11) NOT NULL AUTO_INCREMENT,
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)
);


Щоб все працювало так, як ми хочемо, додамо самий головний файл з оригінальною назвою.

start.sh#!/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 in f ${FILES}
do
scriptName=`expr "$f": '\([a-z_]*\)"
var=$(mysql --user ${username} --password=${password} -D${database} <<< "select count(*) from information_schema.tables as t де 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 in f ${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 де 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 in f ${FILES}
do
scriptName=`expr "$f": '\([a-z0-9]*\)"
var=$(mysql --user ${username} --password=${password} -D${database} <<< "select count(*) from ${database}.dc as t де 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 in f ${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 де 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 де 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 де 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 де 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 ДЕ 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;
— спокійні нерви.

Щиро дякую всім які дочитали за увагу. Вітаю і чекаю критику). Згоден, що продукт місцями вийшов сирої, але для домашніх потреб підходить відмінно.

Тапками сильно не закидайте — це перша спроба опублікуватися на Хабре (дівчатам можна).
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.