Her gelenin, projeyi yeniden baştan yazma isteği

Talha Ocakçı
Mühendis kafası
Published in
7 min readAug 12, 2017

Yeni gelen proje mimarı, eski projeyi görür görmez “bunu baştan yazalım” diyor. Dediğine göre, o RDBMS veritabanı mücadeleyi artık kaybetmiş.

Ya da, manuel sorgu ile veritabanı sorgulamak için bir sebep göremiyormuş, Spring Data, hiç sorgu yazmadan bu sorgulamaları zaten yapabiliyormuş. Manuel kod yazarak kodun bakımını zorlaştırıyormuşuz.

Peki bu adam (ya da kadın) haklı mı? Neden bu kadar gaz? Deneyimlerini mi yansıtmak istiyor yoksa şirkete yeni birinin geldiğini hissettirmek için, okuduğu son 5 makaleden öğrendiklerini bize mi satıyor?

Bu yazıda, yeni gelen bu mimar arkadaşı gömmeden önce, ona başka bir gözle bakmanızı sağlamaya çalışacağım. O, iyi birisi aslında.

Değerlendireceğimiz ilk nokta, uygulama ile veritabanı arasındaki ilişkiler olsun.

Veritabanı seçimi doğru mu?

Veritabanları darboğaz (bottleneck) olduğunda performansı arttırmak için ilk yapacağımız şey sorgularımız üzerinde tekrar çalışmak, dinamik sorgular varsa ya da bir ORM aracı varsa bunları gerektiği noktalarda egale etmek, sonra bir takım dataları önbelleklemek (cachelemek), sonra bu cacheleri doğru zamanlarda periyodik aralıklarla güncellemek.

Hiçbiri bir çözüm olmaz ise veritabanı seçimimizi sorgulamak. Acaba ilişkisel veritabanı bizim için uygun değil miydi pek? Hem sahi, neden en baştan beri NoSql yapmadık ki?

Gelin size, son projemizde, bu aşamalardan nasıl geçtiğimizi Java teknolojileri özelinde biraz anlatayım. Diğer diller ve teknolojiler ile de benzeştiği yerler mutlaka olacaktır.

Manuel sorgu yazmayalım, bakımı zor oluyor

Şunu kabul edelim, ileride bakımı zor olacak diye, manuel kod yazmaktan çekiniyorsak, büyük ihtimalle daha jenerik bir kod yazıyoruz ya da daha jenerik yazılmış bir kütüphaneyi kullanıyoruz demektir. Bu da Reflection API kullanmak demek. Cglib gibi bytecode manipüle eden kütüphaneler kullanmak demek.

Kodun yazımı kolaylaşmasına, gelecekteki bakımı kolaylaşmasına rağmen, asıl işin yanında sınıf yapılarının üzerinde yapılan ekstra işlemler yüzünden yaşanacak performans kaybını büyük ölçüde kabul ediyoruz demektir.

Diyelim ki bunları kabul ettik, her “deneyimli” Java geliştirici gibi, Spring Boot, Spring Data, Spring DI, Hibernate teknolojilerini seçtik. Sonra da veritabanı olarak herhangi bir RDBMS’i (Mysql, PostgreSql) seçtik.

JPA’nın ilk pazarlanışı

JPA ve dolayısıyla Hibernate, ilk çıktığında şöyle pazarlanıyordu: RDBMS’teki satır ve sütun yapısı ile nesne yönelimli programlamadaki yapılar birbirine uymuyor. JPA bunları birbirine arkaplanda dönüştürür ve yazılımcı bunların zorluğuyla uğraşmaz.

Haklılardı. Her seferinde, ResultSet ile entity sınıfı arasında bir dönüştürücü (converter) yazmak, nesne bağımlıklarının hepsini elle doldurmak mantıklı değildi. Dolayısıyla, argüman doğruydu ve JPA kullanmaya hemen başladık. O zaman için aynı şeyi yapan otomatik kod yaratıcıları vardı. Nesnemizi (entity) veriyorduk, gerekli çeviricileri yazıyordu, bu çeviricileri ise üstü kapalı (implicit) olarak kullanabiliyorduk.

Mimarlar arasında o zamanlardaki tartışma “JPA mı kullanalım yoksa otomatik kod yaratıcılar mı kullanalım?” idi.

Bu savaştan JPA’nın ve özellikle bir implementasyonu olan Hibernate’in galip çıktığını kabul ediyorum.

SQL projeksiyonları, her şeyin Entity sınıfları ile yapılmasına izin vermiyor

Sonraları şöyle bir şey fark ediliyor:

Biz veritabanından, her zaman, var olan entityleri dönecek sorgular yazmıyorduk, scalar birkaç değer de dönebiliyorduk. Bu noktada, veritabanı ile uygulamamız arasında JPA izolasyonu olmasına rağmen, scalar değerleri dönüştürmek için üç çözümümüz vardı:

1- Kendi çeviricilerimizi yazmak

2- Sadece bu sonuca özel sınıflar yaratıp onları ilişkilendirmek

3- Gelen scalar değerleri var olan nesnelerimizin içindeki bazı alanlara yedirmeye çalışmak

İkinci çözüm bir sınıf çöplüğüne çeviriyordu ortalığı.Şöyle sınıf isimleri oluyordu: PeopleCountAndShoppingStatisticsEntity, PeopleCountAndDroppedShoppingCartStatistics. PeopleCountAndDroppedShoppingCartStatistics sınıfı, bugün alışveriş sepetini, satın almadan boşaltan kişilerin istatistiğini tutuyordu.

Üçüncü çözümde ise, nesnelerimiz sabitti, içindeki bazı alanlar dolu ya da boş oluyordu. Dolayısıyla servis katmanında, bir nesnenin içindeki hangi alanlar dolu olacak tahmin edemez oluyorduk.

“-dik, -duk” diyorum ama bunlar, RDBMS kullanırken halen var olan bir sorunumuz.

Join belasına kardaş döktüğümüz kan bizim

Veritabanı normalleştirmesini, CS 2xx’lü derslerden beri öğrettiler bize. İş hayatında da en önemli görevlerimizden biriydi doğru miktarda normalleştirme yapmak. İşin suyunu çıkarmadan, veriyi geri toplayamaz hale gelmeyecek kadar küçük parçalar ayırmak…

Bu, bizi, join sorguları belasına soktu. Ortalama bir entity’nin, 3 nesneye bağımlılığı olduğunu düşünürsek, bu entity’nin denk geldiği tablonun kullanılabileceği sorguları 4'e ayırabiliriz: Sadece bu tablonun sorgulandığı joinsiz sorgular, bu tabloyla birlikte bir tabloya daha join atılmış, iki tabloya join atılmış, üç tabloya join atılmış sorgular.

Takdir edersiniz ki, indekslerin mantıklı şekilde inşa edildiğini varsayarsak, en hızlı sorgular joinsiz sorgular olacaktır.

Peki, ya bağımlılıklardan birine gerçekten ihtiyacımız varsa? İşte o zaman joini yapıvereceğiz. Peki ne ile?

Join’li sorgulama yöntemleri

Elimizde üç yöntem var: Ya Hibernate’in HQL’ini kullanacağız ve bir join sorgusu yazacağız, ya bağımlılıklardan birini Lazy’den Eager’a çevireceğiz ya da join yapmadan önce bu tablomuza, sonra da bağımlı tablomuza bir sorgu atarak diğer bağımlılığı da çekeceğiz.

Eğer birincisini kullanacaksak, neden SQL’den vazgeçtik ki, yine sorgu yazıyoruz hem de yarın bir gün dil ya da teknoloji değiştirdiğimizde hiç içimize yaramayacak bir gramer ile…

İkincisini kullandığımızda, yani ilişki tipini eager’a çevirdiğimizde, bu, bütün sorgular için geçerli olacak. Dolayısıyla, bu entity’i içeren joinsiz hiçbir sorgu oluşturulamayacağız otomatik olarak.

Üçüncü çözüm ise, iyi bilinen (N+1) problemi… Yani, bağımlıkları doldurmak için tek tek sorgu atmak, join kullanmamak demek. SQL’e, join syntaxı, bundan belki 40 sene evvel, bunu engellemek için eklenmişti. Şimdi kullanmamak biraz tuhaf olur. (Engellemeye çalıştığımız şey join atmak değil, gereksiz yerde join atmak)

Bu çözümler yetmediği için Hibernate’e iki özellik daha eklendi:

1 — Hibernate Criteria API: Sorguları dinamik olarak oluşturabiliyoruz. Yani bir boolean değere göre, bir join clause’unu sorguya ekleyip çıkartabiliyoruz. Bence, bulunmuş en iyi çözüm. Yanına tikimi atarım. ✓

2 — Hibernate Entity Graph: Bu teknik ile sorgunun join yapısını sorguda belirtmiyoruz. Bu sorguda, entity’nin şu şu şu bağımlılıklarının dolmasını istiyorum diye belirtiyoruz. Arkada sorgular dinamik olarak hazırlanıyor. Size aşağıda bir entity graph örneği göstereyim:

Bu şekilde entity graphlarımızı tanımlıyoruz. Demek oluyor ki, PushEventWithScheduleAndGeofences grafiği, PushEventEntity nesnesinin sadece schedules ve geofences bağımlıklarını dolduracak.

Sorgu sadece PushEventEntity üzerinde atılmasına rağmen içindeki bağımlılıkların doldurulup doldurulmayacağını EntityGraph’ımız söyleceyecek.

Sonra sorgularda, hangi graph’ın kullanılmasını istediğimizi belirtiyoruz. Aşağıdaki sorgu, schedules ve geofences’in doldurulacağını söyleyen graph’ı seçmiş.

İlk başlarda iyi bir fikir gibi görünürken, geliştirme devam ettikçe takım içinde aykırı sesler yükselmeye başlıyor. Kodun bakımının zorlaşmaya başladığını söylüyorlar. Çünkü, her sorgu için doğru da bir isim kullanmak gerekiyor. Repository metodunda, collectWithScheduleAndGeofences diye bir metod ismi vermişiz mesela. Servis katmanının bunlardan uygun olanını seçmesi lazım. Yoksa NullPointerExceptionlar ile uğraşmak zorunda kalabilriz.

Tüm bu çözümler, takım içi teknik tartışmalar, kodu tekrar tekrar baştan yazmalar, hep bu join sorguları yüzünden oluyor. İlk önce Criteria API ile yazıp sonra bu olmadı diyerek entity graphlara geçilebilir, ya da komple bırakılıp HQL’e geçilebilir. Dolayısıyla diyebiliriz ki join, tüm kötülüklerin anasıdır.

Çok fazla seçenek olması, insanın başına gelmiş en kötü şeydir. Seçenekler arasında gidip gelmek, enerjimizi sömürebilir.

Spring Data (Aman spring ekibi, bir şeyden de eksik kalın)

Bizim derdimiz bize yetmezmiş gibi, Spring Data, “sizin işinizi kolaylaştıracağız” diyerek Spring Data’yı çıkardı. Atladık tabi hemen. Yazılım mimarlarının en sevdiği argüman: “Kodun bakımı kolaylaşacaaaak!”

Bok kolaylaşacak.

Şu ana kadar gördüğüm en ama en yanlış konsepti oluşturdular: Metod ismi üzerinden otomatik sorgu oluşturma. Yani şöyle bir metod yazıyoruz ve metod, dinamik bir sorgu oluşturup gerekli projeksiyonları yaparak bize gerekli nesneyi dönüyor. Dikkat edin metod ismi burada bile üç satır :D

public Person findByIdAndShoppingCartItemsCountIsGreaterThanAndShoppingCartLatestShoppinIsLaterThanAndDeletedFalse(String id, int itemCount, Date date);

O da bize, person tablosunu shopping_cart tablosu ile joinliyor ve sonrasında bir takım karşılaştırmalar yapan where şartını ekliyor ve verdiğimiz parametreleri sorguya sırasıyla yerleştiriyor. Bu metodu da hiç utanmadan sıkılmadan servis katmanında çağırıyoruz.

Bu yöntemi nasıl bir motivasyonla implement ettiler bilmiyorum ama bu yanlıştan biran evvel dönmeliyiz.

Şimdi diyebilirsiniz ki, kullanma kardeşim, var olan her şeyi kullanmayın nedir yani? İşte o kadar kolay değil, developer, dökümantasyonda gördüğü her şeyi kullanmak ister, kullanacak, nasıl engelleyeceksin? Code review’da geçirmesen toplantıda başının etini yer böyle bir şey var kullanmıyoruz da manuel kod yazıyoruz diye…

Allah ne verdiyse kullanalım

Allah ne verdiyse koyalım abi, çok lezzetli oluyor

Şahsen ben sucuk ekmeğe domates bile koymayan, sadeliği seven bir insan olarak, birden fazla teknikle aynı şeyin yapıldığını gördüğümüz kodları sevmiyorum. Gerçekte ise maalesef yukardaki “her şeyli tost” gibi bir kod çıkıyor önümüze.

Her gelen yeni bir teknikle kodu en baştan yazmak istiyor. Fakat derinlere girdikçe bu tekniklerin de eksikliklerini görüyor ve 3 yerden birini dokunmadan bırakıyor. En sonunda, altı kaval üstü şişhane bir kod çıkıyor ortaya. Belli yerlerde XML’lere yazılmış SQLler, HQL’ler, bazen entity üzerine yazılmış HQLler, bazen EntityGraph’lar, bazen Hibernate Criteria API’lar, bazı yerlerde Spring Data metod çağrıları, bazen çıplak Hibernate metod kullanımları…

Kısaca, bu kafa karışıklığının en büyük sebebi: Performans vs kod bakım kolaylığı.

Hangi mimar, hangisini tercih ederse ve bu tercih edişi yazılım geliştiricilere mantıklı sebeplerle izah edebilirse o kullanılmalıdır.

Çünkü, geliştirici, bu sebeplere, yeni şeyler öğrenebilmek adına uzun süre direnecektir. Fakat en dirençli geliştirici bile, mühendislik formasyonundan dolayı, matematiksel ispatlara karşı direnmeyecektir. Matematiksel olmayan bu gibi deneyimler ise her zaman tartışmaya açıktır; bu kod karmaşası “doğal” görülebilir. Bu noktada, en baştaki konuya dönüyoruz, mimar, bu işi bilerek mi değiştirmek istiyor, yoksa sadece ego savaşı mı? Buna, zamanla karar verebilecek hale gelebiliyoruz.

NoSql No Cry

Tüm dertlerimiz temelde, SQL querylerinin projeksiyonlarının, Java sınıflarına kolaylıkla taşınamaması (static typing olduğu için), join sorgularının çok çeşitlenebilmesi ve performansı gerçekten çok etkileyebilmesi.

Birincisi için, Java özelinde konuşursak, artık çok geç, yapacak bir şey yok. İkincisi için ise, mantıklı olan koşullarda ilişkisel veritabanlarından NoSql’e geçiş yeterli olacaktır. Kurtulacağımız ilk yük joinler olacak çünkü. Bir sonraki yazımda, NoSql tercihimiz olan Couchbase’den bahsetmeye başlayacağım.

Eminim ki, keyifli, mantıklı temellere oturan bir yazı olacak. Haberdar olmak için Twitter’dan takip edebilirsiniz. Yazıyı beğendiyseniz kalbe tıklayarak yayılmasına yardımcı olursanız çok sevinirim.

--

--

Talha Ocakçı
Mühendis kafası

Yazılarımda bazı şeyleri yanlış anlatıyor olabilirim. Kesin yanlış anlatıyorumdur.