Як видалити спадкування єдиного столу з моноліту рейок

Спадкування легко - поки вам не доведеться мати справу з технічною заборгованістю та податками.

Коли основна база коду Learn з'явилася п'ять років тому, наслідування єдиної таблиці (STI) було досить популярним. У той час колектив Flatiron Labs працював над цим - використовуючи його для всього, від оцінок та навчальних програм до подій та змісту в рамках діяльності в нашій зростаючій системі управління навчанням. І це було чудово - це виконало роботу. Це дозволило викладачам здійснювати навчальні програми, відстежувати успішність учнів та створювати захоплюючу роботу користувачів.

Але як зазначалося багато публікацій в блогах (цей, цей, і цей, наприклад), STI не масштабує надто добре, тим більше, що дані ростуть і нові підкласи починають сильно відрізнятися від їх суперклассів та один від одного. Як ви могли здогадатися, те саме сталося і в нашій кодовій базі! Наша школа розширювалася, і ми підтримували все більше можливостей та типів уроків. З часом моделі почали роздуватися і мутувати і більше не відображають правильну абстракцію для домену.

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

Протягом останніх декількох місяців я взяв на себе завдання по вилученню одного особливо привабливого екземпляра ІПСШ, який стосувався дещо неоднозначно названої моделі змісту. Настільки просто, як STI налаштовувати спочатку, його фактично досить важко видалити.

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

Про спадкування однієї таблиці (STI)

Коротше кажучи, Успадкування однієї таблиці в Rails дозволяє зберігати кілька типів класів в одній таблиці. В Active Record назва класу зберігається як тип у таблиці. Наприклад, у вас можуть бути Lab, Readme та Project усі в прямому ефірі в таблиці вмісту:

клас Lab <Зміст; кінець
клас Readme <Зміст; кінець
клас Проект <Зміст; кінець

У цьому прикладі лабораторії, ридеми та проекти - це всі типи вмісту, які можуть бути пов’язані з уроком.

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

create_table "content", force:: каскад do | t |
  t.integer "навчальний план_id",
  t.string "тип",
  t.text "markdown_format",
  t.string "назва",
  t.integer "track_id",
  t.integer "github_repository_id"
кінець

Визначення сфери роботи

Вміст розповсюджується по всьому додатку, іноді заплутано. Наприклад, це описало взаємозв'язки в моделі уроку.

урок класу <Навчальний план
  has_many: content, -> {порядок (порядковий:: asc)}
  has_one: вміст, Foreign_key:: навчальний план_id
  has_many: readmes, Foreign_key:: навчальний план_id
  has_one: лабораторія, іноземний ключ:: навчальний план_id
  has_one: readme, Foreign_key:: навчальний план_id
  has_many: прызначаний_repos, через:: content
кінець

Плутати? Так було і я. І це була лише одна з багатьох моделей, яку мені довелося змінити.

Отож, зі своїми блискучими та талановитими товаришами по команді (Кейт Траверс, Стівен Нунес та Спенсер Роджерс) я задумав кращий дизайн, щоб допомогти зменшити сум'яття та полегшити цю систему.

Новий дизайн

Концепція, яку Контент намагався представляти, був посередником між GithubRepository та Lesson.

Кожен фрагмент вмісту "канонічного" уроку пов'язаний із сховищем на GitHub. Коли уроки публікуються або «розгортаються» для студентів, ми робимо копію цього сховища GitHub і надаємо учням посилання на нього. Посилання між уроком та розгорнутою версією називається AssignedRepo.

Так є сховища GitHub на обох кінцях уроків: канонічна версія та розгорнута версія.

Вміст класу 
клас AssignedRepo 

Одного разу на уроках вдалося мати декілька змістів, але в нашому теперішньому світі це вже не так. Натомість існують різні види уроків, які можуть самоаналізувати себе, переглянувши файли, що входять у їх пов’язані сховища.

Тож, що ми вирішили зробити, це замінити Зміст новою концепцією під назвою CanonicalMaterial, і надати AssignedRepo пряме посилання на пов’язаний з нею урок замість того, щоб переглядати вміст.

Діаграма від старого до нового, де червоними пунктирними лініями позначені шляхи, позначені для анулювання

Якщо це звучить заплутано і вам подобається багато роботи, це тому, що це так. Ключовим виводом є те, що нам довелося замінити модель на досить великій базі коду, і в кінцевому підсумку змінилося десь у царині 6000 рядків коду.

Ключовим виводом є те, що нам довелося замінити модель на досить великій кодовій базі, і в кінцевому підсумку змінилося десь у царині 6000 рядків коду.

Стратегії рефакторингу та заміни ІПСШ

Нова модель

По-перше, ми створили нову таблицю під назвою canonical_materials і створили нову модель та асоціації.

клас CanonicalMaterial 

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

До таблиці призначений_repos ми додали стовпець_урок.

Подвійні записи

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

Наприклад:

lection.build_content (
  'repo_name' => ім'я repo.name,
  'github_repository_id' => repo_id,
  'markdown_format' => repo.readme
)

урок.canonical_material = repo.canonical_material
урок.зберегти

Це дозволило нам закласти основу для остаточного видалення вмісту.

Засипка

Наступним кроком у процесі було заповнення даних. Ми написали завдання граблі, щоб заповнити наші таблиці і переконатися, що CanonicalMaterial існує для кожного GithubRepository і що кожен урок мав CanonicalMaterial. А потім ми виконували завдання на своєму виробничому сервері.

У цьому раунді рефакторингу ми вважали за краще мати достовірні дані, щоб ми могли зробити чистий розрив із застарілим способом виконання дій. Ще одним можливим варіантом є написання коду, який все ще підтримує більш старі моделі. На наш досвід, підтримувати код, який підтримує застаріле мислення, набагато заплутаніше та затратніше, ніж це було для повторного заповнення та переконання, що дані є достовірними.

На наш досвід, підтримувати код, який підтримує застаріле мислення, набагато заплутаніше та затратніше, ніж це було для повторного заповнення та переконання, що дані є достовірними.

Заміна

А потім розпочалася весела частина. Для того, щоб зробити заміну максимально безпечною, ми використовували прапорці функцій для надсилання темного коду з меншими PR-адресами, що дозволило нам створити швидший цикл зворотного зв’язку і швидше дізнатись, чи все порушиться. Для цього ми використовували дорогоцінний камінь rollout, який ми також використовуємо для розробки стандартних функцій.

Що шукати

Однією з найскладніших частин заміни була велика кількість речей, які потрібно шукати. Слово "вміст", на жаль, надто загальне, тому простого, глобального пошуку та заміни неможливо було зробити, тому я прагнув зробити більш масштабний пошук, намагаючись врахувати варіанти.

Під час видалення ІПСШ слід шукати такі речі:

  • Форми однини та множини моделі, включаючи всі її підкласи, методи, корисні методи, асоціації та запити.
  • Жорсткі коди SQL-запитів
  • Контролери
  • Серіалізатори
  • Перегляди

Наприклад, для вмісту, який означав пошук:

  • : вміст - для асоціацій та запитів
  • : вміст - для асоціацій та запитів
  • .joins (: content) - для запитів приєднання, які повинні бути відзняті попереднім пошуком
  • .includes (: content) - для нетерплячого завантаження асоціацій другого порядку, які також повинні бути спіймані попереднім пошуком
  • вміст: - для вкладених запитів
  • вміст: - знову ж таки, більше вкладених запитів
  • content_id - для запитів безпосередньо за ідентифікатором
  • .content - виклики методу
  • .contents - метод збору викликів
  • .build_content - корисний метод, доданий асоціацією has_one і належить_to
  • .create_content - метод утиліти, доданий асоціацією has_one і належить_to
  • .content_ids - корисний метод, доданий асоціацією has_many
  • Зміст - сама назва класу
  • content - звичайна рядок для будь-яких твердо кодованих посилань або SQL запитів

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

Як реально замінити реалізацію після того, як ви знайшли всіх абонентів

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

  1. Замініть поведінку методу у визначенні або змініть метод на сайті виклику
  2. Напишіть нові методи та зателефонуйте їм за прапором функції на сайті виклику
  3. Розбийте залежності від асоціацій із методами
  4. Якщо ви не впевнені в методі, піднімайте помилки за прапором функції
  5. Обміняйтесь об'єктами, що мають той самий інтерфейс

Ось приклади кожної стратегії.

1а. Замініть поведінку або запит методу

Деякі заміни досить прості. Ви ставите прапор функції на місце, щоб сказати "зателефонувати цьому коду замість цього іншого коду, коли цей прапор увімкнено"

Отже, замість запитів на основі вмісту, ми запитуємо на основі canonical_material.

1б. Змініть метод на сайті виклику

Іноді простіше замінити метод на сайті виклику, щоб стандартизувати названі методи. (Ви повинні запустити тестовий набір та / або написати тести, коли це зробите.) Це може відкрити шлях до подальшого рефакторингу.

Цей приклад демонструє, як розбити залежність від стовпця canonical_id, який незабаром більше не буде існувати. Зауважте, що ми замінили метод на сайті виклику, не ставлячи його за прапор функції. Роблячи цей рефакторинг, ми помітили, що ми викрали canonical_id у більш ніж одному місці, тому ми перетворили логіку, щоб зробити це в іншому методі, щоб ми могли ланцюг на інші запити. Метод на сайті виклику було змінено, але поведінка не змінювалася, поки прапор функції не було ввімкнено.

2. Напишіть нові методи та зателефонуйте їм за прапором функції на сайті виклику

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

3. Розбийте залежності від асоціацій із методами

У цьому наступному прикладі трек has_many labs. Оскільки ми знаємо, що асоціація has_many додає корисні методи, ми замінили той, який найчастіше називається та видалив рядок has_many: labs. Цей метод відповідає одному інтерфейсу, тому все, що викликало метод до ввімкнення функції, продовжувало б працювати.

4. Піднімайте помилки за прапором функції, якщо ви не впевнені в методі

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

5. Обмінюйтесь об'єктами, що мають однаковий інтерфейс

Оскільки ми хотіли позбутися лабораторної асоціації, ми переписали реалізацію лабораторії? метод. Замість того, щоб перевіряти наявність лабораторного запису, ми помінялися на canonical_material, делегували виклик і змусили цей об'єкт відповідати тим самим методом.

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

Тестування та ручне тестування

Оскільки зміни торкнулися функцій у всій базі коду, деякі з яких не проходили тестування, QA було важко з упевненістю, але ми зробили все можливе. Ми провели вручну тестування на нашому QA-сервері, який виявив багато помилок і кращих випадків. А потім ми пішли вперед і за більш критичними шляхами, написали нові тести.

Розкачуйте, переходьте та очищайте

Після проходження QA ми переставили прапор свого функціоналу та дозволили системі влаштуватися. Після того як ми були впевнені, що він стабільний, ми видалили прапорці функції та старі кодові шляхи з бази даних. Це, на жаль, було складніше, ніж очікувалося, оскільки воно тягнуло за собою переписання багатьох тестових наборів, переважно фабрик, які неявно спиралися на модель вмісту. В ретроспективі ми могли б зробити два тести, коли ми рефакторингували: один для поточного коду та один для коду за прапором функції.

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

І це, друзі, - це один із способів позбутися розповсюдження наслідування єдиного столу у вашому моноліті Rails. Можливо, і цей кейс допоможе вам.

Чи є у вас інші способи видалення ІПСШ або рефакторинг? Нам цікаво знати. Повідомте нас у коментарях.

Також ми наймаємо! Приєднуйтесь до нашої команди. Ми круті, обіцяю.

Ресурси та додаткове читання

  • Спадкування напрямних напрямних
  • Як і коли використовувати спадкування одного столу в рейках від Євгенія Ванга (Flatiron Grad!)
  • Рефакторинг нашої програми Rails із спадкування за одним столом
  • Спадщина єдиного столу проти поліморфних асоціацій на рейках
  • Спадкування єдиного столу за допомогою рейлінгів 5.02

Щоб дізнатись більше про школу Flatiron, відвідайте веб-сайт, слідкуйте за нами у Facebook та Twitter та відвідайте нас на майбутніх заходах поблизу вас.

Школа Flatiron є гордим членом родини WeWork. Перегляньте наші блоги із сестринськими технологіями WeWork Technology та створення зустрічей.