Bagaimana Menghapus Warisan Jadual Tunggal dari Monolith Rail Anda

Warisan mudah - sehingga anda perlu berurusan dengan hutang teknikal dan cukai.

Apabila dasar utama Belajar muncul lima tahun yang lalu, Single Table Inheritance (STI) sangat popular. Pasukan Flatiron Labs pada masa itu memasuki semua itu - menggunakannya untuk segala-galanya dari penilaian dan kurikulum hingga aktiviti feed event dan kandungan dalam sistem pengurusan pembelajaran yang semakin berkembang. Dan itu hebat - ia mendapat kerja. Ia membenarkan jurulatih untuk menyampaikan kurikulum, menjejaki kemajuan pelajar, dan mencipta pengalaman pengguna yang menarik.

Tetapi seperti banyak catatan blog telah menunjukkan (yang satu ini, yang satu ini dan yang satu ini sebagai contoh), STI tidak berskala baik, terutamanya apabila data berkembang dan subkategori baru mula berubah-ubah secara meluas dari cermin cakar dan satu sama lain. Seperti yang anda mungkin telah meneka, perkara yang sama berlaku di pangkalan kita! Sekolah kami berkembang dan kami menyokong lebih banyak ciri dan jenis pengajaran. Dari masa ke masa, model mula mengalir dan bermutasi dan tidak lagi mencerminkan abstraksi yang betul untuk domain tersebut.

Kami tinggal di ruang itu untuk seketika, memberi kod itu tempat tidur yang luas, dan menampal hanya apabila diperlukan. Dan kemudian waktunya datang kepada refactor.

Sepanjang beberapa bulan yang lalu, saya memulakan misi untuk menghapuskan satu contoh STI yang sangat kemas, yang melibatkan model ContentContent yang agak samar-samar. Sudah semestinya STI ditubuhkan pada mulanya, ia sebenarnya agak sukar untuk dialih keluar.

Oleh itu, dalam jawatan ini, saya akan membincangkan sedikit mengenai STI, menyediakan beberapa konteks tentang domain kami, menggariskan skop kerja, dan membincangkan strategi yang saya gunakan untuk menggunakan perubahan dengan selamat sambil meminimumkan kawasan permukaan untuk kerosakan yang serius semasa saya memecahkan teras aplikasi kami.

Mengenai Tabel Warisan Satu (STI)

Secara ringkasnya, Jadual Tunggal dalam Rails membolehkan anda menyimpan pelbagai jenis kelas dalam jadual yang sama. Dalam Rekod Aktif, nama kelas disimpan sebagai jenis dalam jadual. Sebagai contoh, anda mungkin mempunyai Lab, Readme, dan Projek yang semuanya hidup dalam jadual kandungan:

kelas Lab 

Dalam contoh ini, makmal, buku bacaan dan projek adalah semua jenis kandungan yang boleh dikaitkan dengan pelajaran.

Skema jadual kandungan kami kelihatan agak seperti ini, jadi anda dapat melihat jenis tersebut disimpan di dalam jadual.

create_table "content", force:: cascade do | t |
  t.integer "kurikulum_id",
  t.string "type"
  t.text "markdown_format",
  t.string "tajuk",
  t.integer "track_id",
  t.integer "github_repository_id"
akhir

Mengenal pasti Skop Kerja

Kandungan tergelincir di seluruh aplikasinya, kadang-kadang agak mengelirukan. Sebagai contoh, ini menerangkan hubungan dalam model Pelajaran.

kelas Pelajaran  {order (ordinal:: asc)}
  has_one: content, foreign_key:: kurikulum_id
  has_many: readmes, foreign_key:: kurikulum_id
  has_one: lab, foreign_key:: kurikulum_id
  has_one: readme, foreign_key:: kurikulum_id
  has_many: assigned_repos, melalui:: contents
akhir

Bingung? Begitu juga I. Dan itu hanya satu model banyak yang perlu saya ubah.

Oleh itu, dengan rakan sepasukan yang cemerlang dan berbakat (Kate Travers, Steven Nunez, dan Spencer Rogers), saya mencadangkan reka bentuk yang lebih baik untuk membantu mengurangkan kekeliruan dan menjadikan sistem ini lebih mudah untuk diperluaskan.

Reka Bentuk Baru

Konsep bahawa Kandungan yang cuba diwakili adalah perantara antara GithubRepository dan Pelajaran.

Setiap kandungan pelajaran "kanonik" dikaitkan dengan repositori pada GitHub. Apabila pelajaran diterbitkan atau "dikerahkan" kepada pelajar, kami membuat salinan repositori GitHub itu dan memberi pelajar pautan kepadanya. Pautan antara pelajaran dan versi yang dipakai dipanggil AssignedRepo.

Jadi terdapat repositori GitHub di kedua-dua hujung pelajaran: versi kanonis dan versi yang digunakan.

kelas Content 
kelas AssignedRepo 

Pada satu ketika, pelajaran dapat mempunyai beberapa keping kandungan, tetapi di dunia kita sekarang, itu tidak lagi berlaku. Sebaliknya, terdapat pelbagai jenis pelajaran, yang boleh mengamalkan diri mereka dengan melihat fail yang termasuk dalam repositori yang berkaitan.

Jadi, apa yang kami buat ialah menggantikan Kandungan dengan konsep baru yang dipanggil CanonicalMaterial, dan memberikan AssignedRepo rujukan langsung kepada pelajaran yang berkaitan dan bukannya melalui Kandungan.

Lama kepada Sistem Baru Diagram, di mana garis putus-putus merah menandakan laluan yang ditandakan untuk ditolak

Sekiranya ia membingungkan dan seperti banyak kerja, ia adalah kerana ia adalah. Takeaway utama walaupun adalah bahawa kita terpaksa mengganti model dalam asas yang cukup besar, dan akhirnya berubah di suatu tempat dalam bidang 6000 baris kod.

Takeaway utama walaupun adalah bahawa kita terpaksa mengganti model dalam asas yang cukup besar, dan akhirnya berubah di suatu tempat dalam bidang 6000 baris kod.

Strategi untuk Refactoring dan Menggantikan STI

Model Baru

Pertama, kami mencipta jadual baru yang dipanggil canonical_materials dan mencipta model dan persatuan baru.

kelas CanonicalMaterial 

Kami juga menambah kunci asing kanonikal_material_id ke jadual kurikulum, supaya Pelajaran dapat mempertahankan rujukannya.

Kepada jadual yang ditugaskan, kami menambah lajur lesson_id.

Dual Writes

Setelah jadual dan lajur baru selesai, kami mula menulis ke jadual lama dan yang baru pada masa yang sama supaya kami tidak perlu menjalankan tugas backfill lebih daripada sekali. Sebarang masa yang cuba mencipta atau mengemas kini baris kandungan, kami juga akan mencipta atau mengemas kini bahan kanonikal.

Sebagai contoh:

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

lesson.canonical_material = repo.canonical_material
lesson.save

Ini membolehkan kami meletakkan asas untuk akhirnya mengeluarkan Kandungan.

Backfilling

Langkah seterusnya dalam proses ini adalah untuk mengembalikan data. Kami menulis tugas rake untuk mengisi jadual kami dan memastikan bahawa Bahan Canonical wujud untuk setiap GithubRepository dan setiap Pelajaran mempunyai Bahan Canonical. Dan kemudian kami menjalankan tugas-tugas di pelayan pengeluaran kami.

Dalam pusingan refactoring ini, kami lebih suka mempunyai data yang sah supaya kami boleh membuat rehat bersih dengan cara warisan melakukan sesuatu. Walau bagaimanapun, satu lagi pilihan yang sesuai ialah menulis kod yang masih menyokong model yang lebih lama. Dalam pengalaman kami, ia telah menjadi lebih mengelirukan dan mahal untuk mengekalkan kod yang menyokong pemikiran legasi daripada yang telah diulangi dan memastikan data sah.

Dalam pengalaman kami, ia telah menjadi lebih mengelirukan dan mahal untuk mengekalkan kod yang menyokong pemikiran legasi daripada yang telah diulangi dan memastikan data sah.

Penggantian

Dan kemudian bahagian yang menyeronokkan bermula. Untuk menjadikan penggantian sebagai selamat mungkin, kami menggunakan bendera ciri untuk menghantar kod gelap dalam PR yang lebih kecil, yang membolehkan kami membuat gelung maklum balas yang lebih cepat dan mengetahui lebih awal jika sesuatu berlaku. Kami menggunakan permata pelancaran, yang kami juga gunakan untuk pembangunan ciri standard, untuk melakukan ini.

Apa yang hendak dicari

Salah satu bahagian yang paling sukar dilakukan ialah penggantian beberapa perkara untuk dicari. Perkataan "kandungan" sayangnya sangat generik, jadi tidak mustahil untuk melakukan carian global yang mudah dan menggantikan, jadi saya cenderung melakukan pencarian yang lebih banyak untuk mencuba variasi.

Apabila mengeluarkan STI, ini adalah perkara yang perlu anda cari:

  • Bentuk tunggal dan jamak model, termasuk semua subkelas, kaedah, kaedah utiliti, persatuan dan pertanyaannya.
  • Pertanyaan SQL yang dikodkan
  • Pengawal
  • Serializers
  • Pandangan

Sebagai contoh, untuk kandungan, itu bermakna mencari:

  • : kandungan - untuk persatuan dan pertanyaan
  • : kandungan - untuk persatuan dan pertanyaan
  • .lawan (: kandungan) - untuk menyertai pertanyaan, yang harus ditangkap oleh carian terdahulu
  • .includes (: contents) - untuk memuatkan persatuan pesanan kedua yang semestinya, yang juga harus ditangkap oleh carian terdahulu
  • kandungan: - untuk pertanyaan bersarang
  • kandungan: - lagi, pertanyaan yang lebih bersarang
  • content_id -for pertanyaan terus dengan id
  • .content - kaedah panggilan
  • .contents - kaedah pengumpulan panggilan
  • .build_content - kaedah utiliti yang ditambah oleh has_one dan belong_to persatuan
  • .create_content - kaedah utiliti ditambah oleh has_one dan belong_to persatuan
  • .content_ids - kaedah utiliti yang ditambah oleh persatuan has_many
  • Kandungan - nama kelas itu sendiri
  • kandungan - rentetan biasa untuk sebarang rujukan kerasata atau pertanyaan SQL

Saya percaya itu adalah senarai yang cukup komprehensif untuk kandungan. Dan kemudian saya melakukan yang sama untuk makmal, readme, dan projek. Anda dapat melihat bahawa kerana Rails sangat fleksibel dan menambah banyak kaedah utiliti, sukar untuk mencari semua tempat yang model akhirnya digunakan.

Bagaimana Sebenarnya Ganti Pelaksanaan Selepas Anda Telah Menemukan Semua Pemanggil

Sebaik sahaja anda benar-benar menemui semua tapak panggilan model yang anda cuba ganti atau mengeluarkan, anda dapat menulis semula perkara. Secara umum, proses yang kami ikuti adalah

  1. Gantikan tingkah laku kaedah dalam definisi atau ubah kaedah di tapak panggilan
  2. Tulis kaedah baru dan hubungi mereka di belakang bendera ciri di tapak panggilan
  3. Tergantung kepada persatuan dengan kaedah
  4. Naikkan kesilapan di sebalik bendera ciri jika anda tidak pasti tentang kaedah
  5. Pertukaran objek yang mempunyai antara muka yang sama

Inilah contoh setiap strategi.

1a. Gantikan tingkah laku kaedah atau pertanyaan

Beberapa pengganti agak mudah. Anda meletakkan bendera ciri di tempat untuk mengatakan "memanggil kod ini dan bukan kod ini yang lain apabila bendera ini dihidupkan."

Jadi, bukannya pertanyaan berdasarkan kandungan, di sini kita menanyakan berdasarkan kanonikal_material.

1b. Tukar kaedah di tapak panggilan

Kadang-kadang, lebih mudah untuk menggantikan kaedah di tapak panggilan untuk menyeragamkan kaedah yang dipanggil. (Anda harus menjalankan ujian ujian dan / atau menulis ujian apabila anda melakukan ini.) Melakukannya boleh membuka laluan untuk refactoring selanjutnya.

Contoh ini menunjukkan cara untuk memecah kebergantungan pada lajur kanonikal_id, yang tidak lama lagi akan wujud. Perhatikan bahawa kami menggantikan kaedah di tapak panggilan tanpa meletakkannya di belakang bendera ciri. Dalam melakukan refactoring ini, kita perhatikan bahawa kita memetik canonical_id di lebih dari satu tempat, jadi kita membungkus logik untuk melakukan itu dengan kaedah lain yang kita boleh berantai ke pertanyaan lain. Kaedah di laman panggilan telah ditukar, tetapi tingkah laku tidak berubah sehingga bendera ciri dihidupkan.

2. Tulis kaedah baru dan panggilnya di belakang bendera ciri di tapak panggilan

Strategi ini berkaitan dengan penggantian kaedah, hanya dalam satu ini, kita menulis kaedah baru dan memanggilnya di sebalik bendera ciri di tapak panggilan. Ia amat berguna untuk satu kaedah yang hanya dipanggil di satu tempat. Ia juga membolehkan kami memberi kaedah itu sebagai tanda yang lebih baik - sentiasa berguna.

3. Terikan dependensi pada persatuan dengan kaedah

Dalam contoh berikut ini, trek mempunyai_ makmal. Kerana kita tahu bahawa persatuan has_many menambah kaedah utiliti, kita menggantikan yang paling biasa dipanggil dan dikeluarkan has_many: garis lab. Kaedah ini mematuhi antara muka yang sama, jadi apa-apa yang memanggil kaedah sebelum ciri dihidupkan akan terus berfungsi.

4. Tingkatkan kesilapan di sebalik bendera ciri jika anda tidak pasti tentang kaedah

Terdapat beberapa kali bahawa kami tidak pasti sama ada kami terlepas laman panggilan. Jadi, bukannya hanya kaedah pengalihan keras pada mulanya, kami sengaja menimbulkan kesilapan supaya kami dapat menangkap mereka semasa fasa ujian manual. Ini memberikan kita cara yang lebih baik untuk mengesan di mana kaedah dipanggil.

5. Tukar objek yang mempunyai antara muka yang sama

Kerana kita mahu menyingkirkan persatuan makmal, kita menulis semula pelaksanaan makmal? kaedah. Daripada memeriksa kehadiran rekod makmal, kami bertukar dalam bahan kanonikal, mewakilkan panggilan, dan membuat objek itu bertindak balas dengan kaedah yang sama.

Ini adalah strategi yang paling membantu untuk memecah kebergantungan dan menukar dalam objek baru di seluruh monolit Rails kami. Selepas mengkaji ratusan definisi dan tapak panggilan, kami menggantikan atau menulis semula mereka satu persatu. Ia adalah satu proses yang membosankan yang tidak saya kehendaki pada sesiapa pun, tetapi akhirnya amat berguna untuk membuat kod kami lebih mudah dibaca dan untuk menghapus kod lama yang duduk tanpa melakukan apa-apa. Ia mengambil beberapa minggu yang mengecewakan dan menarik rambut untuk sampai ke penghujung, tetapi apabila kita telah menggantikan majoriti rujukan, kita mula melakukan ujian manual.

Ujian & Ujian Manual

Kerana perubahan yang mempengaruhi ciri-ciri seluruh seluruh basis kod, beberapa yang tidak diuji, sukar untuk QA dengan kepastian, tetapi kami melakukan yang terbaik. Kami melakukan ujian manual pada pelayan QA kami, yang menangkap banyak kes bug dan kelebihan. Dan kemudian kami pergi ke hadapan dan untuk laluan yang lebih kritikal, menulis ujian baru.

Roll Out, Go Live, & Clean Up

Selepas lulus QA, kami membalik bendera ciri kami dan biarkan sistem menyelesaikan. Selepas kami yakin ia stabil, kami memadamkan bendera ciri dan laluan kod lama dari pangkalan data. Ini, sayangnya, adalah lebih sukar daripada yang dijangkakan kerana ia memerlukan penulisan ulang banyak suite ujian, kebanyakannya kilang-kilang yang secara implisit bergantung pada model Kandungan. Dalam peninjauan semula, apa yang kita boleh lakukan ialah menulis dua set ujian semasa kami sedang refactoring, satu untuk kod semasa dan satu untuk kod di sebalik bendera ciri.

Sebagai langkah terakhir, yang masih akan datang, kami perlu menyandarkan data dan menurunkan jadual kami yang tidak digunakan.

Dan itu, kawan-kawan, adalah satu cara anda menyingkirkan Warisan Jadual Tunggal yang luas dalam monolit Rel anda. Mungkin kajian kes ini akan membantu anda juga.

Adakah anda mempunyai cara lain untuk mengeluarkan STI atau refactoring? Kami ingin tahu. Marilah kita tahu dalam komen.

Juga, kami menyewa! Sertai pasukan kami. Kami sejuk, saya berjanji.

Sumber dan Pembacaan Tambahan

  • Warisan Panduan Kereta Api
  • Bagaimana dan Bilakah Menggunakan Warisan Jadual Tunggal di Laluan oleh Eugene Wang (Gred Flatiron!)
  • Refactoring Our Rails App Out of Single-Table Warherance
  • Tabir Anugerah vs Persatuan Polymorphic dalam Rails
  • Tabung Warisan Menggunakan Rails 5.02

Untuk mengetahui lebih lanjut mengenai Flatiron School, lawati laman web, ikuti kami di Facebook dan Twitter, dan lawati kami pada acara yang akan datang berhampiran anda.

Flatiron School adalah anggota bangsawan keluarga WeWork. Lihat blog teknologi saudara kami WeWork Technology dan Membuat Meetup.