Leitfaden für das Kotlin Exposed Framework

Leitfaden zum Kotlin Exposed Framework

1. Einführung

In diesem Tutorial erfahren Sie, wie Sie eine relationale Datenbank mitExposed abfragen.

Exposed ist eine von JetBrains entwickelte Open-Source-Bibliothek (Apache-Lizenz), die eine idiomatische Kotlin-API für einige relationale Datenbankimplementierungen bereitstellt und gleichzeitig die Unterschiede zwischen Datenbankanbietern ausgleicht.

Exposed can be used both as a high-level DSL over SQL and as a lightweight ORM (Object-Relational Mapping). Daher werden wir im Verlauf dieses Tutorials beide Verwendungen behandeln.

2. Exposed Framework Setup

Exposed ist noch nicht in Maven Central verfügbar, daher müssen wir ein dediziertes Repository verwenden:


    
        exposed
        exposed
        https://dl.bintray.com/kotlin/exposed
    

Dann können wir die Bibliothek aufnehmen:


    org.jetbrains.exposed
    exposed
    0.10.4

In den folgenden Abschnitten werden Beispiele für die Verwendung der H2-Datenbank im Speicher gezeigt:


    com.h2database
    h2
    1.4.197

Wir können die neueste Version vonExposed auf Bintray und die neueste Version vonH2 auf Maven Central finden.

3. Verbindung zur Datenbank herstellen

Wir definieren Datenbankverbindungen mit der KlasseDatabase:

Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")

Wir können auch einuser und einpassword als benannte Parameter angeben:

Database.connect(
  "jdbc:h2:mem:test", driver = "org.h2.Driver",
  user = "myself", password = "secret")

Note that invoking connect doesn’t establish a connection to the DB right away. Es werden nur die Verbindungsparameter für später gespeichert.

3.1. Zusätzliche Parameter

Wenn wir andere Verbindungsparameter angeben müssen, verwenden wir eine andere Überladung derconnect-Methode, mit der wir die vollständige Kontrolle über den Erwerb einer Datenbankverbindung haben:

Database.connect({ DriverManager.getConnection("jdbc:h2:mem:test;MODE=MySQL") })

Diese Version vonconnect erfordert einen Abschlussparameter. Exposed invokes the closure whenever it needs a new connection to the database.

3.2. Verwenden vonDataSource

Wenn wir stattdessen mitDataSource eine Verbindung zur Datenbank herstellen, wie dies normalerweise in Unternehmensanwendungen der Fall ist (z. B. um vom Verbindungspooling zu profitieren), können wir die entsprechende Überlastung vonconnect verwenden:

Database.connect(datasource)

4. Öffnen einer Transaktion

Jede Datenbankoperation in Exposed benötigt eine aktive Transaktion.

Die Methodetransaction schließt und ruft sie mit einer aktiven Transaktion auf:

transaction {
    //Do cool stuff
}

Dastransaction kehrt zurück, unabhängig davon, was der Abschluss zurückgibt. Then, Exposed automatically closes the transaction when the execution of the block terminates.

4.1. Commit und Rollback

Wenn dertransaction block erfolgreich zurückgegeben wird, schreibt Exposed die Transaktion fest. Wenn der Abschluss stattdessen durch Auslösen einer Ausnahme beendet wird, setzt das Framework die Transaktion zurück.

Wir können eine Transaktion auch manuell festschreiben oder zurücksetzen. Der Abschluss, den wirtransaction zur Verfügung stellen, ist dank Kotlin-Magie tatsächlich eine Instanz derTransaction-Klasse.

Wir haben also einecommit- und einerollback-Methode zur Verfügung:

transaction {
    //Do some stuff
    commit()
    //Do other stuff
}

4.2. Protokollierungsanweisungen

Beim Erlernen des Frameworks oder beim Debuggen kann es hilfreich sein, die SQL-Anweisungen und Abfragen zu überprüfen, die Exposed an die Datenbank sendet.

Wir können der aktiven Transaktion einfach einen solchen Logger hinzufügen:

transaction {
    addLogger(StdOutSqlLogger)
    //Do stuff
}

5. Tabellen definieren

Normalerweise arbeiten wir in Exposed nicht mit SQL-Zeichenfolgen und -Namen. Instead, we define tables, columns, keys, relationships, etc., using a high-level DSL.

Wir repräsentieren jede Tabelle mit einer Instanz der KlasseTable:

object StarWarsFilms : Table()

Exposed berechnet automatisch den Namen der Tabelle aus dem Klassennamen. Wir können jedoch auch einen expliziten Namen angeben:

object StarWarsFilms : Table("STAR_WARS_FILMS")

5.1. Säulen

Eine Tabelle ist ohne Spalten bedeutungslos. Wir definieren Spalten als Eigenschaften unserer Tabellenklasse:

object StarWarsFilms : Table() {
    val id = integer("id").autoIncrement().primaryKey()
    val sequelId = integer("sequel_id").uniqueIndex()
    val name = varchar("name", 50)
    val director = varchar("director", 50)
}

Wir haben die Typen der Kürze halber weggelassen, da Kotlin sie für uns ableiten kann. Wie auch immer, jede Spalte ist vom TypColumn<T> und hat einen Namen, einen Typ und möglicherweise Typparameter.

5.2. Primärschlüssel

Wie wir aus dem Beispiel im vorherigen Abschnitt sehen können, istwe can easily define indexes and primary keys with a fluent API.

Für den allgemeinen Fall einer Tabelle mit einem ganzzahligen Primärschlüssel stellt Exposed jedoch die KlassenIntIdTable undLongIdTable bereit, die den Schlüssel für uns definieren:

object StarWarsFilms : IntIdTable() {
    val sequelId = integer("sequel_id").uniqueIndex()
    val name = varchar("name", 50)
    val director = varchar("director", 50)
}

Es gibt auch einUUIDTable;, außerdem können wir unsere eigenen Varianten definieren, indem wirIdTable. unterordnen

5.3. Fremde Schlüssel

Fremdschlüssel sind einfach einzuführen. Wir profitieren auch von der statischen Typisierung, da wir immer auf Eigenschaften verweisen, die zum Zeitpunkt der Kompilierung bekannt sind.

Angenommen, wir möchten die Namen der Schauspieler in jedem Film nachverfolgen:

object Players : Table() {
    val sequelId = integer("sequel_id")
      .uniqueIndex()
      .references(StarWarsFilms.sequelId)
    val name = varchar("name", 50)
}

Um zu vermeiden, dass der Typ der Spalte (in diesem Fallinteger) buchstabiert werden muss, wenn er aus der referenzierten Spalte abgeleitet werden kann, können wir die Methodereference als Kurzform verwenden:

val sequelId = reference("sequel_id", StarWarsFilms.sequelId).uniqueIndex()

Wenn sich der Verweis auf den Primärschlüssel bezieht, können wir den Namen der Spalte weglassen:

val filmId = reference("film_id", StarWarsFilms)

5.4. Tabellen erstellen

Wir können die Tabellen wie oben definiert programmgesteuert erstellen:

transaction {
    SchemaUtils.create(StarWarsFilms, Players)
    //Do stuff
}

Die Tabellen werden nur erstellt, wenn sie noch nicht vorhanden sind. Es gibt jedoch keine Unterstützung für Datenbankmigrationen.

6. Abfragen

Sobald wir einige Tabellenklassen definiert haben, wie in den vorherigen Abschnitten gezeigt, können wir mithilfe der vom Framework bereitgestellten Erweiterungsfunktionen Abfragen an die Datenbank senden.

6.1. Wählen Sie Alle

Um Daten aus der Datenbank zu extrahieren, verwenden wirQuery Objekte, die aus Tabellenklassen erstellt wurden. Die einfachste Abfrage gibt alle Zeilen einer bestimmten Tabelle zurück:

val query = StarWarsFilms.selectAll()

Eine Abfrage ist einIterable,, daher unterstützt sieforEach:

query.forEach {
    assertTrue { it[StarWarsFilms.sequelId] >= 7 }
}

Der Abschlussparameter, der im obigen Beispiel implizit alsit bezeichnet wird, ist eine Instanz der KlasseResultRow. Wir können es als eine Karte sehen, die nach Spalten geordnet ist.

6.2. Auswählen einer Teilmenge von Spalten

Wir können auch eine Teilmenge der Spalten der Tabelle auswählen, d. H. Eine Projektion mit der Methodeslicedurchführen:

StarWarsFilms.slice(StarWarsFilms.name, StarWarsFilms.director).selectAll()
  .forEach {
      assertTrue { it[StarWarsFilms.name].startsWith("The") }
  }

Wir verwendenslice, um eine Funktion auch auf eine Spalte anzuwenden:

StarWarsFilms.slice(StarWarsFilms.name.countDistinct())

Wenn Sie Aggregatfunktionen wiecount undavg, verwenden, benötigen wir häufig eine group by-Klausel in der Abfrage. Wir werden in Abschnitt 6.5 über die Gruppe sprechen.

6.3. Filtern mit Where-Ausdrücken

Exposed contains a dedicated DSL for where expressions, die zum Filtern von Abfragen und anderen Arten von Anweisungen verwendet werden. Dies ist eine Minisprache, die auf den Spalteneigenschaften basiert, auf die wir zuvor gestoßen sind, sowie auf einer Reihe von Booleschen Operatoren.

Dies ist ein where-Ausdruck:

{ (StarWarsFilms.director like "J.J.%") and (StarWarsFilms.sequelId eq 7) }

Sein Typ ist komplex; Es handelt sich um eine Unterklasse vonSqlExpressionBuilder, die Operatoren wielike, eq, and definiert. Wie wir sehen können, handelt es sich um eine Folge von Vergleichen, die mit den Operatorenand undor kombiniert werden.

Wir können einen solchen Ausdruck an die Methodeselectübergeben, die wiederum eine Abfrage zurückgibt:

val select = StarWarsFilms.select { ... }
assertEquals(1, select.count())

Dank der Typinferenz müssen wir den komplexen Typ des where-Ausdrucks nicht buchstabieren, wenn er wie im obigen Beispiel direkt an dieselect-Methode übergeben wird.

Since where expressions are Kotlin objects, there are no special provisions for query parameters. Wir verwenden einfach Variablen:

val sequelNo = 7
StarWarsFilms.select { StarWarsFilms.sequelId >= sequelNo }

6.4. Erweiterte Filterung

Die vonselect und seinen Varianten zurückgegebenenQuery-Objekte verfügen über eine Reihe von Methoden, mit denen wir die Abfrage verfeinern können.

Beispielsweise möchten wir möglicherweise doppelte Zeilen ausschließen:

query.withDistinct(true).forEach { ... }

Oder wir möchten möglicherweise nur eine Teilmenge der Zeilen zurückgeben, beispielsweise beim Paginieren der Ergebnisse für die Benutzeroberfläche:

query.limit(20, offset = 40).forEach { ... }

Diese Methoden geben ein neuesQuery zurück, sodass wir sie leicht verketten können.

6.5. Bestellen nach und Gruppieren nach

DieQuery.orderBy -Smethod akzeptiert eine Liste von Spalten, die einemSortOrder-Wert zugeordnet sind, und gibt an, ob die Sortierung aufsteigend oder absteigend erfolgen soll:

query.orderBy(StarWarsFilms.name to SortOrder.ASC)

Während die Gruppierung nach einer oder mehreren Spalten, insbesondere bei Verwendung von Aggregatfunktionen (siehe Abschnitt 6.2.), Mit der MethodegroupBy erreicht wird:

StarWarsFilms
  .slice(StarWarsFilms.sequelId.count(), StarWarsFilms.director)
  .selectAll()
  .groupBy(StarWarsFilms.director)

6.6. Tritt bei

Joins sind wohl eines der Verkaufsargumente relationaler Datenbanken. In den einfachsten Fällen, wenn wir einen Fremdschlüssel und keine Verknüpfungsbedingungen haben, können wir einen der integrierten Verknüpfungsoperatoren verwenden:

(StarWarsFilms innerJoin Players).selectAll()

Hier haben wirinnerJoin gezeigt, aber wir haben auch Links-, Rechts- und Kreuzverknüpfungen nach dem gleichen Prinzip verfügbar.

Dann können wir Verknüpfungsbedingungen mit einem where-Ausdruck hinzufügen. Wenn beispielsweise kein Fremdschlüssel vorhanden ist und der Join explizit ausgeführt werden muss:

(StarWarsFilms innerJoin Players)
  .select { StarWarsFilms.sequelId eq Players.sequelId }

Im Allgemeinen lautet die vollständige Form eines Joins wie folgt:

val complexJoin = Join(
  StarWarsFilms, Players,
  onColumn = StarWarsFilms.sequelId, otherColumn = Players.sequelId,
  joinType = JoinType.INNER,
  additionalConstraint = { StarWarsFilms.sequelId eq 8 })
complexJoin.selectAll()

6.7. Aliasing

Dank der Zuordnung von Spaltennamen zu Eigenschaften benötigen wir in einem typischen Join kein Aliasing, selbst wenn die Spalten zufällig denselben Namen haben:

(StarWarsFilms innerJoin Players)
  .selectAll()
  .forEach {
      assertEquals(it[StarWarsFilms.sequelId], it[Players.sequelId])
  }

Tatsächlich sind im obigen BeispielStarWarsFilms.sequelId undPlayers.sequelId unterschiedliche Spalten.

Wenn dieselbe Tabelle in einer Abfrage jedoch mehr als einmal vorkommt, möchten wir ihr möglicherweise einen Alias ​​zuweisen. Dafür verwenden wir die Funktionalias:

val sequel = StarWarsFilms.alias("sequel")

Wir können den Alias ​​dann ein bisschen wie eine Tabelle verwenden:

Join(StarWarsFilms, sequel,
  additionalConstraint = {
      sequel[StarWarsFilms.sequelId] eq StarWarsFilms.sequelId + 1
  }).selectAll().forEach {
      assertEquals(
        it[sequel[StarWarsFilms.sequelId]], it[StarWarsFilms.sequelId] + 1)
  }

Im obigen Beispiel sehen wir, dass der Aliassequeleine Tabelle ist, die an einem Join teilnimmt. Wenn wir auf eine der Spalten zugreifen möchten, verwenden wir die Spalte der Alias-Tabelle als Schlüssel:

sequel[StarWarsFilms.sequelId]

7. Aussagen

Nachdem wir nun gesehen haben, wie die Datenbank abgefragt wird, schauen wir uns an, wie DML-Anweisungen ausgeführt werden.

7.1. Daten einfügen

To insert data, we call one of the variants of the insert function. Alle Varianten werden geschlossen:

StarWarsFilms.insert {
    it[name] = "The Last Jedi"
    it[sequelId] = 8
    it[director] = "Rian Johnson"
}

Es gibt zwei bemerkenswerte Objekte, die an der Schließung oben beteiligt sind:

  • this (der Abschluss selbst) ist eine Instanz der KlasseStarWarsFilms; Aus diesem Grund können wir über ihren nicht qualifizierten Namen auf die Spalten zugreifen, bei denen es sich um Eigenschaften handelt

  • it (der Abschlussparameter) ist einInsertStatement; it ist eine kartenartige Struktur mit einem Schlitz für jede einzufügende Spalte

7.2. Extrahieren von Spaltenwerten mit automatischer Inkrementierung

Wenn wir eine insert-Anweisung mit automatisch generierten Spalten haben (normalerweise automatisch inkrementiert oder mit Sequenzen), möchten wir möglicherweise die generierten Werte erhalten.

Im typischen Fall haben wir nur einen generierten Wert und rufeninsertAndGetId: auf

val id = StarWarsFilms.insertAndGetId {
    it[name] = "The Last Jedi"
    it[sequelId] = 8
    it[director] = "Rian Johnson"
}
assertEquals(1, id.value)

Wenn wir mehr als einen generierten Wert haben, können wir sie nach Namen lesen:

val insert = StarWarsFilms.insert {
    it[name] = "The Force Awakens"
    it[sequelId] = 7
    it[director] = "J.J. Abrams"
}
assertEquals(2, insert[StarWarsFilms.id]?.value)

7.3. Daten aktualisieren

Wir können jetzt das, was wir über Abfragen und Einfügungen gelernt haben, verwenden, um vorhandene Daten in der Datenbank zu aktualisieren. Indeed, a simple update looks like a combination of a select with an insert:

StarWarsFilms.update ({ StarWarsFilms.sequelId eq 8 }) {
    it[name] = "Episode VIII – The Last Jedi"
}

Wir können die Verwendung eines where-Ausdrucks in Kombination mit einemUpdateStatement-Abschluss sehen. Tatsächlich teilen sichUpdateStatement undInsertStatement den größten Teil der API und Logik über eine gemeinsame Oberklasse,UpdateBuilder,, die die Möglichkeit bietet, den Wert einer Spalte mithilfe idiomatischer eckiger Klammern festzulegen.

Wenn wir eine Spalte aktualisieren müssen, indem wir einen neuen Wert aus dem alten Wert berechnen, nutzen wir dieSqlExpressionBuilder:

StarWarsFilms.update ({ StarWarsFilms.sequelId eq 8 }) {
    with(SqlExpressionBuilder) {
        it.update(StarWarsFilms.sequelId, StarWarsFilms.sequelId + 1)
    }
}

Dies ist ein Objekt, das Infix-Operatoren (wieplus,minus usw.) bereitstellt, mit denen wir eine Aktualisierungsanweisung erstellen können.

7.4. Daten löschen

Schließlich können wir Daten mit der MethodedeleteWhere löschen:

StarWarsFilms.deleteWhere ({ StarWarsFilms.sequelId eq 8 })

8. Die DAO-API, ein Lightweight-ORM

Bisher haben wir Exposed verwendet, um Operationen von Kotlin-Objekten direkt SQL-Abfragen und -Anweisungen zuzuordnen. Jeder Methodenaufruf wieinsert, update, select usw. führt dazu, dass eine SQL-Zeichenfolge sofort an die Datenbank gesendet wird.

Exposed verfügt jedoch auch über eine übergeordnete DAO-API, die ein einfaches ORM darstellt. Lassen Sie uns jetzt darauf eingehen.

8.1. Entitäten

In den vorherigen Abschnitten haben wir Klassen verwendet, um Datenbanktabellen darzustellen und Operationen mit statischen Methoden darüber auszudrücken.

Wenn Sie einen Schritt weiter gehen, können Sie Entitäten basierend auf den Tabellenklassen definieren, bei denen jede Instanz einer Entität eine Datenbankzeile darstellt:

class StarWarsFilm(id: EntityID) : Entity(id) {
    companion object : EntityClass(StarWarsFilms)

    var sequelId by StarWarsFilms.sequelId
    var name     by StarWarsFilms.name
    var director by StarWarsFilms.director
}

Lassen Sie uns nun die obige Definition Stück für Stück analysieren.

In der ersten Zeile sehen wir, dass eine Entität eine Klasse ist, dieEntity erweitert. Es hat eine ID mit einem bestimmten Typ, in diesem FallInt.

class StarWarsFilm(id: EntityID) : Entity(id) {

Dann stoßen wir auf eine Begleitobjektdefinition. The companion object represents the entity class, that is, the static metadata defining the entity and the operations we can perform on it.

Darüber hinaus verbinden wir in der Deklaration des Begleitobjekts die EntitätStarWarsFilm – Singular, da sie eine einzelne Zeile darstellt, mit der TabelleStarWarsFilms - Plural, da sie die Sammlung darstellt aller Zeilen.

companion object : EntityClass(StarWarsFilms)

Schließlich haben wir die Eigenschaften, die als Eigenschaftsdelegierte in die entsprechenden Tabellenspalten implementiert sind.

var sequelId by StarWarsFilms.sequelId
var name     by StarWarsFilms.name
var director by StarWarsFilms.director

Beachten Sie, dass wir zuvor die Spalten mitval deklariert haben, da sie unveränderliche Metadaten sind. Stattdessen deklarieren wir jetzt die Entitätseigenschaften mitvar,, da es sich um veränderbare Slots in einer Datenbankzeile handelt.

8.2. Daten einfügen

Um eine Zeile in eine Tabelle einzufügen, erstellen wir einfach eine neue Instanz unserer Entitätsklasse mit der statischen Factory-Methodenew in einer Transaktion:

val theLastJedi = StarWarsFilm.new {
    name = "The Last Jedi"
    sequelId = 8
    director = "Rian Johnson"
}

Note that operations against the database are performed lazily; they’re only issued when the warm cache is flushed. Zum Vergleich nennt Hibernate den Warm-Cachesession.

Dies geschieht bei Bedarf automatisch. Wenn wir zum ersten Mal den generierten Bezeichner lesen, führt Exposed im Hintergrund die Anweisung insert aus:

assertEquals(1, theLastJedi.id.value) //Reading the ID causes a flush

Vergleichen Sie dieses Verhalten mit der Methodeinsertaus Abschnitt 7.1., Die sofort eine Anweisung für die Datenbank ausgibt. Hier arbeiten wir auf einer höheren Abstraktionsebene.

8.3. Objekte aktualisieren und löschen

Um eine Zeile zu aktualisieren, weisen wir sie einfach ihren Eigenschaften zu:

theLastJedi.name = "Episode VIII – The Last Jedi"

Um ein Objekt zu löschen, rufen wirdelete auf:

theLastJedi.delete()

Wie beinew werden die Aktualisierung und die Operationen träge ausgeführt.

Updates and deletions can only be performed on a previously loaded object. Es gibt keine API für massive Updates und Löschungen. Stattdessen müssen wir die untergeordnete API verwenden, die wir in Abschnitt 7 gesehen haben. Trotzdem können die beiden APIs in derselben Transaktion zusammen verwendet werden.

8.4. Abfragen

Mit der DAO-API können drei Arten von Abfragen durchgeführt werden.

Um alle Objekte ohne Bedingungen zu laden, verwenden wir die statische Methodeall:

val movies = StarWarsFilm.all()

Um ein einzelnes Objekt nach ID zu laden, rufen wirfindById: auf

val theLastJedi = StarWarsFilm.findById(1)

Wenn es kein Objekt mit dieser ID gibt, gibtfindByIdnull. zurück

Schließlich verwenden wir im allgemeinen Fallfind mit einem where-Ausdruck:

val movies = StarWarsFilm.find { StarWarsFilms.sequelId eq 8 }

8.5. Viele-zu-Eins-Vereinigungen

So wie Joins ein wichtiges Merkmal relationaler Datenbanken sind,the mapping of joins to references is an important aspect of an ORM. Lassen Sie uns also sehen, was Exposed zu bieten hat.

Angenommen, wir möchten die Bewertung jedes Films durch die Benutzer verfolgen. Zunächst definieren wir zwei zusätzliche Tabellen:

object Users: IntIdTable() {
    val name = varchar("name", 50)
}

object UserRatings: IntIdTable() {
    val value = long("value")
    val film = reference("film", StarWarsFilms)
    val user = reference("user", Users)
}

Dann schreiben wir die entsprechenden Entitäten. Lassen Sie die triviale EntitätUserweg und wechseln Sie direkt zur KlasseUserRating:

class UserRating(id: EntityID): IntEntity(id) {
    companion object : IntEntityClass(UserRatings)

    var value by UserRatings.value
    var film  by StarWarsFilm referencedOn UserRatings.film
    var user  by User         referencedOn UserRatings.user
}

In particular, note the referencedOn infix method call on properties that represent associations. Das Muster ist das folgende: avar Deklaration,by die referenzierte Entität,referencedOn die referenzierende Spalte.

Eigenschaften, die auf diese Weise deklariert wurden, verhalten sich wie normale Eigenschaften, aber ihr Wert ist das zugehörige Objekt:

val someUser = User.new {
    name = "Some User"
}
val rating = UserRating.new {
    value = 9
    user = someUser
    film = theLastJedi
}
assertEquals(theLastJedi, rating.film)

8.6. Optionale Zuordnungen

Die Assoziationen, die wir im vorherigen Abschnitt gesehen haben, sind obligatorisch, dh wir müssen immer einen Wert angeben.

Wenn wir eine optionale Zuordnung wünschen, müssen wir zuerst die Spalte in der Tabelle als nullbar deklarieren:

val user = reference("user", Users).nullable()

Dann verwenden wiroptionalReferencedOn anstelle vonreferencedOn in der Entität:

var user by User optionalReferencedOn UserRatings.user

Auf diese Weise kann die Eigenschaftuserauf Null gesetzt werden.

8.7. Eins-zu-viele-Assoziationen

Möglicherweise möchten wir auch die gegenüberliegende Seite des Vereins abbilden. Bei einer Bewertung handelt es sich um einen Film. Dies modellieren wir in der Datenbank mit einem Fremdschlüssel. Folglich hat ein Film eine Reihe von Bewertungen.

Um die Bewertungen eines Films abzubilden, fügen wir einfach eine Eigenschaft zur "einen" Seite der Assoziation hinzu, dh zur Filmeinheit in unserem Beispiel:

class StarWarsFilm(id: EntityID) : Entity(id) {
    //Other properties elided
    val ratings  by UserRating referrersOn UserRatings.film
}

Das Muster ähnelt dem von Viele-zu-Eins-Beziehungen, verwendet jedochreferrersOn.. Die so definierte Eigenschaft istIterable,, sodass wir es mitforEach: durchlaufen können

theLastJedi.ratings.forEach { ... }

Beachten Sie, dass wir im Gegensatz zu regulären Eigenschaftenratings mitval.Indeed, the property is immutable, we can only read it. definiert haben

Der Wert der Eigenschaft hat auch keine API für die Mutation. Um eine neue Bewertung hinzuzufügen, müssen wir sie mit einem Verweis auf den Film erstellen:

UserRating.new {
    value = 8
    user = someUser
    film = theLastJedi
}

Dann enthält dieratings-Liste des Films die neu hinzugefügte Bewertung.

8.8. Viele-zu-viele-Verbände

In einigen Fällen benötigen wir möglicherweise eine Viele-zu-Viele-Assoziation. Angenommen, wir möchten der KlasseStarWarsFilmeinen Verweis auf eineActors-Tabelle hinzufügen:

object Actors: IntIdTable() {
    val firstname = varchar("firstname", 50)
    val lastname = varchar("lastname", 50)
}

class Actor(id: EntityID): IntEntity(id) {
    companion object : IntEntityClass(Actors)

    var firstname by Actors.firstname
    var lastname by Actors.lastname
}

Nachdem wir die Tabelle und die Entität definiert haben, benötigen wir eine weitere Tabelle, um die Assoziation darzustellen:

object StarWarsFilmActors : Table() {
    val starWarsFilm = reference("starWarsFilm", StarWarsFilms).primaryKey(0)
    val actor = reference("actor", Actors).primaryKey(1)
}

Die Tabelle enthält zwei Spalten, die beide Fremdschlüssel sind und auch einen zusammengesetzten Primärschlüssel bilden.

Schließlich können wir die Zuordnungstabelle mit der EntitätStarWarsFilmverbinden:

class StarWarsFilm(id: EntityID) : IntEntity(id) {
    companion object : IntEntityClass(StarWarsFilms)

    //Other properties elided
    var actors by Actor via StarWarsFilmActors
}

Zum Zeitpunkt des Schreibens ist es nicht möglich, eine Entität mit einer generierten Kennung zu erstellen und diese in eine Viele-zu-Viele-Zuordnung in derselben Transaktion aufzunehmen.

In der Tat müssen wir mehrere Transaktionen verwenden:

//First, create the film
val film = transaction {
   StarWarsFilm.new {
    name = "The Last Jedi"
    sequelId = 8
    director = "Rian Johnson"r
  }
}
//Then, create the actor
val actor = transaction {
  Actor.new {
    firstname = "Daisy"
    lastname = "Ridley"
  }
}
//Finally, link the two together
transaction {
  film.actors = SizedCollection(listOf(actor))
}

Hier haben wir der Einfachheit halber drei verschiedene Transaktionen verwendet. Zwei wären jedoch ausreichend gewesen.

9. Fazit

In diesem Artikel haben wir einen umfassenden Überblick über das Exposed-Framework für Kotlin gegeben. Weitere Informationen und Beispiele finden Sie inExposed wiki.

Die Implementierung all dieser Beispiele und Codefragmente befindet sich inthe GitHub project als Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.