Статьи

Как перенести данные Oracle с Ruby

oracle1

Миграция данных — это вершина любого проекта веб-разработки. Это похоже на восхождение на вершину горы, чтобы просто оглянуться назад и прыгнуть. В типичном проекте вы можете проверить все на соответствие новой схеме. Будьте осторожны с ветром и вносите радикальные изменения со скоростью сто миль в час. Но как насчет устаревших данных, которые все еще находятся в производстве?

В этой статье я подробно расскажу о том, что нужно для успешного переноса данных в Oracle. Современные веб-разработчики имеют в своем распоряжении множество инструментов. Здесь я объясню, почему скрипты переноса данных могут быть приятны для работы в Ruby. Я буду иметь дело с Oracle вместо миграций в Rails.

В моих примерах я буду использовать OCI8, который является адаптером Oracle для Ruby. Это позволяет вам писать код Ruby для общения с вашим сервером базы данных Oracle.

Для начала миграция данных включает в себя нормализацию базы данных. Эта статья в Википедии объясняет:

Нормализация базы данных — это процесс организации полей и таблиц реляционной базы данных для минимизации избыточности. Нормализация обычно включает в себя разделение больших таблиц на более мелкие (и менее избыточные) таблицы и определение отношений между ними. Цель состоит в том, чтобы изолировать данные, чтобы в одну таблицу можно было добавлять, удалять и модифицировать поле, а затем распространять через остальную часть базы данных, используя определенные отношения.

Я только что завершил проект, в котором моя прежняя база данных была полна дубликатов данных. Это привело к проблемам с качеством данных и некоторому странному коду. Раздутая и уродливая база данных гарантирует код, который полон неправильных и странных взломов. Одним словом, мой проект остро нуждался в салоне красоты.

Настроить

Давайте определим пару таблиц:

CREATE TABLE answer ( answer_id NUMBER PRIMARY KEY, question_id NUMBER, answer_sequence NUMBER, answer_type VARCHAR2(7), answer_text VARCHAR2(31), updt_date DATE ); CREATE TABLE question ( question_id NUMBER PRIMARY KEY, question_text VARCHAR2(31), placement NUMBER, updt_date DATE ); 

Глядя на этот пример, мы имеем дело с вашей основной анкетой. Каждый ответ относится к вопросу, и ответы имеют определенный тип. Ответы могут быть либо радиобоксами, либо флажками в зависимости от вопроса.

Вот как выглядят наши данные.

Первый вопрос:

 INSERT INTO question ( question_id, -- why not just id? question_text, -- question gets implied placement, -- good, I like it! updt_date -- where is create date? ) VALUES ( 1, 'A question', 1, sysdate ); INSERT INTO answer ( answer_id, question_id, answer_sequence, -- how about placement? answer_type, answer_text, updt_date ) VALUES ( 1, 1, 1, 'MLT', -- duplicate 'First answer', sysdate ); INSERT INTO answer ( answer_id, question_id, answer_sequence, answer_type, answer_text, updt_date ) VALUES ( 2, 1, 2, 'MLT', -- duplicate 'Second answer', sysdate ); INSERT INTO answer ( answer_id, question_id, answer_sequence, answer_type, answer_text, updt_date ) VALUES ( 3, 1, 3, 'MULTI', -- what? that's, messed up! 'Third answer', sysdate ); 

Второй вопрос:

 INSERT INTO question ( question_id, question_text, placement, updt_date ) VALUES ( 2, 'Another question', 2, sysdate ); INSERT INTO answer ( answer_id, question_id, answer_sequence, answer_type, answer_text, updt_date ) VALUES ( 4, 2, 1, 'CHK', -- duplicate 'First answer', sysdate ); INSERT INTO answer ( answer_id, question_id, answer_sequence, answer_type, answer_text, updt_date ) VALUES ( 5, 2, 2, 'CHK', -- duplicate 'Second answer', sysdate ); 

Давайте посмотрим, что данные:

 SQL> SELECT answer_id, question_id, answer_sequence, answer_type, updt_date FROM answer; ANSWER_ID QUESTION_ID ANSWER_SEQUENCE ANSWER_ UPDT_DATE ---------- ----------- --------------- ------- --------- 1 1 1 MLT 23-JUN-14 2 1 2 MLT 23-JUN-14 3 1 3 MULTI 23-JUN-14 4 2 1 CHK 23-JUN-14 5 2 2 CHK 23-JUN-14 SQL> SELECT question_id, placement, updt_date FROM question; QUESTION_ID PLACEMENT UPDT_DATE ----------- ---------- --------- 1 1 23-JUN-14 2 2 23-JUN-14 

Тьфу! У меня есть дубликаты данных, непоследовательные и невообразимые имена столбцов и полуиспеченные отметки времени. Как прямое следствие, мои данные теперь имеют неприятные проблемы с качеством данных. О, парень.

Мы можем сделать лучше:

 CREATE TABLE answers ( id NUMBER CONSTRAINT answers_pk PRIMARY KEY, question_id NUMBER, placement NUMBER, text VARCHAR2(31), created_at DATE, updated_at DATE ); CREATE TABLE questions ( id NUMBER CONSTRAINT questions_pk PRIMARY KEY, type_id NUMBER, placement NUMBER, text VARCHAR2(31), created_at DATE, updated_at DATE ); CREATE TABLE question_types ( id NUMBER CONSTRAINT question_types_pk PRIMARY KEY, name VARCHAR(7), created_at DATE, updated_at DATE ); 

Моя модель данных здесь находится под сильным влиянием Rails. Типы ответов полностью вырываются из таблицы ответов и помещаются в вопрос. Вместо того, чтобы помещать повторяющиеся имена типов вопросов в таблицу, я поместил их в отдельную таблицу и добавил связь. Названия столбцов просты и интуитивно понятны.

Ловушки

Предположительно у вас есть рабочий код с новой схемой, а также прохождение тестов. Давайте теперь обратим наше внимание на производственные данные.

Обязательно установите драгоценный камень:

 gem install ruby-oci8 

Давайте начнем с этого простого скрипта миграции:

 require 'oci8' src = OCI8.new('username/password@schema') tgt = OCI8.new('username/password@schema') tgt.autocommit = false src.exec("SELECT question_id, ( SELECT answer.answer_type FROM answer WHERE answer.question_id = question.question_id AND ROWNUM = 1 ) question_type, question_text, placement, updt_date FROM question") do |qr| src.exec("SELECT answer_id, answer_sequence, answer_text, updt_date FROM answer WHERE question_id = :1", qr[0].to_i) do |ar| puts ar end end src.logoff tgt.logoff 

OCI8 не интуитивен в своих сообщениях об ошибках, поэтому я рекомендую пошаговые шаги. Здесь я запрашиваю данные src чтобы убедиться, что все работает. Обратите внимание, как я могу передать параметры в метод exec используя базовый Oracle. Я конвертирую типы в типы Ruby, чтобы в OCI8 ничего не было неправильно истолковано. tgt.autocommit = false позволяет мне тестировать мой код миграции, пока я не буду готов принять изменения. Вызов .logoff изящно убивает соединение с Oracle. Поскольку я прикрепляю типы ответов к вопросам, я должен выполнить подзапрос.

Больше примеров

Имея этот надежный фундамент, давайте начнем questions таблицу questions . Предполагая, что мы находимся внутри обратного вызова qr :

 QUESTION_TYPE_IDS = { "CHK" => 1, "MLT" => 2 } tgt.exec("SELECT questions_seq.nextval FROM dual") do |qid| tgt.exec("INSERT INTO questions ( id, type_id, placement, text, created_at, updated_at ) VALUES ( :1, :2, :3, :4, :5, :6 )", qid[0].to_i, QUESTION_TYPE_IDS[qr[1]].to_i, qr[3].to_i, qr[2].to_s[0..30], qr[4].to_date, qr[4].to_date) end end 

Я использую простой хеш с именем QUESTION_TYPE_IDS для преобразования типов вопросов в идентификаторы. Oracle применяет последовательности для первичных ключей, что заставляет меня поместить код INSERT в qid вызов qid . В моих хешах я использую приведение типа, чтобы убедиться, что никакие значения nil не nil во вставку. OCI8 будет выдавать неприятные и непонятные ошибки, если ничего не соответствует хешу.

Для текста вопроса типы VARCHAR2 имеют максимальную длину, которую мы не хотим превышать. Для этого просто преобразуйте в строку и выполните [0..30] для усечения значения. Это работает, даже если строка короче, чем предел.

В заключение давайте сосредоточим наше внимание на таблице answers . Этот блок кода идет внутри обратного вызова ar .

 tgt.exec("SELECT answers_seq.nextval FROM dual") do |aid| tgt.exec("INSERT INTO answers ( id, question_id, placement, text, created_at, updated_at ) VALUES ( :1, :2, :3, :4, :5, :6 )", aid[0].to_i, qid[0].to_i, ar[1].to_i, ar[2].to_s[0..30], ar[3].to_date, ar[3].to_date) end 

Это многое из того, что мы видели в questions миграции. Здесь мы отображаем qid из qid из блока вопросов. Таким образом, моя миграция данных учитывает отношения между объектами.

После всего этого нажмите переключатель и наблюдайте, как разворачивается магия.

 tgt.autocommit = true 

Теперь о самом трудном подвиге: откиньтесь на спинку кресла, закиньте руки за голову и дайте ему сделать свое дело. Для моего конкретного проекта эта миграция заняла более 15 минут. Мне казалось, что я погружаюсь в пучину бездны, измученная невыразимой тревогой, время перестало существовать. Но все закончилось успешно, и я благополучно приземлился на другой стороне.

Завершение

Время проверить наши окончательные таблицы answers и questions и посмотреть, как все выглядит. Я опущу поля VARCHAR2 для простоты. Я использую sqlplus в Oracle:

 SQL> SELECT id, question_id, placement, created_at, updated_at FROM answers; ID QUESTION_ID PLACEMENT CREATED_A UPDATED_A ---------- ----------- ---------- --------- --------- 1 1 1 10-JUN-14 10-JUN-14 2 1 2 10-JUN-14 10-JUN-14 3 1 3 10-JUN-14 10-JUN-14 4 2 1 10-JUN-14 10-JUN-14 5 2 2 10-JUN-14 10-JUN-14 SQL> SELECT id, type_id, placement, created_at, updated_at FROM questions; ID TYPE_ID PLACEMENT CREATED_A UPDATED_A ---------- ---------- ---------- --------- --------- 1 2 1 10-JUN-14 10-JUN-14 2 1 2 10-JUN-14 10-JUN-14 

Прекрасный.

Если вы заинтересованы, вы можете скачать все примеры кода с GitHub.

Счастливого взлома!