Ketik Safe SQL di Kotlin

Ekspresivitas adalah properti yang menarik dari bahasa pemrograman. Dengan hanya menggabungkan ekspresi, Anda dapat mencapai hasil yang mengesankan. Beberapa bahasa dengan sengaja menolak gagasan ekspresif, tetapi Kotlin jelas bukan bahasa seperti itu.


Menggunakan konstruksi bahasa dasar dan sedikit gula, kami akan mencoba untuk membuat ulang SQL di sintaks Kotlin sedekat mungkin.


vs.


Tautan GitHub untuk yang tidak sabar


Tujuan kami adalah untuk membantu programmer menangkap bagian kesalahan tertentu pada tahap kompilasi. Kotlin, sebagai bahasa yang diketik sangat, akan membantu kami untuk menghilangkan ekspresi yang tidak valid dalam struktur query SQL. Sebagai bonus, kami akan mendapatkan lebih banyak perlindungan kesalahan ketik dan bantuan dari IDE dalam menulis permintaan. Tidak mungkin untuk memperbaiki kelemahan SQL sepenuhnya, tetapi sangat mungkin untuk memperbaiki beberapa area masalah.


Artikel ini akan memberi tahu Anda tentang perpustakaan Kotlin, yang memungkinkan Anda menulis kueri SQL dalam sintaksis Kotlin. Kami juga melihat bagian dalam perpustakaan untuk memahami cara kerjanya.


Sedikit teori


SQL singkatan dari Structured Query Language, mis. struktur kueri hadir, meskipun sintaksinya buruk - bahasa dibuat sehingga dapat digunakan oleh pengguna yang bahkan tidak memiliki keterampilan pemrograman.


Namun, di bawah SQL terletak fondasi yang agak kuat dalam bentuk teori database relasional - semuanya sangat logis di sana. Untuk memahami struktur kueri, kita beralih ke pilihan sederhana:


SELECT id, name --  (projection), π(id, name) FROM employees --  (table) WHERE organization_id = 1 --    (predicate), σ(organization_id = 1) 

Yang penting dipahami: permintaan terdiri dari tiga bagian berturut-turut. Masing-masing bagian ini, pertama, tergantung pada yang sebelumnya, dan kedua, menyiratkan serangkaian ekspresi terbatas untuk melanjutkan permintaan. Bahkan, itu tidak sepenuhnya: ekspresi FROM di sini jelas utama dalam kaitannya dengan SELECT, karena kumpulan bidang apa yang dapat kita pilih tergantung pada tabel dari mana pemilihan dilakukan, tetapi tidak sebaliknya.


SQL


Porting ke Kotlin


Jadi, FROM adalah yang utama sehubungan dengan konstruksi bahasa permintaan lainnya. Dari ungkapan ini semua opsi yang memungkinkan untuk melanjutkan permintaan muncul. Di Kotlin, kami merefleksikan ini melalui fungsi from (T), yang akan mengambil objek input, yang merupakan tabel yang memiliki seperangkat kolom.


 object Employees : Table("employees") { val id = Column("id") val name = Column("name") val organizationId = Column("organization_id") } 

Fungsi akan mengembalikan objek yang berisi metode yang mencerminkan kemungkinan kelanjutan dari permintaan. Konstruk dari selalu datang terlebih dahulu, sebelum ekspresi lain, sehingga melibatkan sejumlah besar ekstensi, termasuk SELECT akhir (sebagai lawan dari SQL, di mana SELECT selalu datang sebelum DARI). Kode yang setara dengan kueri SQL di atas akan terlihat seperti ini:


 from(Employees) .where { e -> e.organizationId eq 1 } .select { e -> e.id .. e.name } 

Menariknya, dengan cara ini kita dapat mencegah SQL tidak valid bahkan pada waktu kompilasi. Setiap ekspresi, setiap pemanggilan metode dalam rantai menyiratkan sejumlah ekstensi. Kami dapat mengontrol validitas permintaan menggunakan bahasa Kotlin. Sebagai contoh, di mana ekspresi tidak menyiratkan kelanjutan dalam bentuk yang lain di mana dan, apalagi, dari, tetapi groupBy, memiliki, orderBy, limit, offset, dan konstruksi pilih akhir semuanya valid.


Kuery


Lambdas diteruskan sebagai argumen ke mana dan pernyataan tertentu dirancang untuk membangun predikat dan proyeksi, masing-masing (kami sebutkan sebelumnya). Sebuah tabel diteruskan ke input lambda sehingga Anda dapat mengakses kolom. Penting bahwa keselamatan tipe dipertahankan pada level ini juga - dengan bantuan operator yang berlebihan, kami dapat memastikan bahwa predikat pada akhirnya akan menjadi ekspresi pseudo-Boolean yang tidak dapat dikompilasi jika ada kesalahan sintaks atau kesalahan terkait jenis. Hal yang sama berlaku untuk proyeksi.


 fun where(predicate: (T) -> Predicate): WhereClause<T> fun select(projection: (T) -> Iterable<Projection>): SelectStatement<T> 

Bergabunglah


Database relasional memungkinkan Anda untuk bekerja dengan banyak tabel dan hubungan di antara mereka. Akan menyenangkan untuk memberi pengembang kesempatan untuk bekerja dengan BERGABUNG di perpustakaan kami. Untungnya, model relasional cocok dengan semua yang dijelaskan sebelumnya - Anda hanya perlu menambahkan metode join, yang akan menambahkan tabel kedua ke ekspresi kami.


 fun <T2: Table> join(table2: T2): JoinClause<T, T2> 

BERGABUNG, dalam hal ini, akan memiliki metode yang mirip dengan yang disediakan oleh ekspresi FROM, dengan satu-satunya perbedaan adalah bahwa proyeksi dan predikat lambda akan mengambil dua parameter masing-masing untuk dapat mengakses kolom dari kedua tabel.


 from(Employees) .join(Organizations).on { e, o -> o.id eq e.organizationId } .where { e, o -> e.organizationId eq 1 } .select { e, o -> e.id .. e.name .. o.name } 

Manajemen data


Bahasa manipulasi data adalah alat bahasa SQL yang, di samping tabel kueri, memungkinkan Anda untuk menyisipkan, memodifikasi, dan menghapus data. Desain ini sangat cocok dengan model kami. Untuk mendukung pembaruan dan penghapusan, kita hanya perlu menambahkan dari dan tempat ekspresi dengan varian dengan panggilan metode yang sesuai. Untuk mendukung penyisipan, kami memperkenalkan fungsi tambahan ke dalam.


 from(Employees) .where { e -> e.id eq 1 } .update { e -> e.name("John Doe") } from(Employees) .where { e -> e.id eq 0 } .delete() into(Employees) .insert { e -> e.name("John Doe") .. e.organizationId(1) } 

Deskripsi Data


SQL berfungsi dengan data terstruktur dalam bentuk tabel. Tabel membutuhkan deskripsi sebelum bekerja dengannya. Bagian bahasa ini disebut Bahasa definisi data.


Pernyataan CREATE TABLE dan DROP TABLE diimplementasikan dengan cara yang sama - fungsi over akan berfungsi sebagai titik awal.


 over(Employees) .create { integer(it.id).primaryKey(autoIncrement = true).. text(it.name).unique().notNull().. integer(it.organizationId).foreignKey(references = Organizations.id) } 

 over(Employees).drop() 

Source: https://habr.com/ru/post/id414483/


All Articles