Guide du Kotlin Exposed Framework

Guide du cadre exposé de Kotlin

1. introduction

Dans ce didacticiel, nous allons voir comment interroger une base de données relationnelle à l'aide deExposed.

Exposed est une bibliothèque open source (licence Apache) développée par JetBrains, qui fournit une API Kotlin idiomatique pour certaines implémentations de bases de données relationnelles tout en lissant les différences entre les fournisseurs de bases de données.

Exposed can be used both as a high-level DSL over SQL and as a lightweight ORM (Object-Relational Mapping). Ainsi, nous allons couvrir les deux usages au cours de ce tutoriel.

2. Configuration du cadre exposé

Exposed n'est pas encore sur Maven Central, nous devons donc utiliser un référentiel dédié:


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

Ensuite, nous pouvons inclure la bibliothèque:


    org.jetbrains.exposed
    exposed
    0.10.4

De plus, dans les sections suivantes, nous allons montrer des exemples utilisant la base de données H2 en mémoire:


    com.h2database
    h2
    1.4.197

Nous pouvons trouver la dernière version deExposed sur Bintray et la dernière version deH2 sur Maven Central.

3. Connexion à la base de données

Nous définissons les connexions à la base de données avec la classeDatabase:

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

Nous pouvons également spécifier unuser et unpassword comme paramètres nommés:

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. Il enregistre simplement les paramètres de connexion pour plus tard.

3.1. Paramètres supplémentaires

Si nous devons fournir d'autres paramètres de connexion, nous utiliserons une surcharge différente de la méthodeconnect qui nous donne un contrôle total sur l'acquisition d'une connexion à la base de données:

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

Cette version deconnect nécessite un paramètre de fermeture. Exposed invokes the closure whenever it needs a new connection to the database.

3.2. Utilisation d'unDataSource

Si, à la place, nous nous connectons à la base de données en utilisant unDataSource, comme c'est généralement le cas dans les applications d'entreprise (par exemple, pour bénéficier du pool de connexions), nous pouvons utiliser la surchargeconnect appropriée:

Database.connect(datasource)

4. Ouverture d'une transaction

Chaque opération de base de données dans Exposed nécessite une transaction active.

La méthodetransaction prend une clôture et l'invoque avec une transaction active:

transaction {
    //Do cool stuff
}

Letransaction renvoie tout ce que la fermeture renvoie. Then, Exposed automatically closes the transaction when the execution of the block terminates.

4.1. Validation et restauration

Lorsque le sbloctransaction revient avec succès, Exposed valide la transaction. Lorsque, au lieu de cela, la fermeture se termine en levant une exception, le framework annule la transaction.

Nous pouvons également valider ou annuler manuellement une transaction. La fermeture que nous fournissons àtransaction est en fait une instance de la classeTransaction grâce à la magie de Kotlin.

Ainsi, nous avons une méthodecommit et une méthoderollback disponibles:

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

4.2. Instructions de journalisation

Lors de l'apprentissage du framework ou du débogage, il peut s'avérer utile d'inspecter les instructions SQL et les requêtes envoyées par Exposed à la base de données.

Nous pouvons facilement ajouter un tel enregistreur à la transaction active:

transaction {
    addLogger(StdOutSqlLogger)
    //Do stuff
}

5. Définition des tables

En général, dans Exposed, nous ne travaillons pas avec des chaînes et des noms SQL bruts. Instead, we define tables, columns, keys, relationships, etc., using a high-level DSL.

Nous représentons chaque table avec une instance de la classeTable:

object StarWarsFilms : Table()

Exposed calcule automatiquement le nom de la table à partir du nom de la classe, mais nous pouvons également fournir un nom explicite:

object StarWarsFilms : Table("STAR_WARS_FILMS")

5.1. Colonnes

Un tableau n'a pas de sens sans colonnes. Nous définissons les colonnes en tant que propriétés de notre classe de table:

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)
}

Nous avons omis les types par souci de concision, car Kotlin peut les déduire pour nous. Quoi qu'il en soit, chaque colonne est de typeColumn<T> et elle a un nom, un type et éventuellement des paramètres de type.

5.2. Clés primaires

Comme nous pouvons le voir dans l'exemple de la section précédente,we can easily define indexes and primary keys with a fluent API.

Cependant, pour le cas courant d'une table avec une clé primaire entière, Exposed fournit les classesIntIdTable etLongIdTable qui définissent la clé pour nous:

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

Il y a aussi unUUIDTable; en plus, nous pouvons définir nos propres variantes en sous-classantIdTable.

5.3. Clés étrangères

Les clés étrangères sont faciles à introduire. Nous bénéficions également du typage statique car nous nous référons toujours aux propriétés connues au moment de la compilation.

Supposons que nous voulions suivre les noms des acteurs jouant dans chaque film:

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

Pour éviter d'avoir à épeler le type de la colonne (dans ce cas,integer) lorsqu'elle peut être dérivée de la colonne référencée, nous pouvons utiliser la méthodereference comme raccourci:

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

Si la référence est à la clé primaire, nous pouvons omettre le nom de la colonne:

val filmId = reference("film_id", StarWarsFilms)

5.4. Création de tableaux

Nous pouvons créer les tables telles que définies ci-dessus par programmation:

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

Les tableaux ne sont créés que s'ils n'existent pas déjà. Cependant, les migrations de bases de données ne sont pas prises en charge.

6. Des requêtes

Une fois que nous avons défini certaines classes de table comme nous l'avons montré dans les sections précédentes, nous pouvons émettre des requêtes vers la base de données en utilisant les fonctions d'extension fournies par le framework.

6.1. Tout sélectionner

Pour extraire des données de la base de données, nous utilisons des objetsQuery construits à partir de classes de table. La requête la plus simple est celle qui renvoie toutes les lignes d'une table donnée:

val query = StarWarsFilms.selectAll()

Une requête est unIterable, donc elle prend en chargeforEach:

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

Le paramètre de fermeture, appelé implicitementit dans l'exemple ci-dessus, est une instance de la classeResultRow. Nous pouvons le voir comme une carte indexée par colonne.

6.2. Sélection d'un sous-ensemble de colonnes

Nous pouvons également sélectionner un sous-ensemble des colonnes de la table, c'est-à-dire effectuer une projection, en utilisant la méthodeslice:

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

Nous utilisons égalementslice pour appliquer une fonction à une colonne:

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

Souvent, lorsque vous utilisez des fonctions d'agrégation telles quecount etavg,, nous avons besoin d'une clause group by dans la requête. Nous parlerons du groupe dans la section 6.5.

6.3. Filtrage avec des expressions Where

Exposed contains a dedicated DSL for where expressions, qui sont utilisés pour filtrer les requêtes et autres types d'instructions. Il s'agit d'un mini-langage basé sur les propriétés de colonne que nous avons rencontrées précédemment et sur une série d'opérateurs booléens.

Ceci est une expression où:

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

Son type est complexe; c'est une sous-classe deSqlExpressionBuilder, qui définit des opérateurs tels quelike, eq, and. Comme nous pouvons le voir, il s'agit d'une séquence de comparaisons combinées avec les opérateursand etor.

Nous pouvons passer une telle expression à la méthodeselect, qui renvoie à nouveau une requête:

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

Grâce à l'inférence de type, nous n'avons pas besoin d'épeler le type complexe de l'expression where lorsqu'elle est directement transmise à la méthodeselect comme dans l'exemple ci-dessus.

Since where expressions are Kotlin objects, there are no special provisions for query parameters. Nous utilisons simplement des variables:

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

6.4. Filtrage avancé

Les objetsQuery retournés parselect et ses variantes ont un certain nombre de méthodes que nous pouvons utiliser pour affiner la requête.

Par exemple, nous pourrions vouloir exclure les lignes en double:

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

Ou nous pourrions vouloir ne renvoyer qu'un sous-ensemble des lignes, par exemple lors de la pagination des résultats pour l'interface utilisateur:

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

Ces méthodes renvoient un nouveauQuery, donc nous pouvons facilement les chaîner.

6.5. Trier par et grouper par

La méthodeQuery.orderBy accepte une liste de colonnes mappées à une valeurSortOrder indiquant si le tri doit être croissant ou décroissant:

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

Alors que le regroupement par une ou plusieurs colonnes, utile notamment lors de l'utilisation des fonctions d'agrégation (voir section 6.2.), Est réalisé en utilisant la méthodegroupBy:

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

6.6. Se joint

Les jointures sont sans doute l’un des points de vente des bases de données relationnelles. Dans le plus simple des cas, lorsque nous avons une clé étrangère et aucune condition de jointure, nous pouvons utiliser l'un des opérateurs de jointure intégrés:

(StarWarsFilms innerJoin Players).selectAll()

Ici, nous avons montréinnerJoin, mais nous avons également des jointures gauche, droite et croisée disponibles avec le même principe.

Ensuite, nous pouvons ajouter des conditions de jointure avec une expression where; par exemple, s'il n'y a pas de clé étrangère et que nous devons effectuer la jointure explicitement:

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

Dans le cas général, la forme complète d'une jointure est la suivante:

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

6.7. Aliasing

Grâce au mappage des noms de colonnes aux propriétés, nous n'avons besoin d'aucun alias dans une jointure classique, même lorsque les colonnes portent le même nom:

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

En fait, dans l'exemple ci-dessus,StarWarsFilms.sequelId etPlayers.sequelId sont des colonnes différentes.

Cependant, lorsque la même table apparaît plus d'une fois dans une requête, nous pouvons vouloir lui donner un alias. Pour cela, nous utilisons la fonctionalias:

val sequel = StarWarsFilms.alias("sequel")

On peut alors utiliser l'alias un peu comme une table:

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

Dans l'exemple ci-dessus, nous pouvons voir que l'aliassequel est une table participant à une jointure. Lorsque nous voulons accéder à l'une de ses colonnes, nous utilisons la colonne de la table aliasée comme clé:

sequel[StarWarsFilms.sequelId]

7. Déclarations

Maintenant que nous avons vu comment interroger la base de données, voyons comment exécuter des instructions DML.

7.1. Insertion de données

To insert data, we call one of the variants of the insert function. Toutes les variantes prennent une clôture:

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

Il y a deux objets notables impliqués dans la fermeture ci-dessus:

  • this (la fermeture elle-même) est une instance de la classeStarWarsFilms; c'est pourquoi nous pouvons accéder aux colonnes, qui sont des propriétés, par leur nom non qualifié

  • it (le paramètre de fermeture) est unInsertStatement; it est une structure de type carte avec un emplacement pour chaque colonne à insérer

7.2. Extraction de valeurs de colonne à incrémentation automatique

Lorsque nous avons une instruction insert avec des colonnes générées automatiquement (généralement des incrémentations ou des séquences), nous souhaitons peut-être obtenir les valeurs générées.

Dans le cas typique, nous n'avons qu'une seule valeur générée et nous appelonsinsertAndGetId:

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

Si nous avons plus d'une valeur générée, nous pouvons les lire par nom:

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. Mise à jour des données

Nous pouvons maintenant utiliser ce que nous avons appris sur les requêtes et les insertions pour mettre à jour les données existantes dans la base de données. 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"
}

Nous pouvons voir l'utilisation d'une expression where combinée à une fermetureUpdateStatement. En fait,UpdateStatement etInsertStatement partagent la plupart de l'API et de la logique via une superclasse commune,UpdateBuilder,, qui permet de définir la valeur d'une colonne à l'aide de crochets idiomatiques.

Lorsque nous avons besoin de mettre à jour une colonne en calculant une nouvelle valeur à partir de l'ancienne valeur, nous exploitons lesSqlExpressionBuilder:

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

C'est un objet qui fournit des opérateurs d'infixe (commeplus,minus et ainsi de suite) que nous pouvons utiliser pour construire une instruction de mise à jour.

7.4. Suppression de données

Enfin, nous pouvons supprimer des données avec la méthodedeleteWhere:

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

8. L'API DAO, un ORM léger

Jusqu'à présent, nous avons utilisé Exposed pour mapper directement les opérations sur les objets Kotlin aux requêtes et instructions SQL. Chaque appel de méthode commeinsert, update, select et ainsi de suite entraîne l'envoi immédiat d'une chaîne SQL à la base de données.

Cependant, Exposed possède également une API DAO de niveau supérieur qui constitue un simple ORM. Passons maintenant à cela.

8.1. Entités

Dans les sections précédentes, nous avons utilisé des classes pour représenter des tables de base de données et pour exprimer des opérations sur celles-ci, à l'aide de méthodes statiques.

En allant plus loin, nous pouvons définir des entités en fonction de ces classes de tables, chaque instance d’une entité représentant une ligne de base de données:

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
}

Analysons maintenant la définition ci-dessus pièce par pièce.

Dans la première ligne, nous pouvons voir qu'une entité est une classe étendantEntity. Il a un ID avec un type spécifique, dans ce cas,Int.

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

Ensuite, nous rencontrons une définition d'objet compagnon. The companion object represents the entity class, that is, the static metadata defining the entity and the operations we can perform on it.

De plus, dans la déclaration de l'objet compagnon, nous connectons l'entité,StarWarsFilm – singulier, car elle représente une seule ligne à la table,StarWarsFilms - pluriel, car elle représente la collection de toutes les lignes.

companion object : EntityClass(StarWarsFilms)

Enfin, nous avons les propriétés, implémentées en tant que délégués de propriété aux colonnes de la table correspondante.

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

Notez que précédemment, nous déclarions les colonnes avecval car ce sont des métadonnées immuables. Désormais, nous déclarons les propriétés d'entité avecvar, car ce sont des emplacements modifiables dans une ligne de base de données.

8.2. Insertion de données

Pour insérer une ligne dans une table, nous créons simplement une nouvelle instance de notre classe d'entité en utilisant la méthode de fabrique statiquenew dans une transaction:

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. À titre de comparaison, Hibernate appelle le cache chaud asession.

Cela se produit automatiquement lorsque nécessaire. Par exemple, la première fois que nous lisons l'identificateur généré, Exposed exécute en mode silencieux l'instruction insert:

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

Comparez ce comportement avec la méthodeinsert de la section 7.1., Qui émet immédiatement une instruction contre la base de données. Ici, nous travaillons à un niveau d'abstraction plus élevé.

8.3. Mise à jour et suppression d'objets

Pour mettre à jour une ligne, nous affectons simplement à ses propriétés:

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

Alors que pour supprimer un objet, nous appelonsdelete dessus:

theLastJedi.delete()

Comme pournew, la mise à jour et les opérations sont effectuées paresseusement.

Updates and deletions can only be performed on a previously loaded object. Il n'y a pas d'API pour les mises à jour et les suppressions massives. Au lieu de cela, nous devons utiliser l'API de niveau inférieur que nous avons vue dans la section 7. Néanmoins, les deux API peuvent être utilisées ensemble dans la même transaction.

8.4. Requête

Avec l'API DAO, nous pouvons effectuer trois types de requêtes.

Pour charger tous les objets sans conditions, nous utilisons la méthode statiqueall:

val movies = StarWarsFilm.all()

Pour charger un seul objet par ID, nous appelonsfindById:

val theLastJedi = StarWarsFilm.findById(1)

S'il n'y a pas d'objet avec cet ID,findById renvoienull.

Enfin, dans le cas général, nous utilisonsfind avec une expression where:

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

8.5. Associations plusieurs-à-un

Tout comme les jointures sont une fonctionnalité importante des bases de données relationnelles,the mapping of joins to references is an important aspect of an ORM. Voyons donc ce que Exposed a à offrir.

Supposons que nous voulions suivre le classement de chaque film par les utilisateurs. Tout d'abord, nous définissons deux tables supplémentaires:

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)
}

Ensuite, nous écrirons les entités correspondantes. Oublions l'entitéUser, qui est triviale, et passons directement à la classeUserRating:

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. Le modèle est le suivant: une déclarationvar,by l'entité référencée,referencedOn la colonne de référence.

Les propriétés déclarées de cette manière se comportent comme des propriétés normales, mais leur valeur est l'objet associé:

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

8.6. Associations facultatives

Les associations que nous avons vues dans la section précédente sont obligatoires, c'est-à-dire que nous devons toujours spécifier une valeur.

Si nous voulons une association facultative, nous devons d'abord déclarer la colonne nullable dans la table:

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

Ensuite, nous utiliseronsoptionalReferencedOn au lieu dereferencedOn dans l'entité:

var user by User optionalReferencedOn UserRatings.user

De cette façon, la propriétéuser sera Nullable.

8.7. Associations un-à-plusieurs

Nous pourrions également vouloir cartographier le côté opposé de l'association. Une note concerne un film, c'est ce que nous modélisons dans la base de données avec une clé étrangère; par conséquent, un film a un certain nombre de cotes.

Pour cartographier les cotes d’un film, nous ajoutons simplement une propriété au côté «un» de l’association, c’est-à-dire l’entité du film dans notre exemple:

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

Le modèle est similaire à celui des relations plusieurs à un, mais il utilisereferrersOn. La propriété ainsi définie est unIterable, donc nous pouvons le parcourir avecforEach:

theLastJedi.ratings.forEach { ... }

Notez que, contrairement aux propriétés régulières, nous avons définiratings avecval.Indeed, the property is immutable, we can only read it.

La valeur de la propriété n'a pas non plus d'API pour la mutation. Donc, pour ajouter une nouvelle note, nous devons la créer avec une référence au film:

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

Ensuite, la listeratings du film contiendra la note nouvellement ajoutée.

8.8. Associations plusieurs à plusieurs

Dans certains cas, nous aurons peut-être besoin d'une association plusieurs à plusieurs. Disons que nous voulons ajouter une référence à une tableActors à la classeStarWarsFilm:

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
}

Après avoir défini la table et l'entité, nous avons besoin d'une autre table pour représenter l'association:

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

La table a deux colonnes qui sont à la fois des clés étrangères et qui constituent également une clé primaire composite.

Enfin, nous pouvons connecter la table d'association avec l'entitéStarWarsFilm:

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

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

Au moment de la rédaction de cet article, il n’est pas possible de créer une entité avec un identifiant généré et de l’inclure dans une association plusieurs à plusieurs dans la même transaction.

En fait, nous devons utiliser plusieurs transactions:

//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))
}

Ici, nous avons utilisé trois transactions différentes pour plus de commodité. Cependant, deux auraient suffi.

9. Conclusion

Dans cet article, nous avons donné un aperçu complet du framework Exposed pour Kotlin. Pour plus d'informations et d'exemples, consultez lesExposed wiki.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dansthe GitHub project en tant que projet Maven, il devrait donc être facile à importer et à exécuter tel quel.