SQLDelight
Başlarken
Merhaba, bu blog’da Cash App tarafından oluşturulan, yazdığımız SQL sorgularından typesafe kotlin API’leri oluşturmaya yardımcı olan bir veri tabanı kütüphanesinden bahsedeceğim: SQLDelight.
SQLDelight, Kotlin multiplatform bir kitaplıktır. Kotlin multiplatform kitaplıklar, kotlin ile yazılmış bir uygulamanın birden çok platformda çalışmasını sağlar. Multiplatform ile ilgili detaylara kotlinin web sitesinden ulaşabilirsiniz.
SQLDelight bir kod üreticisidir. Temel olarak, veri tabanımız için gerçek SQL ifadeleri yazmak yerine SQLDelight onları kotlinden çağırabileceğimiz sınıflar veya yöntemler haline dönüştürür. Ayrıca istediğimiz herhangi bir SQL veri tabanına uyum sağlayabilir. SQLite, MySQL, PostgreSQL vb.
Bir başka deyişle SQLDelight’ta yazdığımız kodlar, uygulamanın alt yapısında kullanılacak veri tabanı sistemleri için abstract kodlardır.
Son olarak SQLDelight plugin’in yararlı özelliklerinden biri de veri tabanı oluşturacak komut dosyaları için de otomatik tamamlama ve syntax önermesi sağlamasıdır. Böylece kod yazımı hızlanır ve zamandan tasarruf sağlanır.
Proje Oluştururken
Konuyu pekiştirmek açısından bir uygulama üzerine deneme yapalım.
- İlk aşamada SQLDelight IDE eklentisi aşağıdaki adımlar takip edilerek Android Studio’ya eklenmelidir.
File -> Settings -> Plugins -> Marketplace -> SQLDelight ->Install
- SQLDelight için gereken kitaplık projeye eklenmelidir. Proje seviyesindeki
build.gradle
dosyasındakibuildscript -> dependencies
bloğuna aşağıdaki kod eklenmelidir.
classpath("com.squareup.sqldelight:gradle-plugin:1.5.0")
- Son olarak, Gradle eklentisi de uygulama modülüne eklenmelidir. Modül seviyesindeki
build.gradle
dosyasında en üstteki satıra aşağıdaki kod eklenmelidir. Ardından proje sync edilir.
apply plugin: ‘com.squareup.sqldelight:gradle-plugin:1.5.0'
SQLDelight için gradle eklentisi, tüketicilere (domain-specific language) etki alanına özgü bir dil yani DSL sunar. (DSL’in anlamını görmek için tıklayın.)
Modül seviyesindeki build.gradle
dosyasının android bloğunun içine aşağıdaki kod eklenmelidir.
sqldelight {
Database {
packageName = "$<apppackagename>.db"
sourceFolders = ["sqldelight"]
schemaOutputDirectory = file("src/main/sqldelight/schemas")
verifyMigrations = true
}
}
packageName: dönüştürülmek istenen database’in oluşacağı paket sourceFolders : sqldelight syntaxı ile yazılmış kodların bulunduğu directory
Not: Yukarıdaki blok içerisine dependencyKey diye bir değişken daha eklenebilir. Dependency key veri tabanının içeriğini birden çok gradle modülü arasında dağıtmayı ve ardından bunları uygulama modülünüzde tekrar bir araya getirmeyi sağlar.
VERİ TABANI İŞLEMLERİ
SQL Delight, tablolar ve sorgular gibi işlevlerini açıklamak için özel komut dosyaları kullanır. Bu dosyalar .sq dosya uzantısını kullanır. Bu .sq uzantılı dosyalar src/main/sqldelight paketi içerisinde oluşturulur.
Yukarıdaki adımları tamamladıktan sonra .sq uzantılı dosya artık oluşturulabilir. db klasörüne: sağ tıkla -> new -> sqldelight file/table
burada file,table ve migration olmak üzere üç farklı dosya çeşidi eklenebilir.
Bir SQLDelight dosyasında yazılabilecek basit query ifadelerini deneyelim:
- CREATE TABLE
- INSERT
- SELECT
- DELETE
- REPLACE
SQLDelight kullanarak veri tabanı oluşturacağımız bir uygulama yapalım. Uygulamayı GitHub hesabında bulabilirsiniz. https://github.com/pawntoqueen/Movies-SQLDelight
CREATE TABLE
SQLDelight kütüphanesinin film ismi, IMDB puanı ve yayınlanma yılı bilgilerini bulunduracak veri tabanını oluşturması için .sq uzantılı dosyaya aşağıdaki gibi create table komutu yazabiliriz.
CREATE TABLE Movies (
movie_name TEXT NOT NULL,
movie_imdb_point INTEGER NOT NULL,
release_year INTEGER NOT NULL
);
INSERT
Uygulamanın alt yapısındaki veri tabanı sistemine (örnekte verdiğim SQLite) veri eklemek için aşağıdaki gibi bir Insert fonksiyonu yazabiliriz. Alınacak parametreler “?” ile temsil edilir.
add_movie:
INSERT INTO Movies
VALUES(?,?,?);
SELECT
SQLite için query üretmesi üzere .sq dosyasına aşağıdaki gibi logic komutları yazılabilir. Örnekte, tüm filmleri seçme veya isme göre seçim yapma fonksiyonları yazılmıştır.
select_all:
SELECT *
FROM Movies
ORDER BY movie_name;select_by_name:
SELECT *
FROM Movies
WHERE movie_name = ?;
DELETE
Oluşturulacak olan tablodan veri silme temsilini aşağıdaki gibi gösterebiliriz.
delete_by_name:
DELETE
FROM Movies
WHERE movie_name = ?;empty_table:
DELETE FROM Movies;
TEST EDELİM
Veri tabanı oluşturmak üzere yapılması gereken tüm adımlar bitti. İşlemleri doğru yapıp yapmadığımızı kontrol etmek için bir Unit Test yazalım.
Uygulama bazında kullanılacak veritabanı SQLite olacağından modül seviyesindeki build.gradle
dosyasına aşağıdaki bağımlılık eklenmelidir.
testImplementation "com.squareup.sqldelight:sqlite-driver:1.3.0
MoviesDatabaseTest.kt isimli bir test dosyası oluşturalım.
Test aşamasında, veri tabanını oluşturmak veya migrate etmek gibi ara aşamalarla uğraşmamak adına JdbcSqliteDriver
sınıfı kullanılması önerilir. Bu unit test yazmak için kullanılan, sadece test esnasında aktif olacak şekilde bellekte oluşturulan bir sınıftır.
private val inMemorySqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).apply {
Database.Schema.create(this)
}
Test kodlarının tamamı aşağıdaki gibidir.
package com.pawntoqueen.movieswithsqldelight
import com.pawntoqueen.sqldelight.models.db.Movies
import com.raywenderlich.android.sqldelight.db.Database
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import junit.framework.TestCase.assertEquals
import org.junit.Test class MovieDatabaseTest { private val inMemorySqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).apply {
Database.Schema.create(this)
} private val queries = Database(inMemorySqlDriver).moviesQueries @Test
fun smokeTest() {
val emptyItems: List = queries.selectAll().executeAsList()
assertEquals(emptyItems.size, 0)
queries.add_movie(
movie_name = "The Batman",
movie_imdb_point = 8,
release_year = 2022
)
val items: List<Movies> =queries.select_all().executeAsList()
assertEquals(items.size, 1)
val movie =queries.select_by_name("The Batman").executeAsOneOrNull() assertEquals(movie?.movie_imdb_point?.toInt(), 8) assertEquals(movie?.release_year?.toInt(), 2022)
}
UYGULAMA
Uygulama bazında SQLite kullanacığımızdan bahsetmiştik. SQLDelight kütüphanesinin SQLite sürücüsü implemente edilmelidir. Modül seviyesindeki build.gradle
dosyasına aşağıdaki AndroidSqliteDriver bağımlılığı eklenmelidir.
Not: Bu projede sadece bir app modülü olduğu için aşağıdaki bağımlılığı eklemek yeterlidir. Ancak çok modüllü bir proje olsaydı yukarıdaki anlattığım database bloğunu oluşturmak daha uygun olurdu.
implementation "com.squareup.sqldelight:android-driver:1.3.0"
Şimdi SQLDelight tarafından üretilmesini beklediğimiz sorguları MainActivity’de çağıralım;
val androidSqlDriver = AndroidSqliteDriver(
schema = Database.Schema,
context = applicationContext,
name = "items.db"
)
Test aşamasında kullanılan geçici bellekte veri tabanı oluşturmaya yarayan JdbcSqliteDriver sınıfı yerine verilerin kalıcı olarak bellekte tutulmasını sağlayan SQLite sürücüsü olan AndroidSqliteDriver sınıfından bir obje türetilir.
SQLDelightin ürettiği SQLite veri tabanı sınıfında otomatik olarak <db_name>Queries
isimli bir fonsiyonu oluşur. Bu fonksiyonla Create, Insert, Select, Delete sorgularına ulaşılabilir.
val queries = Database(androidSqlDriver).moviesQueries
Not: Veri tabanına veri ekleme kodları doğrudan MainActivity içinde olamaz. Ancak anlatım kolaylığı açısından şu anda aşağıdaki gibi yeni filmler ekledik.
queries.add_movie("Spiderman", 8, 2002) queries.add_movie("Ironman",8 , 2008) queries.add_movie("Captain America", 7, 2011)val itemsAfter: List<Movies> = queries.select_all().executeAsList() Log.d("ItemDatabase", "Items After: $itemsAfter")
Uygulama Hazır!
VERİ TABANI MIGRATION
.sq dosyası bir veri tabanının en son halinin nasıl oluşturulduğunu açıklar. Veri tabanın şu anda daha eski bir sürümdeyse, .sqm uzantılı migration dosyası oluşturulmalıdır.
Oluşturulan her veritabanının ilk versiyonu 1’dir. Migration dosyalarının isimleri hep <version>.sqm
olarak adlandırılır. İlk versiyondan ikinci versiyona geçiş için 1.sqm dosyasının içine şu yazılır:
ALTER TABLE movies ADD COLUMN <DEĞİŞİKLİKLER> INTEGER;
Bu kod parçası Database.Schema.migrate()
fonksiyonunu tetikler ve migration işlemi gerçekleştirilmiş olur.