MongoDB’nin dinamik şeması aynı anda güçlü ve zordur. Java’da, ortak bir yaklaşım şemayı uygulama katmanında açık yapmak için bir nesne-belge eşleyiciyi kullanmaktır. Kotlin, ek güvenlik ve özlülük sağlayarak bu yaklaşımı daha da ileri götürür. Bu yazı, MongoDB ile yapılan geliştirmelerin Kotlin’den nasıl faydalanabileceğini ve uygulamada hangi kalıpların faydalı olabileceğini göstermektedir. Kodlama ve şema tasarımı için en iyi uygulamaları da ele alacağız.
- MongoDB’nin dinamik şeması güçlüdür, ancak daha fazla hataya yol açabilir. Güvenliği sağlamak için, şemanın uygulama katmanında açık ve zorunlu kılınması aşağıdakilerle daha da önemlidir:
- Statik olarak yazılmış bir dili kullanma
- Nesne-belge eşleştirmeyi kullanma (ODM)
- Kotlin bu yaklaşımı güzel bir şekilde tamamlar:
- İsteğe bağlı alanları modellemek için boş farkında olan türler
- Boş / isteğe bağlı alanları kolayca idare etmek için güçlü araçlar
- Değiştirilebilir özellikler. Bu yüzden gerekli alanları sağlamayı unutamayız.
- İsteğe bağlı alanlar için ekipte artan farkındalık
- Nesne-belge-haritacısı için kolayca değiştirilemez ve korunabilir veri yapıları oluşturmak için veri sınıfları
- Coroutines, engelleme yapmayan sürücüyü geri aramalarla uğraşmadan kullanabilmenizi sağlar
- Mevcut nesne-belge eşleştiricileri, değişken veri sınıflarını sorunsuz bir şekilde destekliyor
- En İyi Uygulamalar:
- Bir alanı eklerken veya kaldırırken, boş yazı türlerini önlemek için genellikle tüm belgeleri güncellemek daha iyidir.
- Projeksiyonlar için özel veri sınıfları kullanın.
- İsteğe bağlı alanları isteğe bağlı ek bir veri sınıfına sarın. Bu şekilde, veri sınıfındaki alanlar geçersiz sayılabilir.
- Sabitlenmiş iç içe
object
s, belgenin alanlarına sezgisel olarak erişmek için kullanışlıdır.
MongoDB’nin Java’daki Dinamik Şeması ile Başa Çıkma Statüsü
MongoDB şema değildir. Her zaman bir şema vardır, ancak dinamiktir ve veritabanı tarafından zorlanmaz. Bu, MongoDB’yi çok esnek ve uyarlanabilir kılar. Bununla birlikte, şema ve varyasyonlarının izini kaybetme tehlikesi vardır. Biz de sona erebilir “alanı tanımlanmamış” hatalar , hataya açık dize concatenations, yanlış türleri ve yazım hataları. Çözüm nedir?
Statik olarak yazılmış bir dil ve nesne eşlemesi.
Belgeleri, statik olarak tanımlanmış bir yapıya sahip bir sınıfın örnekleri olan nesnelerle eşleriz. Bu şekilde, derleyici değerleri okurken ve yazarken bize rehberlik eder. Bu yaklaşım, MongoDB’yi kullanmanın (tip-) güvenli bir yolunu sağlar ve şemayı, uygulama katmanında açık, zorunlu, (doğrulanmış) ve belgelenmiş hale getirir.
Yani hepsi harika ama yeni bir şey değil. Bu yaklaşımı bir süredir Java’da kullanıyoruz. Ancak, Kotlin bu yaklaşımı daha da genişletip geliştirebilir!
nullability
Boş Bilinen Türler
Java ve nesne eşleyiciyi kullandığımız zaman bile, isteğe bağlı alanları takip etmekte zorlanıyoruz. Özellikle veritabanı modeli birkaç yıl içinde büyüdükten sonra aynı ekip üyesi ayrıldı ve yenisine katıldı.
// Java
public class Design {
private String name; // can name be null?
private Statistics statistics; // can statistics be null?
// getter & setter boilerplate
}
Ünlü NullPointerExceptions ile karşılaşmak olasıdır:
int likes = design.getStatistics().getLikeCount() // NPE because statistics is null in some cases!
@Nullable
Burada yardım gibi ek açıklamalar , ancak derleyici tarafından uygulanmadıklarını ve bakımlarını yapmalarının unutulması kolaydır.
Mesele şu ki, Java’nın tip sistemi null ve null olmayan tipler arasında ayırım yapmamaktadır. Neyse ki, Kotlin yapar. Sadece ?
türün arkasına bir tane eklemek zorundayız .
// Kotlin
data class Design(
val name: String, // name can never be null
val statistics: Statistics? // statistics are optional/nullable
)
Bu nedenle, Kotlin sınıflarımız ayrıca hangi alanların boş bırakılabileceğini ve hangilerinin olmayacağını da belgelemektedir. Ve daha da iyisi: Derleyici, değere erişmeden önce boş değerlerin kullanılmasını zorlar. Bu can sıkıcı NullPointerExceptions önler.
val likes = design.statistics.likeCount // Compile Error!
// We are not allowed to access likeCount directly, because statistics can be null.
Başka bir nokta: name
Bir Design
belgenin alanının kod tabanında her yerde kullanıldığını varsayalım . Bu alanı isteğe bağlı yapmaya karar verdiğimizi varsayalım. Kotlin’de biz sadece bir tane ekliyoruz ?
ve derleyici boş değerlerin üstesinden gelmek için ayarlanması gereken özelliğe her erişimde bizi işaret ediyor name
. Bu çok güçlü.
Benim için, Kotlin’in kırılganlığı, MongoDB’nin dinamik şemasını açık ve uygulama kodunda belgelendirmenin eksik kısmıdır. Ayrıca, güvenliği önemli ölçüde artırır.
İsteğe Bağlı Alanları Kullanmak İçin Güçlü Araçlar
Bir alanın boş olabileceğinin farkında olduğumuzu varsayalım. Sonra, gerçek boş kontrolü yapmak hala Java’da hantal.
// Java
int likes;
if (design != null && design.getStatistics() != null) {
likes = design.getStatistics().getLikeCount();
} else {
like = 0;
}
Bu iç içe boş çeklerin unutulması kolaydır. Neyse ki, Kotlin’in null alanlarını idare etmek için güçlü ve özlü araçları var.
val likes = design?.statistics?.likeCount ?: 0
Taşınmaz Özellikleri Olan Zorunlu Alanları Zorla
Veri sınıflarında ve nesne eşleyicide değişmez özellikler kullanıyorsak, başka bir fayda daha var: Gerekli (= null olmayan) bir alan ayarlamayı unutamayız.
val newDesign = Design() // compile error! The non-nullable property `name` is missing.
mongoTemplate.insert(newDesign)
Bu gerçekten büyük bir hata kaynağını ortadan kaldırır ve şemayı tutarlı tutar.
İsteğe Bağlı Alanlar İçin Artan Farkındalık
“
statistics
Tasarım belgesine bir alan eklemeliyiz ”“Alan boş olabilir mi?”
Combo MongoDB + Kotlin’i bir süre kullandıktan sonra, ekibimizde şu etkiyi keşfettik: Her yeni bir alan ortaya koyduğumuzda, otomatik olarak çekilebilirliğini tartışmaya başlarız. Etkileyici. Tip sistemi, özelliği ilgili veri sınıfına eklediğimiz anda, boşluğa karar vermeye zorlar. Bu, zorunlu ve isteğe bağlı alanlar hakkındaki farkındalığımızı şekillendirdi ve yeni alanlar söz konusu olduğunda sorduğumuz ilk sorulardan biri haline geldi.
Veri Sınıfları
Güçlü Veri Yapısı Tanımı
Nesne-belge eşleştiricinin katı kullanımı, her belge için karşılık gelen bir sınıf yazmanızı gerektirir. Neyse ki, Kotlin’in veri sınıfları Java’nın aksine büyük bir rahatlama.
data class Design(
val name: String,
val dateCreated: Instant,
val statistics: Statistics?
)
data class Statistics(
val likeCount: Int,
val dislikeCount: Int
)
Bu kadar. Veri yapılarının tanımının nasıl daha özlü olabileceğini hayal edemiyorum. Ek avantajlar:
- Değişmez özelliklerinden dolayı yüksek güvenlik.
hashCode()
,equals()
VetoString()
üretilir ve (daha önemlisi) muhafaza edilmesi gerekmez.- Adlandırılmış değişkenler nedeniyle okunabilirlik.
Kotlin-Dostu Nesne-Belge-Eşleyici
MongoDB için mevcut nesne-belge eşleyici, Kotlin’in değişmez veri sınıfları ile iyi çalışır. Bunu aldığımız için almamalıyız. Sadece Hazırda Bekleme ve Kotlin’le birlikte çalışması için gereken çekicilere bir göz atın . Yine de, veri sınıflarından yararlanmak mümkün değildir.
Bazı ODM’lere genel bir bakış:
- Projelerimizde ağırlıklı olarak Spring Data MongoDB kullanıyoruz . Çoğu zaman daha düşük seviyeli MongoTemplate kullanıyoruz .
- Hafif ve Kotlinli KMongo da oldukça ümit verici görünüyor. Ama henüz üretimde kullanmadım. Bunu kontrol et.
Not: Nesne yönelimli ile belge dünyası arasında daha az empedans uyumsuzluğumuz olduğundan, nesne-belge eşlemesi çok daha basittir (ORM’lere kıyasla). Daha az karmaşıklık genellikle daha az belaya neden olur. Aslında, haritalama çerçevemizde hiçbir haritalama sorunumuz ya da hata ayıklama seansımız olmadı. Sadece kutunun dışında çalışıyor.
Projeksiyonlar için Özel Veri Sınıfları
Çoğu durumda, belgenin tamamına değil sadece bazı alanlara ihtiyacımız var. Tüm belgeyi almak ve tüm veri sınıfına eşlemek daha yüksek sorgu sürelerine ve hafıza israfına yol açar. Neyse ki, Kotlin bu sorgu için uyarlanmış veri sınıflarını tanımlamayı kolaylaştırıyor:
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.core.mapping.Field
@Document(collection = "designs")
data class DesignWithLikeCount(
val name: String,
@Field("statistics.likeCount")
val likeCount: Int
)
Şimdi, sorguya sadece gerekli projeksiyonu eklemek zorundayız ve bittik.
// Usage: projection and mapping to a tailored class
query.fields()
.include("name")
.include("statistics.likeCount")
val entities = mongoTemplate.find(query, DesignWithLikeCount::class.java)
Yukarıdaki snippet’ler, alanları belirtmek için düz dizeler kullanıyor. Daha sonraki bir bölümde daha iyi yaklaşımlardan bahsedeceğiz .
Şema Tasarımı
Yuvalamada Sızdırmazlıktan Kaçının
Şema tasarımına gelince boşuna dikkat etmeliyiz. Bir örnek:
data class Design(
val name: String,
val likeCount: Int?,
val vectorDesign: Bool?,
val dislikeCount: Int?
)
Burada yanlış olan iki şey var:
- İlk olarak, birçok null alan var. Şemamızdaki kırılganlığı azaltmak için onu daha basit ve daha az hataya eğilimli hale getirmek için çaba göstermeliyiz.
- İkincisi, bu şema bize anlamsal olarak birbirine ait alanlar varsa söylemez. Sadece alana bak
likeCount
vedislikeCount
. Bu alanların bir araya getirildiğini ya da hiçbirinin ayarlanmadığını varsayalım. Bu şema / veri sınıfına bakarak açık değildir.
Bir çözüm, bu alan grubunu birlikte yeni bir veri sınıfına sarmaktır Statistics
:
data class Design(
val name: String,
val vectorDesign: Bool?,
val statistics: Statistics?
)
data class Statistics(
val likeCount: Int, // non-nullable
val dislikeCount: Int // non-nullable
)
Bu şekilde, şema açıkça belirtir: Eğer bir statistics
alan varsa, tüm alt alanları asla boş olmaz.
Not: Bu arada, nesne haritalamanın şema tasarımı üzerindeki etkisini seviyorum. Anlaşılması zor, daha fazla hataya eğilimli ve işlemesi daha zor olan oldukça değişken bir şemaya (birçok null alanına veya tam rastgele alan adlarına) engel olur. Temel kuralm: Bir şemayı sınıflarla kolayca eşleştiremezsek, genellikle iyi değildir.
Yeni Alanlar: Boş Türler yerine Göç
Yeni alanı dateCreated
Tasarım belgesine eklememiz gerektiğini varsayalım . Bu alan null olabilir mi, değil mi? İlk bakışta, evet, çünkü yeni uygulama sürümünü yayınladığı sırada (bu alanı yazmaya başlayan) mevcut belgelerin hiçbiri bu alana sahip değil.
data class Design(
val name: String,
// is the new field `dateCreated` nullable?
val dateCreated: Instant?
// Well, there are existing documents without this field...
)
MongoDB’de sık sık şemayı değiştiririz çünkü uygulamanın değişen erişim düzenine göre ayarlamamız gerekir . Ancak, bazı belgeler yeni şemayı yerine getirirken bazılarını eskime alanlarından oluşan bir şemaya götürebilir.
dateCreated
Tasarım açısından null / isteğe bağlı olup olmadığını veya daha sonra eklendiğinden emin değiliz (değişen bir şemanın yan etkisi). Bu şema belirsiz ve kavramak zorlaştırır. Bu nedenle, bu alanı her belgede özel bir geçiş komut dosyasıyla ayarlayarak gerçek bir şema geçişi gerçekleştirmenizi şiddetle tavsiye ederim .
db.designs.updateMany({}, { $set: { "dateCreated": ISODate() } })
Böylece, yeni alanı güvenli dateCreated
olmayan olarak null olarak işaretleyebiliriz . Sonuç olarak: Sonuçta ve kalıcı olarak şemanızı temizleyin. Tam şema geçişleri yaparak mümkün olduğunca tutarlı olmasını sağlayın. Boşluklu alanları şema değişiklikleri için değil tasarıma göre kullanmaya çalışın.
Çeşitli
object
Alan Adları için İç İçe Sabitleri kullanın
Genellikle sorgular için Spring’s MongoTemplate kullanıyoruz. Bu durumda, alan adına başvurmamız gerekir.
val query = Query().addCriteria(Criteria.where("statistics.likeCount").gt(10))
val designs = mongoTemplate.find(query, Design::class.java)
Etrafında dizelerle uğraşmak "statistics.likeCount"
hataya açıktır. Sabitleri kullanmak bariz bir çözümdür. Ancak sabitleri nasıl düzenleyebiliriz ki belgemizin yerleştirilmesini yansıtsınlar? İç içe object
s.
object DesignFields {
const val NAME = "name"
object Statistics {
const val SELF = "statistics"
const val LIKE_COUNT = "$SELF.likeCount"
}
}
Kullanımı:
// typesafe and auto-completion-friendly
Query().addCriteria(Criteria.where(DesignFields.Statistics.LIKE_COUNT).gt(10))
Bu oldukça iyi çalışıyor. Bununla birlikte, bu yaklaşımın bir dezavantajı vardır: Bir alan değişirse, iki noktayı korumalısınız: Veri sınıfları ve alan sabitleri. Data sınıfının bir özelliğini değiştirdikten sonra string sabitlerini güncellemeyi unutmak kolaydır. Neyse ki, Kotlin’in yansıma API’sini kullanabilir ve veri sınıflarının özelliklerine başvurabiliriz.
object DesignFields {
val name = Design::name.name // "name"
object Statistics {
val self = Design::statistics.name // "statistics"
val likeCount = "$self.${Statistics::likeCount.name}" // "statistics.likeCount"
}
}
const
Burada anahtar sözcüğü kullanamayacağımıza dikkat edin, çünkü özellikler derleme zamanı sabiti değildir. Dolayısıyla, bu “sabitleri” @Field
, özel projeksiyonlar için kullanılan Spring’s gibi açıklama değerleri için kullanamayız .
Başka bir alternatif, doğrudan sorgudaki özellikleri kullanarak yazılan sorguları destekleyen KMongo olabilir :
col.findOne(Design::name eq "Cat")
col.findOne(Design::statistics / Statistics::likeCount gt 10)
// or using the experimental annotation processing
import Design_.Statistics
col.findOne(Statistics.likeCount gt 10)
Coroutines: Geri Çağrısız Asenkron Sürücü
Eşzamansız MongoDB sürücüsü, iş parçacıklarının verimli kullanılmasına izin verir. Ancak, bu genellikle daha karmaşık bir koda yol açar, çünkü geri aramalarla veya akış tabanlı API’lerle uğraşmamız gerekir. Fortunatelly, KMongo, MongoDB’nin eşzamansız sürücüsü çevresinde koroutine dayalı bir sarmalayıcı sağlar. Böylece eşzamanlı görünen, ancak eşzamansız ve kaputun altında engelleyen bir kod yazabiliriz.
GIPHY App Key not set. Please check settings