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

Наследование легко - пока вам не придется иметь дело с техническими долгами и налогами.

Когда пять лет назад появилась основная кодовая база Learn, Single Table Inheritance (STI) была довольно популярна. Команда Flatiron Labs в то время активно участвовала в этом - использовала ее для всего: от оценок и учебных программ до событий и контента в рамках нашей растущей системы управления обучением. И это было здорово - это сделало свою работу. Это позволило преподавателям составлять учебные планы, отслеживать успеваемость учащихся и создавать привлекательный пользовательский опыт.

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

Некоторое время мы жили в этом пространстве, предоставляя этому коду широкий доступ и исправляя его только при необходимости. И вот пришло время рефакторинга.

За последние несколько месяцев я приступил к выполнению задачи по удалению одного особенно грубого случая ИППП, который включал несколько неоднозначно названную модель содержания. Первоначально настроить STI так же просто, как удалить его.

Итак, в этом посте я расскажу немного о STI, предоставлю некоторый контекст о нашем домене, обрисую объем работ и расскажу о стратегиях, которые я использовал для безопасного развертывания изменений, при минимизации площади поверхности для серьезных повреждений, пока я потрошил ядро. нашего приложения.

О наследовании одной таблицы (STI)

Вкратце, наследование отдельных таблиц в Rails позволяет хранить несколько типов классов в одной таблице. В Active Record имя класса хранится как тип в таблице. Например, у вас могут быть Lab, Readme и Project, все живые в таблице содержимого:

класс Lab 

В этом примере labs, readmes и проекты - это все типы контента, которые могут быть связаны с уроком.

Схема нашей таблицы содержимого выглядит примерно так, так что вы можете видеть, что тип просто хранится в таблице.

create_table "content", force:: cascade do | t |
  t.integer "curriculum_id",
  t.string "тип",
  текст "markdown_format",
  t.string "название",
  t.integer "track_id",
  t.integer "github_repository_id"
конец

Определение объема работ

Контент растянулся по всему приложению, иногда смущает. Например, это описало отношения в модели урока.

Урок класса <Учебный план
  has_many: содержание, -> {порядок (порядковый номер:: ASC)}
  has_one: content, foreign_key:: curriculum_id
  has_many: readmes, foreign_key:: curriculum_id
  has_one: lab, foreign_key:: curriculum_id
  has_one: readme, foreign_key:: curriculum_id
  has_many: assign_repos, через:: содержание
конец

Смущенный? Я тоже. И это была лишь одна из многих моделей, которые мне пришлось изменить.

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

Новый дизайн

Концепция, которую Content пытался представить, была посредником между GithubRepository и Уроком.

Каждый фрагмент «канонического» содержания урока связан с хранилищем на GitHub. Когда уроки публикуются или «развертываются» для студентов, мы делаем копию этого репозитория GitHub и даем студентам ссылку на него. Связь между уроком и развернутой версией называется AssignedRepo.

Итак, репозитории GitHub имеются на обоих концах урока: каноническая версия и развернутая версия.

Класс Content 
класс AssignedRepo 

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

Итак, мы решили заменить Контент новой концепцией CanonicalMaterial и дать AssignedRepo прямую ссылку на соответствующий урок вместо того, чтобы переходить через Контент.

Схема старой системы к новой, где красными пунктирными линиями обозначены пути, помеченные как устаревшие

Если это звучит странно и требует много работы, то это потому, что так оно и есть. Однако ключевой вывод заключается в том, что нам пришлось заменить модель в довольно большой кодовой базе, и в итоге мы изменили где-то в области 6000 строк кода.

Однако ключевой вывод заключается в том, что нам пришлось заменить модель в довольно большой кодовой базе, и в итоге мы изменили где-то в области 6000 строк кода.

Стратегии рефакторинга и замены ИППП

Новая модель

Сначала мы создали новую таблицу с именем canonical_materials и создали новую модель и ассоциации.

класс CanonicalMaterial 

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

В таблицу assign_repos мы добавили столбец lesson_id.

Dual пишет

После того, как новые таблицы и столбцы были созданы, мы начали записывать старые и новые таблицы одновременно, чтобы нам не нужно было выполнять задание обратной засыпки более одного раза. Каждый раз, когда что-то пыталось создать или обновить строку содержимого, мы также создавали или обновляли canonical_material.

Например:

lesson.build_content (
  'repo_name' => repo.name,
  'github_repository_id' => repo_id,
  'markdown_format' => repo.readme
)

lesson.canonical_material = repo.canonical_material
lesson.save

Это позволило нам заложить основу для окончательного удаления контента.

тампонирование

Следующим шагом в этом процессе было заполнение данных. Мы написали рейк-задачи для заполнения наших таблиц и обеспечения того, чтобы CanonicalMaterial существовал для каждого GithubRepository и чтобы каждый урок имел CanonicalMaterial. И затем мы выполнили задачи на нашем производственном сервере.

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

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

замена

И тут началось самое интересное. Чтобы сделать замену максимально безопасной, мы использовали флаги функций для отправки темного кода в меньшие PR, что позволило нам создать более быструю обратную связь и быстрее узнать, что происходит. Для этого мы использовали гем свитка, который мы также используем для разработки стандартных функций.

Что искать

Одной из самых сложных частей замены было огромное количество вещей для поиска. Слово «контент», к сожалению, является очень общим, поэтому было невозможно выполнить простой, глобальный поиск и замену, поэтому я старался выполнять поиск более ограниченного масштаба, пытаясь учесть различия.

При удалении ИППП вам нужно искать следующие вещи:

  • Форма единственного и множественного числа модели, включая все ее подклассы, методы, служебные методы, ассоциации и запросы.
  • Жестко закодированные SQL-запросы
  • Контроллеры
  • сериализаторы
  • Взгляды

Например, для контента это означало поиск:

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

Я считаю, что это довольно полный список контента. И затем я сделал то же самое для лаборатории, readme и проекта. Вы можете видеть, что, поскольку Rails настолько гибок и добавляет много служебных методов, трудно найти все места, где модель в конечном итоге используется.

Как на самом деле заменить реализацию после того, как вы нашли всех абонентов

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

  1. Замените поведение метода в определении или измените метод на сайте вызова
  2. Напишите новые методы и вызовите их за флагом функции на сайте вызовов.
  3. Разрушить зависимости от ассоциаций с методами
  4. Поднимите ошибки за флагом функции, если вы не уверены в методе
  5. Поменяйте местами объекты с одинаковым интерфейсом

Вот примеры каждой стратегии.

1a. Заменить поведение метода или запрос

Некоторые из замен довольно просты. Вы устанавливаете флаг функции на место, чтобы сказать «вызовите этот код вместо этого другого кода, когда этот флаг включен».

Таким образом, вместо запросов на основе содержимого, здесь мы запрашиваем на основе canonical_material.

1б. Изменить метод на сайте вызова

Иногда проще заменить метод на сайте вызова для стандартизации вызываемых методов. (При этом вы должны запустить свой набор тестов и / или написать тесты.) Это может открыть путь к дальнейшему рефакторингу.

Этот пример демонстрирует, как сломать зависимость от столбца canonical_id, который скоро больше не будет существовать. Обратите внимание, что мы заменили метод на сайте вызовов, не помещая его за флаг функции. Выполняя этот рефакторинг, мы заметили, что мы собрали canonical_id более чем в одном месте, поэтому мы свернули логику, чтобы сделать это в другом методе, который мы могли бы связать с другими запросами. Метод на сайте вызова был изменен, но поведение не изменилось, пока не был включен флаг функции.

2. Напишите новые методы и вызовите их за флагом функции на сайте вызовов.

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

3. Разрушить зависимости от ассоциаций с методами.

В следующем примере трек has_many labs. Поскольку мы знаем, что ассоциация has_many добавляет служебные методы, мы заменили один из наиболее часто вызываемых и удалили строку has_many: labs. Этот метод соответствует тому же интерфейсу, поэтому все, что вызывало метод до включения функции, продолжало бы работать.

4. Поднимите ошибки за флагом функции, если вы не уверены в методе

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

5. Поменяйте местами объекты с одинаковым интерфейсом

Поскольку мы хотели избавиться от лабораторной ассоциации, мы переписали реализацию лаборатории? метод. Вместо того, чтобы проверять наличие лабораторной записи, мы обменивались каноническим_материалом, делегировали вызов и заставляли этот объект отвечать на тот же метод.

Это были самые полезные стратегии для разрушения зависимостей и обмена новыми объектами в нашем монолите Rails. Изучив сотни определений и сайтов вызовов, мы заменили или переписали их одно за другим. Это утомительный процесс, который я никому не желаю, но в конечном итоге он был чрезвычайно полезен для того, чтобы сделать нашу кодовую базу более разборчивой и удалить старый код, который бездействовал, ничего не делая. Потребовалось несколько разочаровывающих и затягивающих недель, чтобы дойти до конца, но как только мы заменили большинство ссылок, мы начали проводить ручное тестирование.

Тестирование и ручное тестирование

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

Развернись, живи и убирайся

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

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

И это, друзья, это один из способов избавиться от растущего наследования единой таблицы в вашем монолите Rails. Возможно, этот пример также поможет вам.

У вас есть другие способы удаления ИППП или рефакторинга? Нам интересно знать. Дайте нам знать об этом в комментариях.

Кроме того, мы нанимаем! Присоединиться к нашей команде. Мы крутые, я обещаю.

Ресурсы и дополнительное чтение

  • Наследование направляющих рельсов
  • Как и когда использовать наследование одной таблицы в Rails от Eugene Wang (Flatiron Grad!)
  • Рефакторинг нашего Rails-приложения из-за наследования одной таблицы
  • Наследование в одной таблице против полиморфных ассоциаций в Rails
  • Наследование в одной таблице с использованием Rails 5.02

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

Школа Flatiron является гордым членом семьи WeWork. Посетите наши родственные технологические блоги WeWork Technology and Making Meetup.