Bu makalemizde, pg-migrator modülü sayesinde development ve versiyon geçişleri sonrasında müşterilere kurulum yaparken muhtelif sindirim sistemi rahatsızlıklarına karşı bize çok yardımı dokunacak bir tekniği inceleyeceğiz.

Bu makaleyi yazarken, sanki kendi yazdığım npm modülünün reklamını yapıyormuş gibi gözükebilirim ama durum inan ki öyle değil. Bu yöntem, hem versiyonlar arası geçişlerin daha stabil ve sorunsuz olmasına, hem de insani hatalar sebebiyle veri kayıplarının yaşanmamasına yardımcı oluyor.

Yöntemin mucidi ben değilim. Bu yöntemi uygulayabilmek için farklı platformlarda pek çok farklı araç var. Örneğin .Net dünyasındakiler Fluent Migrator'ı iyi biliyorlardır (Eğer bilmiyorlarsa da en kısa zamanda öğrenmelerini tavsiye ederim). Ben sadece, Node üzerinde PostgreSQL ile çalışanlar için db migration işlemini yapacak benzer bir araç hazırladım.

Malum, npm sitesi bizim değil. Bu yüzden açıklamaları İngilizce yazmak zorunda kaldım. Yabancı dili olanlar, bu adresten modülün sayfasına ulaşabilir ve adım adm kullanım talimatını okuyabilirler. Yabancı dili olmayan kardeşlerim için de bu makaleyi hazırladım, haydi başlayalım.
Öncelikle "Bu alete niye ihtiyacımız var ki?" sorusuna cevap arayalım. Yazılım geliştirme süreci bitmek bilmez. Bir yazılım yaparız ve müşteriye kurarız. Aynı ürüne bir ek geliştirme yapıp başka bir müşteriye daha kurarız. Ama senkronizasyonun kaybolmaması için ilk müşteriye de gidip güncelleme yapmak isteriz. Müşteri sayısı arttıkça, bunun yönetimi de tam bir işkenceye dönüşür. Özellikle de veritabanı seviyesinde değişiklikler varsa.

Aslında yukarıdaki sıkıntı için birden fazla müşteriye de ihtiyacımız yok. Tek bir müşteride kurulu olan uygulamayı sürekli geliştiriyorsak, her yeni versiyon geçişinde mevcut Prod. veritabanını da yenisine migrate etmek zorundayız demektir. Bu iş genelde gece, uykusuz bir vakitte olur ve ufak tefek gözden kaçan detaylar birkaç gün sonra ortaya çıkan hesap uyuşmazlıkları sebebiyle fark edilir.

Belirli bir olgunluğa kısmen ulaşmış yazılım geliştirme firmalarında bunun üstesinden nasıl geliyoruz?

Üzerinde geliştirme yapılan Development veritabanı haricinde, kurulum yapılan her müşterinin Production veritabanının şemasal bir kopyası da geliştirme sunucusunda bulunur. Müşteriye yeni versiyon çıkılacağı zaman SQL Compare gibi ürünler vasıtasıyla, iki veritabanı arasındaki fark Script'i çıkartılır. Ardından kurulum zamanı bu Script müşteri veritabanında çalıştırılarak, geliştirme veritabanının seviyesine gelmesi sağlanır.

Peki bu yöntemde yanlış olan nedir? Öncelikle Production veritabanının sadece şemasal kopyası geliştirme sunucusunda bulunur. Yani müşteride bulunan veriler, bu veritabanında bulunmaz/bulunamaz. Bu yüzden, çıkartılan fark Script'inin müşterideki veriler ile sıkıntı yaşayıp yaşamayacağını, çalıştırma zamanına kadar bilemeyiz. Genellikle de sorun çıkartan birkaç istisnai kayıt olur ve bunun farkına kurulum gecesi varırız.

Genel olarak geliştirmeyi yapan kişiler ile fark Script'ini çıkartan ve çalıştıran kişiler farklıdır. Diyelim ki yazılımı geliştiren kişi, var olan bir tabloya NULL olamayan yeni bir sütun ekledi. Bunu yaparken bir sorunla karşılaşmadı, çünkü geliştirme veritabanındaki tabloda hiç kayıt yoktu. Ardından fark Script'i çıkartıldı ve kuruluma gidildi. Gel gelelim kurulum sırasında fark edildi ki, müşteri ortamındaki bu tabloda veri var. Eğer ki geliştirmeyi yapan kişi, bu yeni sütun için default değer vermediyse (ki bu genelde önerilmeyen bir yöntemdir) Script çalışmayacak, ayrıca Script'i çalıştıracak kişinin bu sütunda nasıl bir veri bulunması gerektiği konusunda da hiçbir fikri olmayacaktır.

Gelelim, belirli bir olgunluğa yeterince ulaşmış yazılım geliştirme firmalarında bunun üstesinden nasıl geldiğimize.

Bu tarz firmalarda, veritabanı üzerinde yapılan her türlü değişikliğin Script'leri bir yerde, belirli bir düzen içerisinde biriktirilir. Bu Script'ler içerisinde sadece şemasal değişiklikler değil, aynı zamanda ihtiyaç duyulan veri manipülasyonlarına dair komutlar da bulunur.

Not : Bu Script'ler nasıl çıkartılır diye kara kara düşünmene gerek yok. Kullandığın grafik arayüz ile istediğin değişiklikleri yaparsın. Ardından bu değişikliklerin Script'ini bu arayüzden çıkartmasını isteyebilirsin. Hemen her arayüz bu yeteneğe sahiptir. Örneğin pgAdmin'de değişiklik yaptığımız pencerenin en sağ tarafındaki "SQL" başlıklı sekmesinde, yapılan değişikliklere karşılık gelen Script bulunur.

Ardından tüm bu Script'ler, tercihen bir DB Migration aracı vasıtasıyla müşteri veritabanında işletilir. Örneğin bu tarz Script'ler içerisinde var olan bir tabloya yeni bir sütun eklemek için, öncelikle NULL değere izin veren bir sütun ekleme komutu, ardından tablonun bu yeni eklenen sütununu güncelleme komutu, son olarak da sütunun NULL değere izin vermemesine ilişkin değiştirme komutu bulunur. Böylece, DB Migration aracını çalıştıran kişi bizzat müşterinin kendisi dahi olsa, değişiklikler hakkında hiçbir fikri olmamasına karşın bir problemle karşılaşmaz.

Not : Bu fark Script'leri elbette elle de çalıştırılabilir ancak bu durumda, müşteri veritabanı ile geliştirme veritabanı arasındaki farkın ne kadar olduğunu bilmek ve ona göre uygun Script'leri seçerek uygun sırasıyla çalıştırmak gerekir. Olası bir geri alma senaryosunda da, bunların geri dönüş Script'lerini bulmak ve yine uygun sırasıyla çalıştırmak gerekir. Oysa DB Migration araçları, her iki veritabanının da versiyonlarını bilir, aradaki farkı otomatik olarak hesaplar ve ilgili Script'leri Transactional olarak çalıştırır. Hatta "Veritabanını şu versiyona yükselt/düşür" dediğimiz zaman da, gerekli Script'leri kendisi bulur ve kullanıcıya iş bırakmaz.

Ben bu makaleyi yazarken, .Net dünyasında en bilinen araç Fluent Migrator'dı. pg-migrator ise, PostgreSQL veritabanları için Node üzerinde kullanılabilecek bir DB Migration aracı. Ben bu makaleyi yazarken güncel sürümü 1.0.2 versiyonuydu. Bu versiyonun genel özelliklerini aşağıdaki gibi sıralayabiliriz.
  • Veritabanını sıfırdan son versiyona otomatik yükseltme
  • İleri ve geri yönlü adım adım Migration
  • Belirtilen bir versiyona tek adımda Migration
  • Hiyerarşik klasörlenmiş fark Script'lerini bulma ve çalıştırma desteği
  • Transactional Migration (Herhangi bir adımda başarısızlık durumunda otomatik toplu Roll Back)
  • Uzaktan Migration (Veritabanı sunucusuna uzak masaüstü bağlantısı gerektirmez)

Sıfırdan adım adım kullanımını anlatacağım ancak öncesinde bilinmesi gereken birkaç nokta var.

DB Migration araçları çift yönlü çalışır. Yani hem versiyon yükseltme, hem de versiyon düşürme yetenekleri vardır. Bu yüzden, her bir işleme ait iki adet Script gereksinimi vardır. Bunlardan bir tanesi değişikliği yapma, diğeri de geri alma Script'idir. Örneğin yükseltme Script'i yeni bir tablo ekliyorsa, geri alma Script'i bu tabloyu siler. Yükseltme Script'i var olan bir tabloya yeni bir sütun ekliyorsa, geri alma Script'i bu sütunu siler vs.

Birazdan göreceğimiz gibi pg-migrator, çalıştırıldığı klasör ve tüm alt klasörlerini tarayarak "x-y.sql" formatında isimlendirilmiş dosyalar arar. Burada x ve y'nin bitişik sayılar olması şartı vardır. Örneğin "2-3.sql" ve "3-2.sql" gibi. Bu formatın haricinde isimlendirilmiş tüm dosyalar yok farz edilir.

Burada "x" şu andaki versiyonu, "y" ise hedef versiyonu simgeler. Örneğin "2-3.sql" dosyası, 2. versiyondan 3. versiyona geçiş için gerekli olan Script'i, "3-2.sql" dosyası ise 3. versiyondan 2. versiyona geri dönüş için gerekli olan Script'i içerir.


Adım Adım Gidelim

Şimdiye kadar anlattıklarım biraz karışık ve anlaşılması zor gelmiş olabilir. En güzeli, sıfırdan adım adım örneklendirerek gitmek.

Örneğe başlayabilmek için boş bir veritabanına ihtiyacımız olacak. Ben "testdb" adında boş bir veritabanı oluşturdum. Şu anki görüntüsü aşağıdaki gibi olmalı (Ki buna versiyon 1 diyeceğiz).




Diyelim ki, uygulamamızı geliştirmeye başlıyoruz. İlk olarak "user" ve "user_login_history" adında iki adet tabloya ihtiyacımız var. Bunları oluşturan ve test amaçlı birkaç data insert eden Script'imizi aşağıdaki gibi hazırlayıp, "1-2.sql" ismi ile kaydedelim.

/*** Add user and user_login_history tables and insert some data ***/

CREATE TABLE "user"
(
   id serial,
   username character(20)  NOT NULL,
   name_surname character(50) NOT NULL,
   CONSTRAINT pk_user PRIMARY KEY (id),
   CONSTRAINT uk_user UNIQUE (username)
)
WITH (
  OIDS = FALSE
);

CREATE TABLE "user_login_history"
(
   id serial,
   user_id integer NOT NULL,
   login_date date NOT NULL,
   CONSTRAINT pk_user_login_history PRIMARY KEY (id),
   CONSTRAINT fk_user_login_history FOREIGN KEY (user_id) REFERENCES "user" (id) ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS = FALSE
);

INSERT INTO "user"(username, name_surname)
    VALUES ('user1', 'User 1');

INSERT INTO "user"(username, name_surname)
    VALUES ('user2', 'User 2');

INSERT INTO user_login_history(user_id, login_date)
    VALUES (1, '2014-01-01');

INSERT INTO user_login_history(user_id, login_date)
    VALUES (1, '2014-01-02');

INSERT INTO user_login_history(user_id, login_date)
    VALUES (2, '2014-02-01');

INSERT INTO user_login_history(user_id, login_date)
    VALUES (2, '2014-02-02');


Bu Script çalıştırıldığı zaman, veritabanı versiyonumuz 1'den 2'ye geçirilmiş olacak diye farz edelim. Bir de bunun tam tersi, veritabanı versiyonunu 2'den 1'e geri döndürecek bir Script'e ihtiyacımız var. Bunu da aşağıdaki gibi hazırlayıp, "2-1.sql" ismi ile kaydedelim.

/*** Remove user and user_login_history tables ***/

DROP TABLE "user_login_history";

DROP TABLE "user";


Aslında mantık gayet basit. İlk Script'te ne yaptıysak, burada onu geri alıyoruz.

Devam edelim. Geliştirmeye sürdükçe veritabanında bazı değişikliklere ihtiyaç duyuyoruz. Örneğin "user" tablosuna "is_admin" isimli bir sütun eklememiz gerekti. Bunun için aşağıdaki gibi bir Script hazırlayıp "2-3.sql" ismi ile kaydedelim.

/*** Add is_admin column to user table ***/

ALTER TABLE "user"
  ADD COLUMN is_admin bit;

UPDATE "user" SET is_admin = '0';

ALTER TABLE "user"
   ALTER COLUMN is_admin SET NOT NULL;


Dikkat : Burada neden öncelikle NULL olabilir bir sütun ekleyip, ardından bu yeni sütunu tüm tabloda doldurup sonrasında da NULL olamaz hale getirdiğimizi düşünenler olabilir. Bu işin kolay yolu, yeni sütuna DEFAULT değer vermek olabilirdi. Böylece tabloda var olan veriler için bu sütuna DEFAULT değer atanmış olurdu. Ancak Prod. ortamında sütunların DEFAULT değerinin olması Best Practice'e uymaz. Bu Business Logic'e ait bir konudur ve Business Logic'in bölünmemesi gerekir (Yarısı uygulamada, yarısı veritabanında şeklinde).

Bir de bunun ters yönlü Script'ini "3-2.sql" ismi ile yazalım. Tahmin edeceğin gibi, yeni eklediğimiz sütunu siliyoruz.

/*** Remove is_admin column from user table ***/

ALTER TABLE "user" DROP COLUMN is_admin;


Geliştirmeye devam edelim. Şimdi de "company" isimli bir tabloya ihtiyacımız var. Ayrıca mevcut "user" tablosunu da bir Foreign Key ile bu tabloya bağlayacağız. Bunun için aşağıdaki gibi bir Script hazırlayıp "3-4.sql" ismi ile kaydedelim.

/*** Add company table, insert some data and connect with user table ***/

CREATE TABLE "company"
(
   id serial,
   company_name character(20) NOT NULL,
   CONSTRAINT pk_company PRIMARY KEY (id),
   CONSTRAINT uk_company UNIQUE (company_name)
)
WITH (
  OIDS = FALSE
);

INSERT INTO "company"(company_name)
    VALUES ('Company 1');

ALTER TABLE "user"
  ADD COLUMN company_id integer;

UPDATE "user" SET company_id = 1;

ALTER TABLE "user"
   ALTER COLUMN company_id SET NOT NULL;

ALTER TABLE "user"
  ADD CONSTRAINT fk_user FOREIGN KEY (company_id) REFERENCES company (id) ON UPDATE NO ACTION ON DELETE NO ACTION;


Ve yaptıklarımızı geri alacağımız "4-3.sql" Script'i...

/*** Remove company table and disconnect user table ***/

ALTER TABLE "user" DROP CONSTRAINT fk_user;

ALTER TABLE "user" DROP COLUMN company_id;

DROP TABLE "company";


Biliyorum, biraz bunalttım ama gösteriye başlamadan önce son bir çift daha ekleyeceğiz. Diyelim ki projeyi bitirdik ancak performans sıkıntımız çıktı. Bu yüzden de aşağıdaki gibi bir Index çalışması yaptık (4-5.sql).

/*** Create indexes for user and company tables ***/

CREATE INDEX ix_user
   ON "user" (username ASC NULLS LAST);

CREATE INDEX ix_company
   ON "company" (company_name ASC NULLS LAST);


Aynı şekilde eklenen Index'leri silen "5-4.sql" Script'i...

/*** Remove indexes from user and company tables ***/

DROP INDEX ix_user;

DROP INDEX ix_company;


Hepsi bu kadar. Söz veriyorum, bundan sonrası daha eğlenceli olacak.

pg-migrator'ın alt klasörleri arama ve işletme yeteneği var. Bu yüzden Script'lerimizi klasörler ile istediğimiz gibi kategorize edebiliriz. Ben Script'leri aşağıdaki şekilde klasörlere ayırdım. Hatta isimlendirme kurallarına uymayan dosyaların yok sayıldığını göstermek için "ignored-files" isimli bir klasör de ekledim.




Bundan sonrasında iki şeye ihtiyacımız olacak. İlki, pg-migrator aracı ki,  terminal (Linux veya Mac OS) veya command prompt (Windows) penceresinden aşağıdaki komutu yazarak bunu elde edebiliriz (Duruma göre başına "sudo" yazmak gerekebilir).

npm install -g pg-migrator

Böylece gerekli araç makinemize kurulmuş olacak. Bir de testlerde kullanmak üzere yetkili bir kullanıcıya ihtiyacımız var. Ben bu makalede "test" isimli ve "test" şifreli bir kullanıcı ile çalışacağım.

İlk olarak terminal (Linux veya Mac OS) veya command prompt (Windows) penceresinden aşağıdaki komutu, Migration Script klasörlerinin kök klasöründe çalıştıralım.


pg-migrator postgres://test:test@localhost/testdb


Bu durumda aşağıdaki gibi bir ekranla karşılaşmış olman gerekiyor.



Söylediğine göre, boş veritabanımızı versiyon 1'den versiyon 5'e geçirmiş. Yani aslında sırasıyla "1-2.sql" -> "2-3.sql" -> "3-4.sql" -> "4-5.sql" Script'lerini çalıştırmış. Bir de gözle görelim bakalım, doğru mu konuşuyor.


Dikkat : Burada bizim eklediklerimizin haricinde "version" isimli bir tablo bulunuyor. Bu tablo, pg-migrator tarafından veritabanının anlık versiyonunun ne olduğunu tutmak için kullanılır. Bazı DB Migrator araçlarında böyle bir tablo yerine, aracın çalıştığı bilgisayarın dosya sisteminde versiyonun saklandığını da gördüm. Ancak bu durum bana hiç mantıklı gelmiyor. Bu öncelikle, herkesin aynı dosya ile çalışabilmesi için veritabanı sunucusuna RDP veya SSH ile bağlanıp uygulamayı orada çalıştırmayı gerektirir. Ayrıca veritabanının gerçek versiyonu ile bu dosyada yazan değerin senkronizasyonu kolaylıkla kaybolabilir. Bu sebeple ben de, veritabanı versiyonunu tutmak için en doğru yerin veritabanının kendisi olduğu görüşünü savunuyorum.

Diyelim ki, yapılan son Index çalışmasını geri almak istiyoruz. Yani versiyonda bir adım geri gitmek istiyoruz. Bu durumda terminal (Linux veya Mac OS) veya command prompt (Windows) penceresinden aşağıdaki komutu çalıştırmamız gerekir.


pg-migrator postgres://test:test@localhost/testdb -1


Komutunun ardından aşağıdaki gibi bir ekranla karşılaşmış olman gerekiyor.




Index'lerin kaldırılarak 4. versiyona dönüldüğünü söylüyor. Hemen gözle de kontrol edelim.




Güzel, şimdiye kadar sıkıntı yok. Diyelim ki, bu sefer de 2. versiyona dönme ihtiyacını hissettik. Bu durumda terminal (Linux veya Mac OS) veya command prompt (Windows) penceresinden aşağıdaki komutu çalıştırmamız gerekir.


pg-migrator postgres://test:test@localhost/testdb 2


Komutunun ardından aşağıdaki gibi bir ekranla karşılaşmış olman gerekiyor.


Söylediğine göre veritabanı versiyonunun 4. versiyon olduğunu anlamış ve sırasıyla "4-3.sql" -> "3-2.sql" Script'lerini bularak çalıştırmış. Bir de gözle görelim.




Peki. Bir adım ileri gitmek, yani 3. versiyona geçmek istiyoruz diyelim. Bu durumda terminal (Linux veya Mac OS) veya command prompt (Windows) penceresinden aşağıdaki komutu çalıştırmamız gerekir.


pg-migrator postgres://test:test@localhost/testdb +1


Komutunun ardından aşağıdaki gibi bir ekranla karşılaşmış olman gerekiyor.



3. versiyonun gereği olan sütunu eklediğini ve tablodaki veriyi güncellediğini söylüyor. Gözle de görelim.



Tamamdır, bu kadar oyun yeter. Artık aşağıdaki komutla en son versiyona geri dönelim.

pg-migrator postgres://test:test@localhost/testdb




5. versiyona sorunsuzca geçtiğimizi gözle de kontrol edelim.



pg-migrator Komutları

Yukarıdaki örnekte, ileri-geri güzel bir oyun oynadık ve kullanabileceğimiz tüm komutları kullandık. Burada bu komutların toplu listesini ve anlamlarını bir kez daha veriyorum.

pg-migrator postgres://test:test@localhost/testdb    : Son versiyona geçiş
pg-migrator postgres://test:test@localhost/testdb +1 : Bir versiyon ileri
pg-migrator postgres://test:test@localhost/testdb -1 Bir versiyon geri
pg-migrator postgres://test:test@localhost/testdb x  : Direk x versiyona geçiş



Sık Karşılaşılan Hatalar

pg-migrator'ı kullanırken aşağıdaki hatalara düşmek olası, dikkat etmek lazım.
  • pg-migrator komutu, Migration Script'lerinin kök klasöründe çalıştırılmalıdır. pg-migrator, mevcut klasörü ve tüm alt klasörleri arar. Dış klasörlere bakmaz.
  • Migration Script'leri "x-y.sql" isimlendirme formatında olmalıdır. Burada x ve y geçerli birer sayı olup kesinlikle ardışık olmalıdır.
  • Yukarıdaki x ile y arasında "3-5.sql" gibi sayısal boşluk olmamalıdır.
  • pg-migrator, tüm Migration Script'lerini Transaction Scope içerisinde çalıştırır (Bir Script dosyasının tamamı işletilir veya Roll Back edilir). Bu yüzden Migration Script'lerine ayrıca bir Transaction ifadesi yazmamak gerekir.
  • Genel olarak veritabanı şeması ile ilgili işlemler yapıldığı için, buna yetkili bir kullanıcı ile pg-migrator aracını kullanmak gerekir.


Geri
PostgreSQL ve pgAdmin Kurulumu
İleri
Bu en son makaledir

Yorum Gönder

 
Top