Veritabanı Taşımaları: Tırtılları Kelebeklere Dönüştürmek
Yayınlanan: 2022-03-11Kullanıcılar kullandıkları yazılımın içinde ne olduğu ile ilgilenmezler; sadece sorunsuz, güvenli ve göze batmayan bir şekilde çalışmasını sağlar. Geliştiriciler bunu gerçekleştirmek için çabalıyor ve çözmeye çalıştıkları sorunlardan biri de veri deposunun ürünün mevcut sürümüne uygun bir durumda olmasını nasıl sağlayacakları. Yazılım gelişir ve örneğin tasarım hatalarını düzeltmek için veri modeli de zamanla değişebilir. Sorunu daha da karmaşık hale getirmek için, birkaç test ortamınız veya ürünün daha yeni sürümlerine farklı hızlarda geçiş yapan müşterileriniz olabilir. Mağazanın yapısını ve parlak yeni sürümü tek bir bakış açısıyla kullanmak için hangi manipülasyonların gerekli olduğunu belgeleyemezsiniz.
Bir keresinde, doğrudan geliştiriciler tarafından talep üzerine güncellenen yapılara sahip birkaç veritabanına sahip bir projeye katıldım. Bu, yapıyı en son sürüme geçirmek için hangi değişikliklerin uygulanması gerektiğini bulmanın açık bir yolu olmadığı ve sürüm oluşturma kavramının olmadığı anlamına geliyordu! Bu, DevOps öncesi dönemdeydi ve günümüzde tam bir karmaşa olarak kabul edilecekti. Verilen veritabanına her değişikliği uygulamak için kullanılacak bir araç geliştirmeye karar verdik. Göçleri vardı ve şema değişikliklerini belgeleyecekti. Bu, tesadüfi bir değişiklik olmayacağından ve şema durumunun tahmin edilebilir olacağından emin olmamızı sağladı.
Bu makalede, ilişkisel veritabanı şeması geçişlerinin nasıl uygulanacağına ve eşlik eden sorunların nasıl üstesinden gelineceğine bakacağız.
Her şeyden önce, veritabanı geçişleri nedir? Bu makale bağlamında geçiş , bir veritabanına uygulanması gereken bir dizi değişikliktir. Bir tablo, sütun veya dizin oluşturmak veya bırakmak, geçişlerin yaygın örnekleridir. Şemanızın şekli, özellikle gereksinimler hala belirsizken geliştirme başlatıldıysa, zamanla önemli ölçüde değişebilir. Bu nedenle, bir sürüme giden yolda birkaç kilometre taşı boyunca, veri modeliniz evrim geçirecek ve en başta olduğundan tamamen farklı hale gelmiş olabilir. Göçler sadece hedef duruma giden adımlardır.
Başlamak için, zaten iyi yapılmış olanı yeniden icat etmekten kaçınmak için araç kutumuzda neler olduğunu keşfedelim.
Araçlar
Yaygın olarak kullanılan her dilde, veritabanı geçişlerini kolaylaştırmaya yardımcı olan kitaplıklar vardır. Örneğin, Java söz konusu olduğunda, popüler seçenekler Liquibase ve Flyway'dir. Örneklerde Liquibase'i daha fazla kullanacağız, ancak kavramlar diğer çözümler için geçerlidir ve Liquibase'e bağlı değildir.
Bazı ORM'ler zaten bir şemayı otomatik olarak yükseltme ve eşlenen sınıfların yapısıyla eşleştirme seçeneği sunuyorsa, neden ayrı bir şema geçiş kitaplığı kullanmakla uğraşasınız ki? Pratikte, bu tür otomatik geçişler, örneğin tablolar ve sütunlar oluşturmak gibi yalnızca basit şema değişiklikleri yapar ve veritabanı nesnelerini bırakmak veya yeniden adlandırmak gibi potansiyel olarak yıkıcı şeyler yapamaz. Bu nedenle, otomatik olmayan (ancak yine de otomatikleştirilmiş) çözümler genellikle daha iyi bir seçimdir çünkü geçiş mantığını kendiniz tanımlamak zorunda kalırsınız ve veritabanınıza tam olarak ne olacağını bilirsiniz.
Otomatik ve manuel şema değişikliklerini karıştırmak da çok kötü bir fikirdir, çünkü manuel değişiklikler yanlış sırada uygulanırsa veya hiç uygulanmazsa, gerekli olsa bile benzersiz ve öngörülemeyen şemalar üretebilirsiniz. Araç seçildikten sonra, tüm şema geçişlerini uygulamak için kullanın.
Tipik Veritabanı Taşımaları
Tipik geçişler, diziler, tablolar, sütunlar, birincil ve yabancı anahtarlar, dizinler ve diğer veritabanı nesneleri oluşturmayı içerir. En yaygın değişiklik türleri için Liquibase, ne yapılması gerektiğini açıklamak için farklı bildirim öğeleri sağlar. Liquibase veya diğer benzer araçlar tarafından desteklenen her önemsiz değişikliği okumak çok sıkıcı olurdu. Değişiklik kümelerinin nasıl göründüğü hakkında bir fikir edinmek için, bir tablo oluşturduğumuz aşağıdaki örneği göz önünde bulundurun (kısa olması için XML ad alanı bildirimleri atlanmıştır):
<?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> Gördüğünüz gibi, değişiklik günlüğü bir dizi değişiklik kümesidir ve değişiklik kümeleri değişikliklerden oluşur. createTable gibi basit değişiklikler, daha karmaşık geçişleri uygulamak için birleştirilebilir; örneğin, tüm ürünler için ürün kodunu güncellemeniz gerektiğini varsayalım. Aşağıdaki değişiklikle kolayca elde edilebilir:
<sql>UPDATE product SET code = 'new_' || code</sql>Milyonlarca ürününüz varsa performans düşer. Geçişi hızlandırmak için, aşağıdaki adımlara yeniden yazabiliriz:
- Daha önce gördüğümüz gibi
createTableile ürünler için yeni bir tablo oluşturun. Bu aşamada, mümkün olduğunca az kısıtlama oluşturmak daha iyidir. Yeni tabloyaPRODUCT_TMPadını verelim. -
PRODUCT_TMPsqldeğişikliğini kullanarakINSERT INTO ... SELECT ...biçiminde SQL ile doldurun. - İhtiyacınız olan tüm kısıtlamaları (
addNotNullConstraint,addUniqueConstraint,addForeignKeyConstraint) ve dizinleri (createIndex) oluşturun. -
PRODUCTtablosunuPRODUCT_BAKgibi bir adla yeniden adlandırın. Liquibase bunurenameTableile yapabilir. -
PRODUCT_TMPPRODUCTolarak yeniden adlandırın (yinerenameTablekullanarak). - İsteğe bağlı olarak,
dropTableilePRODUCT_BAKkaldırın.
Tabii ki, bu tür geçişlerden kaçınmak daha iyidir, ancak ihtiyaç duyduğunuz nadir durumlardan biriyle karşılaşmanız durumunda bunları nasıl uygulayacağınızı bilmek iyidir.
Değişiklikleri tanımlama görevi için XML, JSON veya YAML'nin çok tuhaf olduğunu düşünüyorsanız, o zaman sadece düz SQL kullanın ve tüm veritabanı satıcısına özgü özellikleri kullanın. Ayrıca, herhangi bir özel mantığı düz Java'da uygulayabilirsiniz.
Liquibase'in sizi gerçek veritabanına özel SQL yazmaktan muaf tutma şekli aşırı güvene yol açabilir, ancak hedef veritabanınızın tuhaflıklarını unutmamalısınız; örneğin, bir yabancı anahtar oluşturduğunuzda, kullanılan belirli veritabanı yönetim sistemine bağlı olarak bir dizin oluşturulabilir veya oluşturulmayabilir. Sonuç olarak, kendinizi garip bir durumda bulabilirsiniz. Liquibase, bir değişiklik kümesinin yalnızca belirli bir veritabanı türü, örneğin PostgreSQL, Oracle veya MySQL için çalıştırılması gerektiğini belirtmenize olanak tanır. Farklı veritabanları için aynı satıcıdan bağımsız değişiklik kümelerini ve satıcıya özel sözdizimi ve özellikleri kullanarak diğer değişiklik kümelerini kullanarak bunu mümkün kılar. Aşağıdaki değişiklik kümesi yalnızca bir Oracle veritabanı kullanılıyorsa yürütülecektir:
<changeSet dbms="oracle" author="..."> ... </changeSet>Oracle'ın yanı sıra Liquibase, kutudan çıktığı gibi birkaç başka veritabanını da destekler.
Veritabanı Nesnelerini Adlandırma
Oluşturduğunuz her veritabanı nesnesinin adlandırılması gerekir. Bazı nesne türleri için, örneğin kısıtlamalar ve dizinler için açıkça bir ad vermeniz gerekmez. Ancak bu nesnelerin adlarının olmayacağı anlamına gelmez; adları yine de veritabanı tarafından oluşturulacaktır. Sorun, düşürmek veya değiştirmek için o nesneye başvurmanız gerektiğinde ortaya çıkar. Bu yüzden onlara açık isimler vermek daha iyidir. Ama hangi isimlerin verileceği konusunda herhangi bir kural var mı? Cevap kısa: Tutarlı olun; örneğin, dizinleri şöyle adlandırmaya karar verdiyseniz: IDX_<table>_<columns> , o zaman yukarıda bahsedilen CODE sütunu için bir dizin IDX_PRODUCT_CODE olarak adlandırılmalıdır.
Adlandırma kuralları inanılmaz derecede tartışmalıdır, bu nedenle burada kapsamlı talimatlar vermeyi varsaymayacağız. Tutarlı olun, ekibinize veya proje sözleşmelerinize saygı gösterin veya yoksa onları icat edin.
Değişiklik Setlerini Düzenleme
Karar verilecek ilk şey, değişiklik kümelerinin nerede saklanacağıdır. Temel olarak iki yaklaşım vardır:
- Uygulama koduyla değişiklik kümelerini saklayın. Değişiklik kümelerini ve uygulama kodunu birlikte kaydedip gözden geçirebildiğiniz için bunu yapmak uygundur.
- Değişiklik kümelerini ve uygulama kodunu ayrı tutun , örneğin ayrı VCS havuzlarında. Bu yaklaşım, veri modeli birkaç uygulama arasında paylaşıldığında uygundur ve tüm değişiklik setlerini özel bir havuzda depolamak ve bunları uygulama kodunun bulunduğu birden çok havuza dağıtmamak daha uygundur.
Değişiklik kümelerini nerede saklarsanız saklayın, genellikle bunları aşağıdaki kategorilere ayırmanız mantıklıdır:
- Çalışan sistemi etkilemeyen bağımsız geçişler. Şu anda dağıtılan uygulama henüz bunlardan haberdar değilse, yeni tablolar, diziler vb. oluşturmak genellikle güvenlidir.
- Mağazanın yapısını değiştiren şema değişiklikleri , örneğin sütun ve dizin ekleme veya bırakma. Bu değişiklikler, uygulamanın eski bir sürümü hala kullanımdayken uygulanmamalıdır, çünkü bu, şemadaki değişiklikler nedeniyle kilitlenmelere veya garip davranışlara yol açabilir.
- Küçük miktarlarda veri ekleyen veya güncelleyen hızlı geçişler. Birden çok uygulama dağıtılıyorsa, bu kategorideki değişiklik kümeleri, veritabanı performansını düşürmeden eşzamanlı olarak yürütülebilir.
- Çok fazla veri ekleyen veya güncelleyen potansiyel olarak yavaş geçişler. Bu değişikliklerin, başka benzer geçişler yürütülmediğinde uygulanması daha iyidir.

Bu geçiş kümeleri, bir uygulamanın daha yeni bir sürümünü dağıtmadan önce art arda çalıştırılmalıdır. Bir sistem birkaç ayrı uygulamadan oluşuyorsa ve bazıları aynı veritabanını kullanıyorsa, bu yaklaşım daha da pratik hale gelir. Aksi takdirde, yalnızca çalışan uygulamaları etkilemeden uygulanabilecek olan değişiklik kümelerini ayırmaya değer ve kalan değişiklik kümeleri birlikte uygulanabilir.
Daha basit uygulamalar için, uygulama başlangıcında gerekli geçişlerin tamamı uygulanabilir. Bu durumda, tüm değişiklik kümeleri tek bir kategoriye girer ve uygulama her başlatıldığında çalıştırılır.
Geçişleri uygulamak için hangi aşama seçilirse seçilsin, aynı veritabanının birden çok uygulama için kullanılmasının, geçişler uygulanırken kilitlenmelere neden olabileceğini belirtmekte fayda var. Liquibase (diğer birçok benzer çözüm gibi) meta verilerini kaydetmek için iki özel tablo kullanır: DATABASECHANGELOG ve DATABASECHANGELOGLOCK . İlki, uygulanan değişiklik kümeleri hakkında bilgi depolamak için kullanılır ve ikincisi, aynı veritabanı şeması içinde eşzamanlı geçişleri önlemek için kullanılır. Bu nedenle, herhangi bir nedenle birden çok uygulamanın aynı veritabanı şemasını kullanması gerekiyorsa, kilitlenmeleri önlemek için meta veri tabloları için varsayılan olmayan adlar kullanmak daha iyidir.
Artık üst düzey yapı açık olduğuna göre, her kategorideki değişiklik kümelerini nasıl düzenleyeceğinize karar vermeniz gerekiyor.
Büyük ölçüde belirli uygulama gereksinimlerine bağlıdır, ancak aşağıdaki noktalar genellikle makuldür:
- Değişiklik günlüklerini ürününüzün sürümlerine göre gruplandırın. Her sürüm için yeni bir dizin oluşturun ve ilgili değişiklik günlüğü dosyalarını buna yerleştirin. Bir kök değişiklik günlüğüne sahip olun ve sürümlere karşılık gelen değişiklik günlüklerini ekleyin. Sürüm değişiklik günlüklerinde, bu sürümü içeren diğer değişiklik günlüklerini dahil edin.
- Değişiklik günlüğü dosyaları ve değişiklik kümesi tanımlayıcıları için bir adlandırma kuralına sahip olun ve elbette bunu izleyin.
- Çok fazla değişiklik içeren değişiklik kümelerinden kaçının. Tek bir uzun değişiklik kümesine birden çok değişiklik kümesini tercih edin.
- Saklı yordamlar kullanıyorsanız ve bunları güncellemeniz gerekiyorsa, bu saklı yordamın eklendiği değişiklik
runOnChange="true"özniteliğini kullanmayı düşünün. Aksi takdirde, her güncellendiğinde, saklı yordamın yeni bir sürümüyle yeni bir değişiklik seti oluşturmanız gerekir. Gereksinimler değişiklik gösterir, ancak bu tür bir geçmişi izlememek genellikle kabul edilebilir. - Özellik dallarını birleştirmeden önce gereksiz değişiklikleri ezmeyi düşünün. Bazen, bir özellik dalında (özellikle uzun ömürlü olanda) sonraki değişiklik kümelerinin önceki değişiklik kümelerinde yapılan değişiklikleri iyileştirdiği görülür. Örneğin, bir tablo oluşturabilir ve daha sonra ona daha fazla sütun eklemeye karar verebilirsiniz. Bu özellik dalı henüz ana dalla birleştirilmediyse, bu sütunları ilk
createTabledeğişikliğine eklemeye değer. - Bir test veritabanı oluşturmak için aynı değişiklik günlüklerini kullanın. Bunu yapmaya çalışırsanız, her değişiklik kümesinin test ortamı için geçerli olmadığını veya söz konusu belirli test ortamı için ek değişiklik kümelerinin gerekli olduğunu kısa sürede anlayabilirsiniz. Liquibase ile bu sorun, bağlamlar kullanılarak kolayca çözülür. Yalnızca testlerle yürütülmesi gereken değişiklik kümelerine
context="test"niteliğini ekleyin ve ardındantestbağlamı etkinken Liquibase'i başlatın.
geri alma
Diğer benzer çözümler gibi, Liquibase de "yukarı" ve "aşağı" geçiş şemasını destekler. Ancak uyarılmalıdır: Geçişleri geri almak kolay olmayabilir ve her zaman çabaya değmez. Uygulamanız için taşıma işlemlerini geri almayı desteklemeye karar verdiyseniz, tutarlı olun ve geri alınması gereken her değişiklik kümesi için bunu yapın. Liquibase ile, bir değişiklik kümesinin geri alınması, bir geri alma gerçekleştirmek için gerekli değişiklikleri içeren bir rollback etiketi eklenerek gerçekleştirilir. Aşağıdaki örneği göz önünde bulundurun:
<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> Açık geri alma burada gereksizdir çünkü Liquibase aynı geri alma eylemlerini gerçekleştirir. Liquibase, desteklenen değişiklik türlerinin çoğunu otomatik olarak geri alabilir, örneğin createTable , addColumn veya createIndex .
Geçmişi Düzeltmek
Hiç kimse mükemmel değildir ve hepimiz hata yaparız. Bozulmuş değişiklikler zaten uygulandığında bazıları çok geç keşfedilebilir. Günü kurtarmak için neler yapılabileceğini keşfedelim.
Veritabanını El İle Güncelleyin
DATABASECHANGELOG ve veritabanınızla aşağıdaki şekillerde uğraşmayı içerir:
- Kötü değişiklik kümelerini düzeltmek ve bunları yeniden yürütmek istiyorsanız:
-
DATABASECHANGELOGdeğişiklik kümelerine karşılık gelen satırları kaldırın. - Değişiklik kümeleri tarafından sunulan tüm yan etkileri kaldırın; örneğin, düştüyse bir tabloyu geri yükleyin.
- Kötü değişiklik kümelerini düzeltin.
- Taşıma işlemlerini yeniden çalıştırın.
-
- Kötü değişiklik setlerini düzeltmek istiyor ancak bunları tekrar uygulamayı atlamak istiyorsanız:
- Hatalı değişiklik kümelerine karşılık gelen satırlar için
MD5SUMalan değeriniNULLolarak ayarlayarakDATABASECHANGELOGgüncelleyin. - Veritabanında neyin yanlış yapıldığını manuel olarak düzeltin. Örneğin, yanlış türe sahip bir sütun eklenmişse, türünü değiştirmek için bir sorgu yayınlayın.
- Kötü değişiklik kümelerini düzeltin.
- Taşıma işlemlerini yeniden çalıştırın. Liquibase yeni sağlama toplamını hesaplayacak ve
MD5SUM. Düzeltilen değişiklik kümeleri tekrar çalıştırılmayacak.
- Hatalı değişiklik kümelerine karşılık gelen satırlar için
Açıkçası, geliştirme sırasında bu hileleri yapmak kolaydır, ancak değişiklikler birden çok veritabanına uygulanırsa çok daha zorlaşır.
Düzeltici Değişiklik Kümeleri Yaz
Uygulamada, bu yaklaşım genellikle daha uygundur. Merak edebilirsiniz, neden sadece orijinal değişiklik setini düzenlemiyorsunuz? Gerçek şu ki, neyin değiştirilmesi gerektiğine bağlı. Liquibase, her değişiklik kümesi için bir sağlama toplamı hesaplar ve daha önce uygulanan değişiklik kümelerinden en az biri için sağlama toplamı yeniyse yeni değişiklikleri uygulamayı reddeder. Bu davranış, runOnChange="true" özniteliği belirtilerek değişiklik kümesi bazında özelleştirilebilir. Ön koşulları veya isteğe bağlı değişiklik kümesi özniteliklerini ( context , runOnChange vb.) değiştirirseniz sağlama toplamı etkilenmez.
Şimdi, merak ediyor olabilirsiniz, sonunda değişiklik kümelerini hatalarla nasıl düzeltirsiniz?
- Bu değişikliklerin yeni şemalara uygulanmaya devam etmesini istiyorsanız, düzeltici değişiklik kümeleri eklemeniz yeterlidir. Örneğin, yanlış türe sahip bir sütun eklenmişse, yeni değişiklik kümesindeki türünü değiştirin.
- Bu kötü değişiklik kümeleri hiç var olmamış gibi davranmak istiyorsanız, aşağıdakileri yapın:
- Değişiklik kümelerini kaldırın veya bir daha asla böyle bir bağlamla taşıma uygulamaya çalışmayacağınızı garanti eden bir değerle
contextözniteliğini ekleyin, örneğin,context="graveyard-changesets-never-run". - Yanlış yapılanı geri döndürecek veya düzeltecek yeni değişiklik kümeleri ekleyin. Bu değişiklikler yalnızca kötü değişiklikler uygulanmışsa uygulanmalıdır.
changeSetExecutedgibi ön koşullarla elde edilebilir. Bunu neden yaptığınızı açıklayan bir yorum eklemeyi unutmayın. - Şemayı doğru şekilde değiştiren yeni değişiklik kümeleri ekleyin.
- Değişiklik kümelerini kaldırın veya bir daha asla böyle bir bağlamla taşıma uygulamaya çalışmayacağınızı garanti eden bir değerle
Gördüğünüz gibi geçmişi düzeltmek her zaman kolay olmasa da mümkündür.
Büyüyen Ağrıları Azaltmak
Uygulamanız eskidikçe değişiklik günlüğü de büyür ve yol boyunca her şema değişikliğini biriktirir. Bu tasarım gereğidir ve bunda doğal olarak yanlış bir şey yoktur. Örneğin, ürünün her bir sürümünü yayınladıktan sonra, geçişleri düzenli olarak ezerek uzun değişiklik günlükleri kısaltılabilir. Bazı durumlarda, yeni şemanın daha hızlı başlatılmasını sağlar.
Ezmek her zaman önemsiz değildir ve pek çok fayda sağlamadan gerilemelere neden olabilir. Diğer bir harika seçenek de, tüm değişiklik kümelerini yürütmekten kaçınmak için bir tohum veritabanı kullanmaktır. Mümkün olduğunca hızlı bir veritabanına ihtiyacınız varsa, hatta belki bazı test verileriyle bile ortamları test etmek için çok uygundur. Bunu, değişiklik kümelerini ezmenin bir biçimi olarak düşünebilirsiniz: Bir noktada (örneğin, başka bir sürüm yayınladıktan sonra), şemanın bir dökümünü yaparsınız. Dökümü geri yükledikten sonra, taşıma işlemlerini her zamanki gibi uygularsınız. Yalnızca yeni değişiklikler uygulanacaktır, çünkü daha eski olanlar dökümü yapmadan önce uygulanmış; bu nedenle, çöplükten geri yüklendiler.
Çözüm
Genel olarak gelişen şemalara odaklanan kısa ve öz bir makale sunmak için Liquibase'in özelliklerine daha derinden dalmaktan kasten kaçındık. Umarım, veritabanı şeması geçişlerinin otomatik olarak uygulanmasının ne gibi yararlar ve sorunlar getirdiği ve bunların DevOps kültürüne ne kadar iyi uyduğu açıktır. İyi fikirleri bile dogmaya dönüştürmemek önemlidir. Gereksinimler değişiklik gösterir ve veritabanı mühendisleri olarak kararlarımız, yalnızca internetteki birinden gelen önerilere bağlı kalmayı değil, bir ürünü ileriye taşımayı da teşvik etmelidir.
