Rails Monolith에서 단일 테이블 상속을 제거하는 방법

기술 부채와 세금을 처리해야 할 때까지 상속은 쉽습니다.

Learn의 주요 코드베이스가 5 년 전에 등장했을 때 STI (Single Table Inheritance)는 매우 인기가있었습니다. 당시 Flatiron Labs 팀은 평가 및 커리큘럼부터 활동 피드 이벤트 및 컨텐츠에 이르기까지 모든 학습 관리 시스템 내에서이를 사용했습니다. 그리고 그것은 훌륭했습니다. 그것은 일을 끝냈습니다. 이를 통해 강사는 커리큘럼을 제공하고 학생의 진행 상황을 추적하며 매력적인 사용자 환경을 만들 수있었습니다.

그러나 많은 블로그 게시물 (예 :이 게시물,이 게시물 및이 게시물)에서 지적했듯이 STI는 특히 데이터가 커지고 새로운 하위 클래스가 수퍼 클래스와 서로 크게 달라짐에 따라 확장 성이 떨어집니다. 짐작 하셨겠지만 코드베이스에서도 마찬가지입니다! 우리 학교는 확장되었고 점점 더 많은 기능과 수업 유형을 지원했습니다. 시간이 지남에 따라 모델이 부풀어 오르고 변형되기 시작했으며 더 이상 도메인에 대한 올바른 추상화를 반영하지 않습니다.

우리는 그 공간에서 잠시 동안 살면서 코드에 폭을 넓히고 필요할 때만 패치했습니다. 그리고 시간이 리팩토링되었습니다.

지난 몇 달 동안, 나는 다소 불명확하게 명명 된 Content 모델을 포함하는 STI의 특히 끔찍한 인스턴스를 제거하는 임무를 시작했습니다. STI를 처음 설치하는 것처럼 쉽게 제거 할 수는 없습니다.

이 글에서는 STI에 대해 약간 다루고, 도메인에 대한 컨텍스트를 제공하고, 작업 범위를 설명하고, 코어를 잡아 당기는 동안 표면적을 최소화하면서 표면적을 최소화하면서 변경 사항을 안전하게 배포하기 위해 사용한 전략에 대해 설명합니다. 우리 앱의.

단일 테이블 상속 (STI) 정보

간단히 말해서 Rails의 단일 테이블 상속을 사용하면 여러 유형의 클래스를 동일한 테이블에 저장할 수 있습니다. 활성 레코드에서 클래스 이름은 테이블에 유형으로 저장됩니다. 예를 들어, 랩, 추가 정보 및 프로젝트가 모두 목차 테이블에있을 수 있습니다.

실험실 <내용; 종료
Readme 클래스 <내용; 종료
프로젝트 <내용; 종료

이 예제에서 랩, 추가 정보 및 프로젝트는 모두 레슨과 연관 될 수있는 모든 유형의 컨텐츠입니다.

우리의 목차 테이블의 스키마는 약간 비슷해 보였으므로 테이블에 유형이 저장되어 있음을 알 수 있습니다.

create_table "content", force : : cascade do | t |
  t. 정수 "curriculum_id",
  t.string "유형",
  t.text "markdown_format",
  t.string "제목",
  t. 정수 "track_id",
  t.integer "github_repository_id"
종료

작업 범위 식별

콘텐츠가 앱 전체에 퍼져서 때로는 혼란 스러웠습니다. 예를 들어, 이는 학습 모델의 관계를 설명했습니다.

수업 레슨 <커리큘럼
  has_many : contents,-> {order (ordinal : : asc)}
  has_one : content, foreign_key : : curriculum_id
  has_many : readmes, foreign_key : : curriculum_id
  has_one : lab, foreign_key : : 커리큘럼 _ID
  has_one : readme, foreign_key : : curriculum_id
  has_many : assigned_repos를 통해 : : contents
종료

혼란 스러운가? 저도 마찬가지였습니다. 그리고 그것은 제가 바꿔야 할 많은 모델 중 하나 일뿐입니다.

저의 훌륭한 재능있는 팀 동료들 (Kate Travers, Steven Nunez, Spencer Rogers)과 함께 저는 혼란을 줄이고이 시스템을보다 쉽게 ​​확장 할 수 있도록 더 나은 디자인을 고안했습니다.

새로운 디자인

콘텐츠가 나타내려고하는 개념은 GithubRepository와 레슨 간의 중개자였습니다.

"정식"강의 컨텐츠의 각 부분은 GitHub의 저장소에 연결됩니다. 수업이 학생들에게 게시되거나 "배치"될 때, 우리는 해당 GitHub 저장소의 사본을 만들어 학생들에게 링크를 제공합니다. 레슨과 배치 된 버전 간의 링크를 AssignedRepo라고합니다.

따라서 레슨의 양쪽 끝에 표준 버전과 배포 된 버전의 GitHub 리포지토리가 있습니다.

클래스 내용 
AssignedRepo 

어느 시점에서 수업은 여러 개의 컨텐츠를 가질 수 있었지만 현재 세계에서는 더 이상 그렇지 않습니다. 대신, 다양한 종류의 레슨이 있으며 관련 레파지토리에 포함 된 파일을 살펴봄으로써 스스로를 조사 할 수 있습니다.

따라서 우리가 결정한 것은 Content를 CanonicalMaterial이라는 새로운 개념으로 바꾸고 AssignedRepo에게 Content를 거치지 않고 관련 레슨에 대한 직접적인 참조를 제공하는 것입니다.

구식에서 새로운 시스템 다이어그램으로, 빨간색 점선은 더 이상 사용되지 않는 경로를 나타냅니다.

혼란스럽고 많은 일처럼 들린다면 그 이유 때문입니다. 그러나 중요한 점은 꽤 큰 코드베이스에서 모델을 교체하고 6000 줄의 코드 영역에서 어딘가에서 변경해야한다는 것입니다.

그러나 중요한 점은 꽤 큰 코드베이스에서 모델을 교체하고 6000 줄의 코드 영역에서 어딘가에서 변경해야한다는 것입니다.

STI 리팩토링 및 교체 전략

새로운 모델

먼저 canonical_materials라는 새 테이블을 만들고 새 모델과 연결을 만들었습니다.

CanonicalMaterial 클래스 

또한 커리큘럼 테이블에 외래 키 canonical_material_id를 추가하여 레슨이 참조를 유지할 수있었습니다.

assignment_repos 테이블에 lesson_id 열을 추가했습니다.

이중 쓰기

새 테이블과 열을 배치 한 후에는 이전 테이블과 새 테이블에 동시에 쓰기 시작하여 백업 광고 작업을 두 번 이상 실행할 필요가 없습니다. 콘텐츠 행을 만들거나 업데이트하려고 할 때마다 canonical_material도 만들거나 업데이트합니다.

예를 들면 다음과 같습니다.

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

lesson.canonical_material = repo.canonical_material
강의. 저장

이를 통해 궁극적으로 콘텐츠를 제거 할 수있는 토대를 마련했습니다.

되메우기

프로세스의 다음 단계는 데이터를 다시 채우는 것이 었습니다. 우리는 테이블을 채우고 각 GithubRepository에 CanonicalMaterial이 있고 각 레슨에 CanonicalMaterial이 있는지 확인하는 레이크 작업을 작성했습니다. 그런 다음 프로덕션 서버에서 작업을 실행했습니다.

이 리팩토링 과정에서 우리는 유효한 데이터를 선호하여 레거시 작업 방식을 완전히 깨뜨릴 수있었습니다. 그러나 또 다른 가능한 옵션은 여전히 ​​구형 모델을 지원하는 코드를 작성하는 것입니다. 우리의 경험에 따르면, 기존의 생각을 뒷받침하는 코드를 유지하면서 데이터를 다시 채우고 데이터를 확인하는 것보다 혼란스럽고 비용이 많이 듭니다.

우리의 경험에 따르면, 기존의 생각을 뒷받침하는 코드를 유지하면서 데이터를 다시 채우고 데이터를 확인하는 것보다 혼란스럽고 비용이 많이 듭니다.

바꿔 놓음

그리고 재미있는 부분이 시작되었습니다. 가능한 한 안전하게 교체하기 위해 피처 플래그를 사용하여 더 작은 PR에 다크 코드를 제공하여 피드백 루프를 빠르게 만들고 문제가 발생했는지 더 빨리 알 수있었습니다. 이를 위해 표준 기능 개발에도 사용하는 롤아웃 젬을 사용했습니다.

무엇을 검색

교체 작업을 수행하는 데있어 가장 어려운 부분 중 하나는 검색해야 할 부분이 많았습니다. '콘텐츠'라는 단어는 불행히도 매우 일반적인 단어이므로 간단한 전역 검색 및 바꾸기를 수행 할 수 없었기 때문에 변형을 설명하기 위해보다 범위가 넓은 검색을 수행하는 경향이있었습니다.

STI를 제거 할 때 다음을 검색해야합니다.

  • 모든 서브 클래스, 메소드, 유틸리티 메소드, 연관 및 조회를 포함하여 모델의 단수형 및 복수형.
  • 하드 코드 된 SQL 쿼리
  • 컨트롤러
  • 시리얼 라이저
  • 조회수

예를 들어 콘텐츠의 경우 다음을 찾습니다.

  • : content — 연결 및 쿼리
  • : contents — 연관 및 쿼리
  • .joins (: contents) — 조인 쿼리의 경우 이전 검색에서 포착해야합니다.
  • .includes (: contents) — 2 차 연결을 열망하는 데 사용되며 이전 검색에서도 포착해야합니다.
  • content : — 중첩 쿼리
  • 내용 : — 다시 한 번 더 중첩 된 쿼리
  • content_id —ID로 직접 쿼리
  • .content — 메소드 호출
  • .contents — 컬렉션 메서드 호출
  • .build_content — has_one 및 belongs_to 연관에 의해 추가 된 유틸리티 메소드
  • .create_content — has_one 및 belongs_to 연관에 의해 추가 된 유틸리티 메소드
  • .content_ids — has_many 연관에 의해 추가 된 유틸리티 메소드
  • 내용 — 클래스 이름 자체
  • contents — 하드 코딩 된 참조 또는 SQL 쿼리의 일반 문자열

나는 이것이 내용에 대한 포괄적 인 목록이라고 생각합니다. 그런 다음 랩, 추가 정보 및 프로젝트에 대해서도 동일한 작업을 수행했습니다. Rails는 매우 유연하고 많은 유틸리티 방법을 추가하기 때문에 모델이 사용되는 모든 장소를 찾기가 어렵습니다.

모든 발신자를 찾은 후 실제로 구현을 교체하는 방법

교체하거나 제거하려는 모델의 모든 통화 사이트를 실제로 찾은 후 다시 작성해야합니다. 일반적으로 우리가 따르는 과정은

  1. 정의에서 메소드 동작을 바꾸거나 호출 사이트에서 메소드를 변경하십시오.
  2. 새 메소드를 작성하고 호출 사이트에서 기능 플래그 뒤에서 호출하십시오.
  3. 메소드와의 연관성에 대한 종속성
  4. 방법이 확실하지 않은 경우 기능 플래그 뒤에서 오류 발생
  5. 인터페이스가 같은 객체로 교체

각 전략의 예는 다음과 같습니다.

1a. 메서드 동작 또는 쿼리 교체

대체품 중 일부는 매우 간단합니다. 기능 플래그를 "이 플래그가 켜져있을 때이 다른 코드 대신이 코드를 호출하십시오"라고 표시하십시오.

콘텐츠를 기반으로 쿼리하는 대신 canonical_material을 기반으로 쿼리합니다.

1b. 콜 사이트에서 방법 변경

때로는 호출 사이트에서 메소드를 교체하여 호출 된 메소드를 표준화하는 것이 더 쉽습니다. (이 작업을 수행 할 때 테스트 스위트를 실행하거나 테스트를 작성해야합니다.) 그렇게하면 추가 리팩토링의 길을 열 수 있습니다.

이 예는 더 이상 존재하지 않는 canonical_id 열에 대한 종속성을 해제하는 방법을 보여줍니다. 기능 플래그 뒤에 메소드를 배치하지 않고 콜 사이트에서 메소드를 교체했습니다. 이 리팩토링을 수행하면서 canonical_id를 여러 곳에서 뽑았으므로 다른 쿼리에 연결할 수있는 다른 방법으로 논리를 마무리했습니다. 통화 사이트의 방법이 변경되었지만 기능 플래그가 설정 될 때까지 동작은 변경되지 않았습니다.

2. 새로운 메소드를 작성하고 호출 사이트에서 기능 플래그 뒤에서 호출하십시오.

이 전략은 메소드 대체와 관련이 있으며,이 메소드에서만 새 메소드를 작성하여 호출 사이트의 기능 플래그 뒤에서 호출합니다. 한 곳에서만 호출 된 메소드에 특히 유용했습니다. 또한이 방법을 통해 더 나은 서명을 제공 할 수있었습니다. 항상 유용합니다.

3. 메소드와의 연관성에 대한 종속성

이 다음 예에서는 트랙 has_many labs입니다. has_many 연관이 유틸리티 메소드를 추가한다는 것을 알고 있으므로 has_many : labs 행을 가장 일반적으로 호출하여 제거했습니다. 이 메소드는 동일한 인터페이스를 준수하므로 기능을 켜기 전에 메소드를 호출 한 모든 것이 계속 작동합니다.

4. 방법이 확실하지 않은 경우 기능 플래그 뒤에 오류 발생

통화 사이트를 놓쳤는 지 확실하지 않은 경우가있었습니다. 따라서 처음에는 방법을 강제로 제거하는 대신 의도적으로 오류를 발생시켜 수동 테스트 단계에서 오류를 포착 할 수있었습니다. 이를 통해 메소드가 호출 된 위치를 추적하는 더 좋은 방법을 얻을 수있었습니다.

5. 동일한 인터페이스를 가진 객체를 교체

랩 연결을 없애고 싶었 기 때문에 랩 구현을 다시 작성 했습니까? 방법. 랩 레코드가 있는지 확인하는 대신 canonical_material을 바꾸고 호출을 위임했으며 해당 개체가 동일한 메서드에 응답하도록했습니다.

레일스 모놀리스 전체에서 의존성을 깨고 새로운 객체를 교체하는 데 가장 유용한 전략이었습니다. 수백 개의 정의와 호출 사이트를 검토 한 후 하나씩 바꾸거나 다시 작성했습니다. 지루한 과정은 아무도 원하지 않지만 코드베이스를 더 읽기 쉽게 만들고 아무것도하지 않는 오래된 코드를 제거하는 데 매우 도움이되었습니다. 끝날 때까지 몇 번의 실망스럽고 힘든 시간이 걸렸지 만 대부분의 참고 자료를 교체 한 후에는 수동 테스트를 시작했습니다.

테스트 및 수동 테스트

변경 사항이 전체 코드베이스에서 기능에 영향을 미쳤으므로 일부는 테스트되지 않았으므로 확실하게 품질 관리하기가 어려웠지만 최선을 다했습니다. QA 서버에서 수동 테스트를 수행하여 많은 버그와 엣지 사례가 발생했습니다. 그리고 나서 우리는 더 중요한 길을 가고 새로운 테스트를 작성했습니다.

롤아웃, 라이브 및 정리

QA를 통과 한 후 기능 플래그를 켜고 시스템을 정착시킵니다. 안정적인지 확인한 후 코드베이스에서 기능 플래그와 이전 코드 경로를 제거했습니다. 슬프게도 이것은 많은 테스트 스위트를 재 작성해야했기 때문에 예상보다 어려웠습니다. 주로 대부분 콘텐츠 모델에 의존했던 팩토리입니다. 돌이켜 보면 리팩토링하는 동안 현재 코드와 기능 플래그 뒤에있는 코드에 대한 두 가지 테스트 세트를 작성하는 것이 가능했습니다.

아직 다가올 마지막 단계로 데이터를 백업하고 사용하지 않는 테이블을 삭제해야합니다.

그리고 그 친구는 Rails 모놀리스에서 단일 테이블 상속을 없애는 한 가지 방법입니다. 이 사례 연구도 도움이 될 것입니다.

STI를 제거하거나 리팩토링하는 다른 방법이 있습니까? 우리는 궁금합니다. 의견에 알려주십시오.

또한 채용 중입니다! 우리 팀에 합류하십시오. 우리는 시원합니다, 약속합니다.

자료 및 추가 자료

  • 레일 가이드 상속
  • Eugene Wang (Flatiron Grad!)의 레일에서 단일 테이블 상속을 사용하는 방법과시기
  • 단일 테이블 상속에서 Rails 앱 리팩토링
  • 레일의 단일 테이블 상속 및 다형성 연관
  • Rails 5.02를 이용한 단일 테이블 상속

Flatiron School에 대한 자세한 내용을 보려면 웹 사이트를 방문하고 Facebook 및 Twitter에서 팔로우하고 가까운 행사에 방문하십시오.

Flatiron School은 WeWork 가족의 자랑스러운 회원입니다. 자매 기술 블로그 WeWork Technology 및 Making Meetup을 확인하십시오.