Migrasi Basis Data: Mengubah Ulat Menjadi Kupu-Kupu

Diterbitkan: 2022-03-11

Pengguna tidak peduli apa yang ada di dalam perangkat lunak yang mereka gunakan; hanya saja ia bekerja dengan lancar, aman, dan tidak mencolok. Pengembang berusaha keras untuk mewujudkannya, dan salah satu masalah yang mereka coba pecahkan adalah bagaimana memastikan bahwa penyimpanan data dalam keadaan yang sesuai untuk versi produk saat ini. Perangkat lunak berkembang, dan model datanya juga dapat berubah seiring waktu, misalnya, untuk memperbaiki kesalahan desain. Untuk memperumit masalah lebih lanjut, Anda mungkin memiliki sejumlah lingkungan pengujian atau pelanggan yang bermigrasi ke versi produk yang lebih baru dengan kecepatan yang berbeda. Anda tidak bisa hanya mendokumentasikan struktur toko dan manipulasi apa yang diperlukan untuk menggunakan versi baru yang mengkilap dari satu perspektif.

Migrasi Basis Data: Mengubah Ulat Menjadi Kupu-Kupu

Saya pernah bergabung dengan proyek dengan beberapa database dengan struktur yang diperbarui sesuai permintaan, langsung oleh pengembang. Ini berarti bahwa tidak ada cara yang jelas untuk mengetahui perubahan apa yang perlu diterapkan untuk memigrasikan struktur ke versi terbaru dan tidak ada konsep pembuatan versi sama sekali! Ini terjadi selama era pra-DevOps dan akan dianggap sebagai kekacauan total saat ini. Kami memutuskan untuk mengembangkan alat yang akan digunakan untuk menerapkan setiap perubahan ke database yang diberikan. Itu memiliki migrasi dan akan mendokumentasikan perubahan skema. Ini membuat kami yakin bahwa tidak akan ada perubahan yang tidak disengaja dan status skema dapat diprediksi.

Pada artikel ini, kita akan melihat bagaimana menerapkan migrasi skema database relasional dan bagaimana mengatasi masalah yang menyertainya.

Pertama-tama, apa itu migrasi basis data? Dalam konteks artikel ini, migrasi adalah kumpulan perubahan yang harus diterapkan ke database. Membuat atau menghapus tabel, kolom, atau indeks adalah contoh umum dari migrasi. Bentuk skema Anda dapat berubah secara dramatis dari waktu ke waktu, terutama jika pengembangan dimulai ketika persyaratan masih belum jelas. Jadi, selama beberapa pencapaian dalam perjalanan menuju rilis, model data Anda akan berevolusi dan mungkin menjadi sangat berbeda dari awalnya. Migrasi hanyalah langkah ke status target.

Untuk memulai, mari jelajahi apa yang kita miliki di kotak peralatan kita untuk menghindari menemukan kembali apa yang sudah dilakukan dengan baik.

Peralatan

Di setiap bahasa yang banyak digunakan, ada perpustakaan yang membantu memudahkan migrasi basis data. Misalnya, dalam kasus Java, opsi populer adalah Liquibase dan Flyway. Kami akan lebih banyak menggunakan Liquibase dalam contoh, tetapi konsepnya berlaku untuk solusi lain dan tidak terikat dengan Liquibase.

Mengapa repot-repot menggunakan pustaka migrasi skema terpisah jika beberapa ORM sudah menyediakan opsi untuk memutakhirkan skema secara otomatis dan membuatnya cocok dengan struktur kelas yang dipetakan? Dalam praktiknya, migrasi otomatis seperti itu hanya melakukan perubahan skema sederhana, misalnya membuat tabel dan kolom, dan tidak dapat melakukan hal-hal yang berpotensi merusak seperti menjatuhkan atau mengganti nama objek database. Jadi solusi non-otomatis (tetapi masih otomatis) biasanya merupakan pilihan yang lebih baik karena Anda dipaksa untuk menjelaskan sendiri logika migrasi, dan Anda tahu apa yang sebenarnya akan terjadi pada database Anda.

Ini juga merupakan ide yang sangat buruk untuk mencampur modifikasi skema otomatis dan manual karena Anda dapat menghasilkan skema yang unik dan tidak terduga jika perubahan manual diterapkan dalam urutan yang salah atau tidak diterapkan sama sekali, bahkan jika diperlukan. Setelah alat dipilih, gunakan untuk menerapkan semua migrasi skema.

Migrasi Basis Data Umum

Migrasi khas termasuk membuat urutan, tabel, kolom, kunci utama dan asing, indeks, dan objek database lainnya. Untuk jenis perubahan yang paling umum, Liquibase menyediakan elemen deklaratif yang berbeda untuk menjelaskan apa yang harus dilakukan. Akan terlalu membosankan untuk membaca tentang setiap perubahan sepele yang didukung oleh Liquibase atau alat serupa lainnya. Untuk mendapatkan gambaran tentang tampilan set perubahan, pertimbangkan contoh berikut di mana kita membuat tabel (deklarasi namespace XML dihilangkan untuk singkatnya):

 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog> <changeSet author="demo"> <createTable tableName="PRODUCT"> <column name="ID" type="BIGINT"> <constraints primaryKey="true" primaryKeyName="PK_PRODUCT"/> </column> <column name="CODE" type="VARCHAR(50)"> <constraints nullable="false" unique="true" uniqueConstraintName="UC_PRODUCT_CODE"/> </column> </createTable> </changeSet> </databaseChangeLog>

Seperti yang Anda lihat, changelog adalah kumpulan kumpulan perubahan, dan kumpulan perubahan terdiri dari perubahan. Perubahan sederhana seperti createTable dapat digabungkan untuk mengimplementasikan migrasi yang lebih kompleks; misalnya, Anda perlu memperbarui kode produk untuk semua produk. Itu dapat dengan mudah dicapai dengan perubahan berikut:

 <sql>UPDATE product SET code = 'new_' || code</sql>

Performa akan menurun jika Anda memiliki jutaan produk. Untuk mempercepat migrasi, kita dapat menulis ulang menjadi langkah-langkah berikut:

  1. Buat tabel baru untuk produk dengan createTable , seperti yang kita lihat sebelumnya. Pada tahap ini, lebih baik membuat batasan sesedikit mungkin. Beri nama tabel baru PRODUCT_TMP .
  2. Isi PRODUCT_TMP dengan SQL berupa INSERT INTO ... SELECT ... menggunakan sql change.
  3. Buat semua batasan ( addNotNullConstraint , addUniqueConstraint , addForeignKeyConstraint ) dan indeks ( createIndex ) yang Anda butuhkan.
  4. Ganti nama tabel PRODUCT menjadi sesuatu seperti PRODUCT_BAK . Liquibase dapat melakukannya dengan renameTable .
  5. Ganti nama PRODUCT_TMP menjadi PRODUCT (sekali lagi, menggunakan renameTable ).
  6. Secara opsional, hapus PRODUCT_BAK dengan dropTable .

Tentu saja, lebih baik untuk menghindari migrasi seperti itu, tetapi ada baiknya untuk mengetahui bagaimana menerapkannya jika Anda mengalami salah satu kasus langka di mana Anda membutuhkannya.

Jika Anda menganggap XML, JSON, atau YAML terlalu aneh untuk tugas menjelaskan perubahan, maka gunakan saja SQL biasa dan manfaatkan semua fitur khusus vendor basis data. Selain itu, Anda dapat menerapkan logika khusus apa pun di Java biasa.

Cara Liquibase membebaskan Anda dari menulis SQL spesifik database yang sebenarnya dapat menyebabkan terlalu percaya diri, tetapi Anda tidak boleh melupakan kebiasaan database target Anda; misalnya, saat Anda membuat kunci asing, indeks mungkin dibuat atau tidak, tergantung pada sistem manajemen basis data tertentu yang digunakan. Akibatnya, Anda mungkin menemukan diri Anda dalam situasi yang canggung. Liquibase memungkinkan Anda untuk menentukan bahwa changeset harus dijalankan hanya untuk tipe database tertentu, misalnya, PostgreSQL, Oracle, atau MySQL. Ini memungkinkannya menggunakan rangkaian perubahan agnostik vendor yang sama untuk database yang berbeda, dan untuk rangkaian perubahan lainnya, menggunakan sintaks dan fitur khusus vendor. Changeset berikut akan dieksekusi hanya jika menggunakan database Oracle:

 <changeSet dbms="oracle" author="..."> ... </changeSet>

Selain Oracle, Liquibase mendukung beberapa database lain di luar kotak.

Penamaan Objek Database

Setiap objek database yang Anda buat perlu diberi nama. Anda tidak diharuskan untuk secara eksplisit memberikan nama untuk beberapa jenis objek, misalnya untuk batasan dan indeks. Tapi itu tidak berarti bahwa benda-benda itu tidak akan memiliki nama; nama mereka akan dihasilkan oleh database pula. Masalah muncul ketika Anda perlu mereferensikan objek itu untuk menjatuhkan atau mengubahnya. Jadi lebih baik memberi mereka nama eksplisit. Tetapi apakah ada aturan tentang nama apa yang harus diberikan? Jawabannya singkat: Konsisten; misalnya, jika Anda memutuskan untuk memberi nama indeks seperti ini: IDX_<table>_<columns> , maka indeks untuk kolom CODE yang disebutkan di atas harus diberi nama IDX_PRODUCT_CODE .

Konvensi penamaan sangat kontroversial, jadi kami tidak akan memberikan instruksi yang komprehensif di sini. Konsisten, hormati tim atau konvensi proyek Anda, atau ciptakan saja jika tidak ada.

Mengatur Perubahan

Hal pertama yang harus diputuskan adalah di mana menyimpan set perubahan. Pada dasarnya ada dua pendekatan:

  1. Simpan perubahan dengan kode aplikasi. Lebih mudah untuk melakukannya karena Anda dapat melakukan dan meninjau perubahan dan kode aplikasi bersama-sama.
  2. Pisahkan set perubahan dan kode aplikasi , misalnya di repositori VCS yang terpisah. Pendekatan ini cocok ketika model data dibagikan di beberapa aplikasi dan lebih mudah untuk menyimpan semua perubahan dalam repositori khusus dan tidak menyebarkannya ke beberapa repositori tempat kode aplikasi berada.

Di mana pun Anda menyimpan set perubahan, biasanya masuk akal untuk membaginya ke dalam kategori berikut:

  1. Migrasi independen yang tidak memengaruhi sistem yang sedang berjalan. Biasanya aman untuk membuat tabel baru, urutan, dll, jika aplikasi yang saat ini digunakan belum mengetahuinya.
  2. Modifikasi skema yang mengubah struktur toko , misalnya, menambah atau menghapus kolom dan indeks. Perubahan ini tidak boleh diterapkan saat versi aplikasi yang lebih lama masih digunakan karena hal itu dapat menyebabkan penguncian atau perilaku aneh karena perubahan skema.
  3. Migrasi cepat yang menyisipkan atau memperbarui sejumlah kecil data. Jika beberapa aplikasi sedang digunakan, perubahan dari kategori ini dapat dijalankan secara bersamaan tanpa menurunkan kinerja database.
  4. Migrasi yang berpotensi lambat yang menyisipkan atau memperbarui banyak data. Perubahan ini lebih baik diterapkan saat tidak ada migrasi serupa lainnya yang dijalankan.

representasi grafis dari empat kategori

Kumpulan migrasi ini harus dijalankan secara berurutan sebelum menerapkan versi aplikasi yang lebih baru. Pendekatan ini menjadi lebih praktis jika suatu sistem terdiri dari beberapa aplikasi terpisah dan beberapa di antaranya menggunakan database yang sama. Jika tidak, sebaiknya pisahkan hanya kumpulan perubahan yang dapat diterapkan tanpa memengaruhi aplikasi yang sedang berjalan, dan kumpulan perubahan lainnya dapat diterapkan bersama-sama.

Untuk aplikasi yang lebih sederhana, set lengkap migrasi yang diperlukan dapat diterapkan saat startup aplikasi. Dalam hal ini, semua set perubahan termasuk dalam satu kategori dan dijalankan setiap kali aplikasi diinisialisasi.

Tahap apa pun yang dipilih untuk menerapkan migrasi, perlu disebutkan bahwa menggunakan database yang sama untuk beberapa aplikasi dapat menyebabkan penguncian saat migrasi diterapkan. Liquibase (seperti banyak solusi serupa lainnya) menggunakan dua tabel khusus untuk merekam metadatanya: DATABASECHANGELOG dan DATABASECHANGELOGLOCK . Yang pertama digunakan untuk menyimpan informasi tentang perubahan yang diterapkan, dan yang terakhir untuk mencegah migrasi bersamaan dalam skema database yang sama. Jadi, jika beberapa aplikasi harus menggunakan skema database yang sama untuk beberapa alasan, lebih baik menggunakan nama non-default untuk tabel metadata untuk menghindari penguncian.

Sekarang setelah struktur tingkat tinggi jelas, Anda perlu memutuskan bagaimana mengatur perubahan dalam setiap kategori.

contoh organisasi changeset

Ini sangat tergantung pada persyaratan aplikasi tertentu, tetapi poin-poin berikut biasanya masuk akal:

  1. Simpan log perubahan yang dikelompokkan berdasarkan rilis produk Anda. Buat direktori baru untuk setiap rilis dan tempatkan file changelog yang sesuai ke dalamnya. Miliki changelog root dan sertakan changelog yang sesuai dengan rilis. Dalam changelog rilis, sertakan changelog lain yang terdiri dari rilis ini.
  2. Miliki konvensi penamaan untuk file changelog dan pengidentifikasi changeset—dan ikuti, tentu saja.
  3. Hindari changeset dengan banyak perubahan. Lebih suka beberapa changeset daripada satu changeset panjang.
  4. Jika Anda menggunakan prosedur tersimpan dan perlu memperbaruinya, pertimbangkan untuk menggunakan runOnChange="true" dari set perubahan tempat prosedur tersimpan tersebut ditambahkan. Jika tidak, setiap kali diperbarui, Anda harus membuat set perubahan baru dengan versi baru prosedur tersimpan. Persyaratan bervariasi, tetapi sering kali dapat diterima untuk tidak melacak riwayat tersebut.
  5. Pertimbangkan untuk menekan perubahan yang berlebihan sebelum menggabungkan cabang fitur. Kadang-kadang, terjadi bahwa di cabang fitur (terutama di cabang yang berumur panjang) perubahan selanjutnya memperbaiki perubahan yang dibuat pada perubahan sebelumnya. Misalnya, Anda dapat membuat tabel dan kemudian memutuskan untuk menambahkan lebih banyak kolom ke dalamnya. Sebaiknya tambahkan kolom tersebut ke perubahan createTable awal jika cabang fitur ini belum digabungkan ke cabang utama.
  6. Gunakan log perubahan yang sama untuk membuat database pengujian. Jika Anda mencoba melakukannya, Anda mungkin segera mengetahui bahwa tidak setiap perubahan dapat diterapkan ke lingkungan pengujian, atau bahwa perubahan tambahan diperlukan untuk lingkungan pengujian tertentu. Dengan Liquibase, masalah ini mudah diselesaikan menggunakan contexts . Cukup tambahkan atribut context="test" ke set perubahan yang perlu dijalankan hanya dengan pengujian, lalu inisialisasi Liquibase dengan konteks test diaktifkan.

Berputar Kembali

Seperti solusi serupa lainnya, Liquibase mendukung migrasi skema "naik" dan "turun." Namun berhati-hatilah: Membatalkan migrasi mungkin tidak mudah, dan tidak selalu sepadan dengan usaha. Jika Anda memutuskan untuk mendukung pembatalan migrasi untuk aplikasi Anda, maka konsistenlah dan lakukan untuk setiap perubahan yang perlu dibatalkan. Dengan Liquibase, membatalkan set perubahan dilakukan dengan menambahkan tag rollback yang berisi perubahan yang diperlukan untuk melakukan rollback. Perhatikan contoh berikut:

 <changeSet author="..."> <createTable tableName="PRODUCT"> <column name="ID" type="BIGINT"> <constraints primaryKey="true" primaryKeyName="PK_PRODUCT"/> </column> <column name="CODE" type="VARCHAR(50)"> <constraints nullable="false" unique="true" uniqueConstraintName="UC_PRODUCT_CODE"/> </column> </createTable> <rollback> <dropTable tableName="PRODUCT"/> </rollback> </changeSet>

Rollback eksplisit berlebihan di sini karena Liquibase akan melakukan tindakan rollback yang sama. Liquibase dapat secara otomatis mengembalikan sebagian besar jenis perubahan yang didukungnya, misalnya createTable , addColumn , atau createIndex .

Memperbaiki Masa Lalu

Tidak ada orang yang sempurna, dan kita semua membuat kesalahan. Beberapa dari mereka mungkin terlambat ditemukan ketika perubahan yang rusak telah diterapkan. Mari kita jelajahi apa yang bisa dilakukan untuk menyelamatkan hari ini.

Perbarui Basis Data Secara Manual

Ini melibatkan mengotak-atik DATABASECHANGELOG dan database Anda dengan cara berikut:

  1. Jika Anda ingin memperbaiki set perubahan yang buruk dan menjalankannya lagi:
    • Hapus baris dari DATABASECHANGELOG yang sesuai dengan set perubahan.
    • Hapus semua efek samping yang diperkenalkan oleh perubahan; misalnya, mengembalikan tabel jika dijatuhkan.
    • Perbaiki perubahan yang buruk.
    • Jalankan migrasi lagi.
  2. Jika Anda ingin memperbaiki set perubahan yang buruk tetapi lewati penerapannya lagi:
    • Perbarui DATABASECHANGELOG dengan menyetel nilai bidang MD5SUM ke NULL untuk baris yang sesuai dengan kumpulan perubahan yang buruk.
    • Perbaiki secara manual apa yang salah dalam database. Misalnya, jika ada kolom yang ditambahkan dengan jenis yang salah, maka buat kueri untuk mengubah jenisnya.
    • Perbaiki perubahan yang buruk.
    • Jalankan migrasi lagi. Liquibase akan menghitung checksum baru dan menyimpannya ke MD5SUM . Kumpulan perubahan yang dikoreksi tidak akan dijalankan lagi.

Jelas, mudah untuk melakukan trik ini selama pengembangan, tetapi akan jauh lebih sulit jika perubahan diterapkan ke banyak basis data.

Tulis Perubahan Korektif

Dalam praktiknya, pendekatan ini biasanya lebih tepat. Anda mungkin bertanya-tanya, mengapa tidak mengedit saja changeset asli? Yang benar adalah bahwa itu tergantung pada apa yang perlu diubah. Liquibase menghitung checksum untuk setiap set perubahan dan menolak untuk menerapkan perubahan baru jika checksum baru untuk setidaknya satu set perubahan yang diterapkan sebelumnya. Perilaku ini dapat dikustomisasi pada basis per-perubahan dengan menentukan runOnChange="true" . Checksum tidak terpengaruh jika Anda mengubah prasyarat atau atribut changeset opsional ( context , runOnChange , dll.).

Sekarang, Anda mungkin bertanya-tanya, bagaimana Anda akhirnya memperbaiki perubahan yang salah?

  1. Jika Anda ingin perubahan tersebut tetap diterapkan untuk skema baru, cukup tambahkan rangkaian perubahan korektif. Misalnya, jika ada kolom yang ditambahkan dengan jenis yang salah, maka ubah jenisnya di changeset baru.
  2. Jika Anda ingin berpura-pura bahwa perubahan buruk itu tidak pernah ada, lakukan hal berikut:
    • Hapus kumpulan perubahan atau tambahkan atribut context dengan nilai yang menjamin Anda tidak akan pernah mencoba menerapkan migrasi dengan konteks seperti itu lagi, misalnya, context="graveyard-changesets-never-run" .
    • Tambahkan set perubahan baru yang akan mengembalikan kesalahan atau memperbaikinya. Perubahan ini harus diterapkan hanya jika perubahan buruk diterapkan. Itu dapat dicapai dengan prasyarat, seperti dengan changeSetExecuted . Jangan lupa untuk menambahkan komentar yang menjelaskan mengapa Anda melakukannya.
    • Tambahkan set perubahan baru yang memodifikasi skema dengan cara yang benar.

Seperti yang Anda lihat, memperbaiki masa lalu adalah mungkin, meskipun mungkin tidak selalu mudah.

Mengurangi Rasa Sakit yang Tumbuh

Seiring bertambahnya usia aplikasi Anda, log perubahannya juga bertambah, mengumpulkan setiap perubahan skema di sepanjang jalurnya. Ini berdasarkan desain, dan tidak ada yang salah dengan ini. Log perubahan yang panjang dapat dibuat lebih pendek dengan secara teratur menekan migrasi, misalnya, setelah merilis setiap versi produk. Dalam beberapa kasus, ini akan membuat inisialisasi skema baru lebih cepat.

ilustrasi changelogs yang tergencet

Squashing tidak selalu sepele dan dapat menyebabkan regresi tanpa membawa banyak manfaat. Pilihan bagus lainnya adalah menggunakan database seed untuk menghindari mengeksekusi semua set perubahan. Ini sangat cocok untuk lingkungan pengujian jika Anda perlu memiliki database yang siap secepat mungkin, bahkan mungkin dengan beberapa data pengujian. Anda mungkin menganggapnya sebagai bentuk pemerasan untuk perubahan: Pada titik tertentu (misalnya, setelah merilis versi lain), Anda membuat dump skema. Setelah memulihkan dump, Anda menerapkan migrasi seperti biasa. Hanya perubahan baru yang akan diterapkan karena yang lama sudah diterapkan sebelum membuat dump; oleh karena itu, mereka dipulihkan dari tempat pembuangan.

ilustrasi database benih

Kesimpulan

Kami sengaja menghindari menyelam lebih dalam di fitur Liquibase untuk menyampaikan artikel yang singkat dan to the point, berfokus pada pengembangan skema secara umum. Mudah-mudahan, jelas manfaat dan masalah apa yang ditimbulkan oleh aplikasi otomatis migrasi skema database dan seberapa cocok semuanya dengan budaya DevOps. Sangat penting untuk tidak mengubah bahkan ide yang bagus menjadi dogma. Persyaratan bervariasi, dan sebagai insinyur basis data, keputusan kami harus mendorong kemajuan produk dan tidak hanya mengikuti rekomendasi dari seseorang di internet.