Миграция БД
Apr. 6th, 2010 10:46 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
В связи с тем, что я таки частично реализовал свою давнишнюю идею генерить опердень из модели опердени, хранящейся в этой же опердени, очередной раз решил поразмыслить над вопросом "как же должна выглядеть миграция данных при изменении модели".
Обычно я делаю примерно так: доработки, изменяющие структуру БД и задевающие хранящиеся данные, продумываются намного дольше и аккуратнее, чем остальные части, которые можно менять независимо (код, запросы, хранимые процедуры, итд), и выделяются в отдельные скрипты для миграции, которые при деплойменте доработок применяются сначала к тестовой, а потом боевой базе. Для некоторых проектов это вообще автоматизировано и выполняется вместе с обновлением софта специальной утилитой.
Но предположим, что мы сделали генератор опердени и дали его специально обученным настройщикам клепать предметную область. При этом они могут менять описания таблиц и полей (как сделано на данный момент) или структуры хранящихся объектов (как хотелось бы сделать будущем) и это, само собой, должно как-то в итоге отобразится на рабочей базе.
Первая мысль - что их изменения в рабочую базу попадают только в виде сгруппированного набора непротиворечивых действий, т.е. вперемежку изменений данных и метаданных таким образом чтобы база не оказалась в состоянии типа "в not null полях сидят null". Некоторые базы такое не дают делать, у некоторых при DDL операциях такое возможно, потом бэкап черезжопными методами восстанавливать придется.
Второе - что их работа над моделью должна вестись под системой контроля версий. Но работа ведется, во-первых, не над текстом модели, а в специализированном GUI (который может отражать состояние некоего текста, а может и не отражать - сейчас модель хранится в специально заточенной базе Firebird), а во вторых, _текстовый_ контроль версий не отражает происходящего с моделью в, условно говоря, строго типизированном виде.
Т.е. например "добавить поле в таблицу" с точки зрения системы типов выглядит как модификация описания этого типа методом "добавить в список полей еще один элемент", а с точки зрения какого-нибудь меркуриала - это просто появление новой строки где-то в середине файла модели и ему пофиг на смысл этой строки. Конечно, из hg diff можно было бы сделать описание операции в типизированном виде, но это, по моему убеждению, есть удаление гланд через задницу, т.к. текст должен генерироваться из строго типизированных данных, а не оные данные должны доставаться методом парсинга текста хз какого формата.
На данный момент у меня рабочая идея примерно такая:закрыть бухгалтерам доступ к базе опердени, написать по емылу им, что их софт больше не обслуживается, выключить телефон, завтра вместо работы пойти купить зеркалку и поехать фотографировать уток на речку к базе данных модели добавляется лог всех произведенных в ней изменений в отдельные таблицы со сквозной нумерацией. Этот номер в логе и будет текущей "ревизией" модели. При каждой генерации и деплойменте в результатирующий скрипт деплоймента вставляется номер ревизии. И в состав скрипта деплоймента будут входить все изменения от начала (ну или если мы точно знаем что база какой-то версии - от этой версии) до текущей ревизии.
Сейчас у меня кодогенерация происходит по текущей модели, т.е. условно говоря, это "создать базу с нуля до текущего состояния". А реально нужно другое, более сложное: нужно накатить на существующую базу изменения "от прошлой кодогенерации до текущей". Причем накатить непротиворечивым образом. И вместо кодогенератора который делает из модели просто скрипты создания всех нужных объектов базы данных и описание GUI к ним, нужно каким-то образом из списка изменений сделать скрипты изменения схемы базы данных и обновления данных в базе (я думаю, не всегда тривиальных).
И в таком случае вылазят всякие хитрые заморочки, в основном связанные с зависимостями объектов на уровне метаданных и зависимостями на уровне данных - например, в Firebird грохнуть объект, на который ссылается другой объект просто невозможно - нужно сначала грохнуть зависимый(а если от него еще что-нибудь зависит - то по цепочке, или же сделать объект с пустым телом). На уровне данных - это всякие внешние ключи, которые не дадут изменить данные если их создать слишком рано (поэтому всякие утилиты импорта данных сначала всасывают данные а потом создают внешние ключи).
С теоретической точки зрения это все выглядит как список функций "МодельN+1=F(N)(МодельN)", потом функция, которая из функций преобразования модели генерит скрипты преобразования базы данных, возможно, анализируя список изменений на предмет "сгрупировать изменения в один скрипт", и функций преобразования "старая версия объекта"->"новая версия объекта" и анализа "можно это сделать средствами SQL(alter table) или придется долбится курсорами по всей базе до посинения".
Кстати, я до сих пор не очень понимаю, что в таких случаях предлагаю всякие NoSQL базы. Я так понимаю, там же ложится/достается чисто объект по ключу в хранилище. А если мы поменяем описание объекта в коде программы, нужно ж будет описывать явно "что делаем при загрузке старой версии объекта". О ад-хок запросах к таким вещам даже и думать не хочется.
Обычно я делаю примерно так: доработки, изменяющие структуру БД и задевающие хранящиеся данные, продумываются намного дольше и аккуратнее, чем остальные части, которые можно менять независимо (код, запросы, хранимые процедуры, итд), и выделяются в отдельные скрипты для миграции, которые при деплойменте доработок применяются сначала к тестовой, а потом боевой базе. Для некоторых проектов это вообще автоматизировано и выполняется вместе с обновлением софта специальной утилитой.
Но предположим, что мы сделали генератор опердени и дали его специально обученным настройщикам клепать предметную область. При этом они могут менять описания таблиц и полей (как сделано на данный момент) или структуры хранящихся объектов (как хотелось бы сделать будущем) и это, само собой, должно как-то в итоге отобразится на рабочей базе.
Первая мысль - что их изменения в рабочую базу попадают только в виде сгруппированного набора непротиворечивых действий, т.е. вперемежку изменений данных и метаданных таким образом чтобы база не оказалась в состоянии типа "в not null полях сидят null". Некоторые базы такое не дают делать, у некоторых при DDL операциях такое возможно, потом бэкап черезжопными методами восстанавливать придется.
Второе - что их работа над моделью должна вестись под системой контроля версий. Но работа ведется, во-первых, не над текстом модели, а в специализированном GUI (который может отражать состояние некоего текста, а может и не отражать - сейчас модель хранится в специально заточенной базе Firebird), а во вторых, _текстовый_ контроль версий не отражает происходящего с моделью в, условно говоря, строго типизированном виде.
Т.е. например "добавить поле в таблицу" с точки зрения системы типов выглядит как модификация описания этого типа методом "добавить в список полей еще один элемент", а с точки зрения какого-нибудь меркуриала - это просто появление новой строки где-то в середине файла модели и ему пофиг на смысл этой строки. Конечно, из hg diff можно было бы сделать описание операции в типизированном виде, но это, по моему убеждению, есть удаление гланд через задницу, т.к. текст должен генерироваться из строго типизированных данных, а не оные данные должны доставаться методом парсинга текста хз какого формата.
На данный момент у меня рабочая идея примерно такая:
Сейчас у меня кодогенерация происходит по текущей модели, т.е. условно говоря, это "создать базу с нуля до текущего состояния". А реально нужно другое, более сложное: нужно накатить на существующую базу изменения "от прошлой кодогенерации до текущей". Причем накатить непротиворечивым образом. И вместо кодогенератора который делает из модели просто скрипты создания всех нужных объектов базы данных и описание GUI к ним, нужно каким-то образом из списка изменений сделать скрипты изменения схемы базы данных и обновления данных в базе (я думаю, не всегда тривиальных).
И в таком случае вылазят всякие хитрые заморочки, в основном связанные с зависимостями объектов на уровне метаданных и зависимостями на уровне данных - например, в Firebird грохнуть объект, на который ссылается другой объект просто невозможно - нужно сначала грохнуть зависимый(а если от него еще что-нибудь зависит - то по цепочке, или же сделать объект с пустым телом). На уровне данных - это всякие внешние ключи, которые не дадут изменить данные если их создать слишком рано (поэтому всякие утилиты импорта данных сначала всасывают данные а потом создают внешние ключи).
С теоретической точки зрения это все выглядит как список функций "МодельN+1=F(N)(МодельN)", потом функция, которая из функций преобразования модели генерит скрипты преобразования базы данных, возможно, анализируя список изменений на предмет "сгрупировать изменения в один скрипт", и функций преобразования "старая версия объекта"->"новая версия объекта" и анализа "можно это сделать средствами SQL(alter table) или придется долбится курсорами по всей базе до посинения".
Кстати, я до сих пор не очень понимаю, что в таких случаях предлагаю всякие NoSQL базы. Я так понимаю, там же ложится/достается чисто объект по ключу в хранилище. А если мы поменяем описание объекта в коде программы, нужно ж будет описывать явно "что делаем при загрузке старой версии объекта". О ад-хок запросах к таким вещам даже и думать не хочется.
no subject
Date: 2010-04-07 12:40 am (UTC)интересно как штуки "выделить емейл из одной таблички и имя из другой в отдельную табличку "люди" и создать форейн кеи в ту табличку" изображаются.
no subject
Date: 2010-04-07 06:36 am (UTC)Никаких DSL изобретать для этой цели не хочется - проще всего написать простейшую функцию, которая пройдется по таблицам базы и вытащит нужные данные. Типа "создать таблицу c полями, затем сложить в нее всех людей, затем добавить внешние ключи, а старые поля грохнуть".
Ключевой аспект - что добавление внешнего ключа переводит модель в несамосогласованный вид и не дает применять изменение к базе до тех пор, пока не будет написана функция, которая вернет такую базу в согласованный вид.
А если еще до этого база была ненормализованной и в результатирующей внешней таблице получаются противоречия (у меня на данный момент есть такое с таблицей клиентов и филиалов банков в которых они обслуживаются), то без вмешательства пользователя при миграции конверсию можно произвести только с потерей части данных.
no subject
Date: 2010-04-07 06:13 pm (UTC)no subject
Date: 2010-04-07 07:02 pm (UTC)no subject
Date: 2010-04-07 01:36 am (UTC)1. тип объекта
2. его атрибуты
3. его значения
ну и связывающие таблицы. Но это довольно муторно и работает на больших объемах данных стремно.
no subject
Date: 2010-04-07 06:37 am (UTC)no subject
Date: 2010-04-07 04:24 am (UTC)Почитай как сделаны migrations в ruby on rails, там как раз оно самое, даже в две стороны, насколько я помню.
no subject
Date: 2010-04-07 06:17 am (UTC)no subject
Date: 2010-04-07 06:59 am (UTC)Кстати, еще ж нужно предусмотреть откаты ошибочных изменений в модели(полное удаление changeset, чтобы оно в базу не попало) и бранчи, на случай если над моделью работает несколько человек одновременно
no subject
Date: 2010-04-07 07:14 am (UTC)no subject
Date: 2010-04-07 06:00 am (UTC)^^^^^^^^^^^^
Некоторые делают проще - пишут конвертер БД с релиза 1.1 до 1.2. В результате видел набор примерно такой:
converter1_2.exe
converter2_3.exe
converter3_4.exe
converter4_5.exe
и на выходе теоретически должна была получиться версия 1.5
Правда сопряжено сие с выводом БД в оффлайн на определенное время.
no subject
Date: 2010-04-07 06:03 am (UTC)И да, алгебра преобразований над моделью должна позволять безмозглое транслирование в SQL's ALTER TABLEs.
no subject
Date: 2010-04-07 06:56 am (UTC)no subject
Date: 2010-04-07 07:32 am (UTC)И реально должно упростить всё. Сколько раз схема изменяется в год? Пусть сотню тысяч раз. Сто тысяч строк прочитать, собрать в непротиворечивый граф в голове, и провалидировать будет стоить меньше секунды.
no subject
Date: 2010-04-07 07:37 am (UTC)no subject
Date: 2010-04-07 06:11 am (UTC)Задача то старая и вроде как решённое (в том плане, что однозначно существуют некоторые подходы по решению данной проблемы и их реализация). Версионность схемы базы данных уже давно не рокет сайенс.
P.S. пример из старого рабочего
class GeodatasUniqueAltAndLen < ActiveRecord::Migration
def self.up
create_table "geodatas_unique_alt_and_lens", :force => false, :row_version => false do |t|
t.column "Breite_Abs", :decimal, :precision => 10, :scale => 8
t.column "Laenge_Abs", :decimal, :precision => 10, :scale => 8
end
load_from_sqlscript("geodatas_unique_alt_and_lens", geodatas_unique_script)
end
def self.load_from_sqlscript(table_name, script)
cnx = ActiveRecord::Base.connection
cnx.insert(script)
end
def self.down
drop_table :geodatas_unique_alt_and_len
end
def self.geodatas_unique_script
<<'END_OF_DATA'
INSERT INTO geodatas_unique_alt_and_lens
("Breite_Abs", "Laenge_Abs")
select distinct "Breite_Abs", "Laenge_Abs"
from geodatas
group by "Breite_Abs","Laenge_Abs"
END_OF_DATA
end
end
no subject
Date: 2010-04-07 06:19 am (UTC)no subject
Date: 2010-04-07 07:30 am (UTC)В Rails поля модели выводяться из миграций(!), и это правильный имхо подход. Более высокоуровневый.
А попытка вывести миграцию из разницы моделей, это уже попахивает странным.
Впрочем то, что хочет metaclass в чистом виде реализовано например у http://south.aeracode.org/docs/about.html (вывод миграции из diff-а модели и базы данных)
no subject
Date: 2010-04-07 06:19 pm (UTC)no subject
Date: 2010-04-07 06:53 am (UTC)Задача в том, чтобы такой или похожий скрипт составился по результатам работы аналитика над моделью задачи и экспорта из модели.
no subject
Date: 2010-04-07 07:30 am (UTC)no subject
Date: 2010-04-07 08:05 am (UTC)то, что не изменилось, переселяецца автоматически
что изменилось, затачиваецца напильнегом
no subject
Date: 2010-04-07 09:01 am (UTC)А еще бывает уныние когда места для второй базы такого же размера тупо нет :)
no subject
Date: 2010-04-07 10:07 am (UTC)индексы для покорёженных таблиц в любом случае придёцца пересоздать ..
no subject
Date: 2010-04-07 10:24 am (UTC)А вот у клиентов - нету. И винты покупать им запрещено :)
Как-то базу у них обслуживали, так админы начали долбится на телефон с воплям "а чего это вы так резко дисковое пространство используете"
no subject
Date: 2010-04-07 12:01 pm (UTC)no subject
Date: 2010-04-07 09:56 am (UTC)no subject
Date: 2010-04-07 12:00 pm (UTC)no subject
Date: 2010-04-07 02:03 pm (UTC)no subject
Date: 2010-04-07 10:04 am (UTC)Как вариант - отказаться от удаления (вообще любого), ввести версионность объектов и метаданных, отказаться от мержинга и соблюдать строгую очередность. Вот бы еще БД, которая метаданные с таблицами таскала и голову не морочила... Cache чтоль вспомнить?
no subject
Date: 2010-04-07 12:01 pm (UTC)no subject
Date: 2010-04-07 12:06 pm (UTC)no subject
Date: 2010-04-07 01:53 pm (UTC)