Az Exposed is természetesen támogatja a join-ok használatát, de nem minden esetben olyan egyszerű, mint a JPA esetében.
Alapozás
Először az Entityket hozzuk létre. Semmi extra, one-to-many kapcsolat Emberek és Macskák között, ahol egy embernek több macskája is lehet. Remény szerint rengeteg.
object Cats : LongIdTable("Cat") {
val owner = reference("owner", Persons)
val name = varchar("name", length = 50)
val breed = varchar("breed", length = 50)
}
class Cat(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<Cat>(Cats)
val owner by Person referencedOn Cats.owner
var name by Cats.name
var breed by Cats.breed
}
object Persons : LongIdTable("Person") {
val name = varchar("name", length = 50)
}
class Person(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<Person>(Persons)
var name by Persons.name
val cats by Cat referrersOn Cats.owner
}
Itt érdemes megnézni a cats
változót. Látjuk, hogy ez csak a DAO-ban van jelen, tehát nem tartozik hozzá konkrét adatbázis mező. A referrersOn miatt viszont "visszahivatkozik" a Cat táblára, és ezen segítségével (is) tudjuk lekérni az adott személyhez tartozó macskákat.
Hogy valamit kezdjünk is az adatokkal, létrehozunk egy pár faék egyszerűségű dummy metódust "responseok" gyártásához: (Nem csináltam egyedi struktúrát, nyilván, bármibe lehetne mappelni, amit nem szégyellsz…)
fun Person.toResponse() = mapOf(
"id" to id.value,
"name" to name,
"cats" to cats.map { cat -> cat.toResponse() }
)
fun Cat.toResponse() = mapOf(
"id" to id.value,
"name" to name,
"breed" to breed
)
DAO lekérés
Persons.selectAll().map { person -> Person.wrapRow(person).toResponse() }
Itt a DSL segítségével lekérjük az összes személyt, ami "magával rántja" a hozzájuk tartozó macskákat is. A map
segítségével pedig a Person objektumokat átalakítjuk a responseokká.
Probléma:
Egyrészt, ez a megközelítés csak akkor használható, ha van DAO objektumunk (lsd: cats változó). Másrészt, sajnos itt az Exposed nem annyira bölcs, hogy join-t használjon, így minden egyes Person objektumhoz külön lekérdezést fog küldeni a macskákért. Így egy csodás N+1 query problémát kaptunk. Nyilván, kis adatmennyiség esetén nem fogja megfektetni a rendszerünket, de semmiképpen sem nevezném optimálisnak.
Érdemes megjegyezni, hogy ezt a megközelítést pl. nem fogjuk tudni használni olyan tábláknál, amik összetett elsődleges kulccsal rendelkeznek. Lásd: Exposed Composite key problémaExposed Composite key probléma
Bármikor lehetünk olyan szerencsétlenek, hogy egy olyan adatbázis táblával találkozunk, aminek nem egy, hanem több mezőből álló, összetett (composite) elsődleges kulcsa van.
Sajnos az Exposed enn...
Lekérés InnerJoin-nal
Ennél a megoldásnál megírjuk a join-t, majd a map segítségével alakítjuk át a kapott adatokat. Sajnos a JPA-tól eltérően, az Exposed nem tud "ORM-ként", objektum szinten gondolkodni, tehát hiába kérem le a személyeket és a macskákat, nem fogja tudni a személyekhez alábontani a macska listát. Az Exposed ResultRow-ban gondolkodik, tehát teljesen hasonló a helyzet, minta kézzel kiadnánk a selectünket, és a lejött adatokból kézzel hoznánk létre a szükséges sturktúrát.
(Persons innerJoin Cats)
.selectAll()
.groupBy { it[Persons.id] }
.map { group ->
val cats = group.value.map { Cat.wrapRow(it) }
Person.wrapRow(group.value.first()).toResponse(cats)
}
Annyi segítséget kapunk, hogy a wrapRow
segítségével a ResultRow-t átalakíthatjuk a megfelelő DAO objektummá, nem kell egyesével beállítani a mezőket. Mivel azt szeretnénk, hogy a Person objektumokhoz tartozó Cat objektumok listaként alá legyenek bontva, így szükséges egy groupingot alkalmazni. A join-os megoldás használatakor nem elég a joint lefuttatni, a kapott adatokat is wrapelni kell, ezért a Cat objektumot is wrapRow-val hozzuk létre.
Azt nyilvánvalóan nekünk kell eldöntenünk, hogy milyen joint alkalmazunk. Ezzel a megoldással egyelőre nincs probléma. A bonyodalom ezután jön.
Lekérés LeftJoin-nal
Ha az előző példát leftJoin
-ra cseréljük, akkor még mindig nem lesz semmi probléma. Egészen addig, amíg egyszer egy személyhez nem tartozik macska. Ekkor ugyanis a Cat.wrapRow(it)
hívás közben az id
mező null lesz, ami a by-design nem megengedett a kulcsoknál. Következésképp egy méretes NullPointerException
-t kapunk. A problémát ki lehet küszöbölni akkor, ha a Cat.wrapRow(it)
hívás előtt ellenőrizzük, hogy az id
mező nem null-e. Elég fájdalmas, hogy ezt kezelgetni kell, de legalább megoldja a problémát.
(Persons leftJoin Cats)
.selectAll()
.groupBy { it[Persons.id] }
.map { group ->
val cats = group.value.filter { it.getOrNull(Cats.id) != null }.map { Cat.wrapRow(it) }
Person.wrapRow(group.value.first()).toResponse(cats)
}
Már csak annyi hiányzik, hogy a response létrehozáskor a Cat objektumokat is átadjuk. Ezt a toResponse
metódust kell módosítani, hogy paraméterként megkapja a macskákat.
fun Person.toResponse(allCats: List<Cat>) = mapOf(
"id" to id.value,
"name" to name,
"cats" to allCats.map { cat -> cat.toResponse() }
)