Veiledning til Kotlin Exposed Framework

1. Introduksjon

I denne opplæringen skal vi se på hvordan du kan spørre en relasjonsdatabase ved hjelp av Exposed.

Exposed er et åpen kildekodebibliotek (Apache-lisens) utviklet av JetBrains, som gir en idiomatisk Kotlin API for noen relasjonelle databaseimplementeringer samtidig som forskjellene mellom databaseleverandører jevnes ut.

Exposed kan brukes både som et høyt nivå DSL over SQL og som en lett ORM (Object-Relational Mapping). Dermed vil vi dekke begge bruksområdene i løpet av denne opplæringen.

2. Eksponert rammeoppsett

Exposed er ennå ikke på Maven Central, så vi må bruke et dedikert depot:

  utsatt eksponert //dl.bintray.com/kotlin/exposed 

Deretter kan vi inkludere biblioteket:

 org.jetbrains.exposed eksponert 0.10.4 

I de følgende avsnittene viser vi eksempler som bruker H2-databasen i minnet:

 com.h2database h2 1.4.197 

Vi finner den nyeste versjonen av Exposed on Bintray og den siste versjonen av H2 på Maven Central.

3. Koble til databasen

Vi definerer databaseforbindelser med Database klasse:

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

Vi kan også spesifisere en bruker og en passord som navngitte parametere:

Database.connect ("jdbc: h2: mem: test", driver = "org.h2.Driver", bruker = "meg selv", passord = "hemmelig")

Legg merke til at påkalle koble oppretter ikke en forbindelse til DB med en gang. Det lagrer bare tilkoblingsparametrene for senere.

3.1. Tilleggsparametere

Hvis vi trenger å oppgi andre tilkoblingsparametere, bruker vi en annen overbelastning av koble metode som gir oss full kontroll over anskaffelsen av en databaseforbindelse:

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

Denne versjonen av koble krever en lukkingsparameter. Exposed påkaller lukkingen når den trenger en ny forbindelse til databasen.

3.2. Bruker en Datakilde

Hvis vi i stedet kobler til databasen ved hjelp av en Datakilde, som det vanligvis er tilfelle i bedriftsapplikasjoner (for eksempel å dra nytte av tilkoblingssamling), kan vi bruke det riktige koble overbelastning:

Database.connect (datakilde)

4. Åpne en transaksjon

Hver databaseoperasjon i Exposed trenger en aktiv transaksjon.

De transaksjon metoden avslutter og påkaller den med en aktiv transaksjon:

transaksjon {// Gjør kule ting}

De transaksjon returnerer uansett stengingen returnerer. Deretter lukker Exposed automatisk transaksjonen når utførelsen av blokken avsluttes.

4.1. Forplikt og tilbakeslag

Når transaksjon blokken returnerer, Exposed forplikter transaksjonen. Når stengingen i stedet avsluttes ved å kaste et unntak, ruller rammen tilbake transaksjonen.

Vi kan også manuelt begå eller tilbakeføre en transaksjon. Nedleggelsen som vi gir til transaksjon er faktisk en forekomst av Transaksjon klasse takket være Kotlin-magi.

Dermed har vi en begå og en tilbakeføring tilgjengelig metode:

transaksjon {// Gjør noen ting begått () // Gjør andre ting}

4.2. Logging Statements

Når vi lærer rammeverket eller feilsøker, kan det være nyttig å inspisere SQL-setningene og spørringene som Exposed sender til databasen.

Vi kan enkelt legge til en slik logger i den aktive transaksjonen:

transaksjon {addLogger (StdOutSqlLogger) // Gjør ting}

5. Definere tabeller

Vanligvis jobber vi ikke med rå SQL-strenger og navn i Exposed. I stedet definerer vi tabeller, kolonner, nøkler, relasjoner osv. Ved hjelp av et høyt nivå DSL.

Vi representerer hver tabell med en forekomst av Bord klasse:

objekt StarWarsFilms: Tabell ()

Exposed beregner automatisk navnet på tabellen fra kursnavnet, men vi kan også oppgi et eksplisitt navn:

objekt StarWarsFilms: Tabell ("STAR_WARS_FILMS")

5.1. Kolonner

En tabell er meningsløs uten kolonner. Vi definerer kolonner som egenskaper for tabellklassen vår:

objekt StarWarsFilms: Tabell () {val id = heltall ("id"). autoIncrement (). primaryKey () val sequelId = heltall ("sequel_id"). uniqueIndex () val name = varchar ("name", 50) val director = varchar ("regissør", 50)}

Vi har utelatt typene for kortfattethet, da Kotlin kan utlede dem for oss. Uansett, hver kolonne er av typen Kolonne og den har et navn, en type og muligens typeparametere.

5.2. Primære nøkler

Som vi kan se fra eksemplet i forrige avsnitt, vi kan enkelt definere indekser og primærnøkler med et flytende API.

For det vanlige tilfellet av en tabell med et heltall primærnøkkel, gir Exposed imidlertid klasser IntIdTable og LongIdTable som definerer nøkkelen for oss:

objekt StarWarsFilms: IntIdTable () {val sequelId = heltall ("sequel_id"). unikIndex () val name = varchar ("name", 50) val director = varchar ("director", 50)}

Det er også en UUIDTable; Videre kan vi definere våre egne varianter ved å underklasse IdTable.

5.3. Utenlandske nøkler

Utenlandske nøkler er enkle å introdusere. Vi har også fordel av statisk skriving fordi vi alltid refererer til egenskaper som er kjent på kompileringstidspunktet.

Anta at vi vil spore navnene på skuespillerne som spiller i hver film:

objekt Spillere: Tabell () {val sequelId = heltall ("sequel_id") .uniqueIndex () .references (StarWarsFilms.sequelId) val name = varchar ("name", 50)}

For å unngå å måtte stave kolonnetypen (i dette tilfellet heltall) når den kan avledes fra den refererte kolonnen, kan vi bruke henvisning metode som stenografi:

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

Hvis referansen er til primærnøkkelen, kan vi utelate kolonnens navn:

val filmId = referanse ("film_id", StarWarsFilms)

5.4. Lage tabeller

Vi kan lage tabellene som definert ovenfor programmatisk:

transaksjon {SchemaUtils.create (StarWarsFilms, Players) // Gjør ting}

Tabellene opprettes bare hvis de ikke allerede eksisterer. Imidlertid er det ingen støtte for databasemigrasjoner.

6. Spørringer

Når vi har definert noen tabellklasser som vi har vist i forrige seksjoner, kan vi sende spørsmål til databasen ved å bruke utvidelsesfunksjonene som leveres av rammeverket.

6.1. Velg alle

For å trekke ut data fra databasen bruker vi Spørsmål objekter bygget fra bordklasser. Den enkleste spørringen er en som returnerer alle radene i en gitt tabell:

val spørring = StarWarsFilms.selectAll ()

Et spørsmål er et Iterabel, så det støtter for hver:

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

Lukkingsparameteren, implisitt kalt den i eksemplet ovenfor, er en forekomst av ResultRow klasse. Vi kan se det som et kart tastet inn etter kolonne.

6.2. Velge et delsett av kolonner

Vi kan også velge et delsett av tabellens kolonner, dvs. utføre en projeksjon ved hjelp av skjære metode:

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

Vi bruker skjære for å bruke en funksjon til en kolonne også:

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

Ofte når du bruker samlede funksjoner som telle og gj.sn. vi trenger en gruppe etter ledd i spørringen. Vi snakker om gruppen i avsnitt 6.5.

6.3. Filtrering med hvor uttrykk

Exposed inneholder en dedikert DSL for hvor uttrykkene, som brukes til å filtrere spørsmål og andre typer utsagn. Dette er et minispråk basert på kolonneegenskapene vi har opplevd tidligere og en serie boolske operatører.

Dette er et hvor uttrykk:

{(StarWarsFilms.director som "J.J.%") og (StarWarsFilms.sequelId eq 7)}

Dens type er kompleks; det er en underklasse av SqlExpressionBuilder, som definerer operatører som som, ekv., og. Som vi kan se, er det en sekvens av sammenligninger kombinert sammen med og og eller operatører.

Vi kan gi et slikt uttrykk til å velge metode, som igjen returnerer et spørsmål:

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

Takket være typen inferens, trenger vi ikke å stave ut den komplekse typen av uttrykket når det sendes direkte til å velge metode som i eksemplet ovenfor.

Siden uttrykk er Kotlin-objekter, er det ingen spesielle bestemmelser for spørsmålsparametere. Vi bruker ganske enkelt variabler:

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

6.4. Avansert filtrering

De Spørsmål gjenstander returnert av å velge og dets varianter har en rekke metoder som vi kan bruke til å avgrense spørringen.

Vi vil for eksempel kanskje ekskludere dupliserte rader:

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

Eller vi vil kanskje bare returnere et delsett av radene, for eksempel når vi paginerer resultatene for brukergrensesnittet:

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

Disse metodene returnerer en ny Spørsmål, slik at vi enkelt kan kjede dem.

6.5. RekkefølgeAv og GruppeAv

De Query.orderBy metoden godtar en liste over kolonner som er tilordnet en Sorteringsrekkefølge verdi som indikerer om sortering skal være stigende eller synkende:

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

Mens grupperingen av en eller flere kolonner, spesielt nyttig når du bruker aggregatfunksjoner (se avsnitt 6.2.), Oppnås ved hjelp av gruppe av metode:

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

6.6. Blir med

Joins er uten tvil et av salgsstedene i relasjonsdatabaser. I de mest enkle tilfellene, når vi har en fremmed nøkkel og ingen tilknytningsbetingelser, kan vi bruke en av de innebygde deltaoperatørene:

(StarWarsFilms innerJoin Players) .selectAll ()

Her har vi vist indre Bli med, men vi har også venstre, høyre og kryssforbindelse tilgjengelig med samme prinsipp.

Deretter kan vi legge til betingelser for et hvor uttrykk; for eksempel hvis det ikke er en fremmed nøkkel, og vi må utføre sammenføyningen eksplisitt:

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

Generelt sett er den fulle formen for en sammenføyning følgende:

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

6.7. Aliasing

Takket være kartleggingen av kolonnenavn til egenskaper, trenger vi ikke noen aliasing i en typisk sammenføyning, selv når kolonnene tilfeldigvis har samme navn:

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

Faktisk, i eksemplet ovenfor, StarWarsFilms.sequelId og Players.sequelId er forskjellige kolonner.

Men når den samme tabellen vises mer enn en gang i et spørsmål, vil vi kanskje gi den et alias. Til det bruker vi alias funksjon:

val sequel = StarWarsFilms.alias ("sequel")

Vi kan da bruke aliaset litt som en tabell:

Bli med (StarWarsFilms, sequel, additionalConstraint = {sequel [StarWarsFilms.sequelId] eq StarWarsFilms.sequelId + 1}). SelectAll (). For Every {assertEquals (it [sequel [StarWarsFilms.sequelId]], it [StarWarsFilms.sequel )}

I eksemplet ovenfor kan vi se at oppfølger alias er et bord som deltar i en delta. Når vi ønsker å få tilgang til en av kolonnene, bruker vi den aliaserte tabellens kolonne som en nøkkel:

oppfølger [StarWarsFilms.sequelId]

7. Uttalelser

Nå som vi har sett hvordan vi kan spørre databasen, la oss se på hvordan du utfører DML-setninger.

7.1. Sette inn data

For å sette inn data kaller vi en av variantene av sett inn funksjon. Alle varianter lukkes:

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

Det er to bemerkelsesverdige gjenstander involvert i lukkingen ovenfor:

  • dette (selve nedleggelsen) er en forekomst av StarWarsFilms klasse; det er derfor vi får tilgang til kolonnene, som er egenskaper, ved deres ukvalifiserte navn
  • den (lukkingsparameteren) er en Sett inn uttalelse; Jegt er en kartlignende struktur med en spalte for hver kolonne å sette inn

7.2. Henter ut automatisk økning av kolonneverdier

Når vi har en innsettingsuttalelse med automatisk genererte kolonner (vanligvis automatisk økning eller sekvenser), vil vi kanskje hente de genererte verdiene.

I det typiske tilfellet har vi bare en generert verdi, og vi kaller insertAndGetId:

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

Hvis vi har mer enn en generert verdi, kan vi lese dem ved navn:

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

7.3. Oppdaterer data

Vi kan nå bruke det vi har lært om spørsmål og innsettinger for å oppdatere eksisterende data i databasen. En enkel oppdatering ser faktisk ut som en kombinasjon av et utvalg med et innlegg:

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

Vi kan se bruken av et hvor uttrykk kombinert med et UpdateStatement nedleggelse. Faktisk, UpdateStatement og InsertStatement dele det meste av API og logikk gjennom en vanlig superklasse, UpdateBuilder, som gir muligheten til å sette verdien til en kolonne ved hjelp av idiomatiske firkantede parenteser.

Når vi trenger å oppdatere en kolonne ved å beregne en ny verdi fra den gamle verdien, utnytter vi SqlExpressionBuilder:

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

Dette er et objekt som gir infiksoperatører (som i tillegg til, minus og så videre) som vi kan bruke til å lage en oppdateringsinstruksjon.

7.4. Slette data

Endelig kan vi slette data med deleteWhere metode:

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

8. DAO API, en lett ORM

Så langt har vi brukt Exposed til å direkte kartlegge fra operasjoner på Kotlin-objekter til SQL-spørsmål og utsagn. Hver metode påkalling som sett inn, oppdater, velg og så videre resulterer i at en SQL-streng umiddelbart blir sendt til databasen.

Imidlertid har Exposed også et høyere nivå DAO API som utgjør en enkel ORM. La oss nå dykke ned i det.

8.1. Enheter

I de forrige avsnittene har vi brukt klasser for å representere databasetabeller og for å uttrykke operasjoner over dem ved hjelp av statiske metoder.

Flytter vi et skritt videre, kan vi definere enheter basert på disse tabellklassene, der hver forekomst av en enhet representerer en databaserad:

klasse StarWarsFilm (id: EntityID): Enhet (id) {ledsagerobjekt: EntityClass (StarWarsFilms) var sequelId av StarWarsFilms.sequelId var navn av StarWarsFilms.name var regissør av StarWarsFilms.director}

La oss nå analysere definisjonen ovenfor stykke for stykke.

I første linje kan vi se at en enhet er en klasse som utvider seg Enhet. Den har en ID med en bestemt type, i dette tilfellet, Int.

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

Deretter møter vi en definisjon av følgesvennobjekt. Ledsagerobjektet representerer enhetsklassen, det vil si den statiske metadata som definerer enheten og operasjonene vi kan utføre på den.

Videre, i erklæringen om ledsagerobjektet, kobler vi enheten, StarWarsFilm - entall, da det representerer en enkelt rad til bordet, StarWarsFilms - flertall, fordi det representerer samlingen av alle radene.

ledsagerobjekt: EntityClass (StarWarsFilms)

Til slutt har vi egenskapene, implementert som eiendomsdelegater til de tilsvarende tabellkolonnene.

var sequelId av StarWarsFilms.sequelId var navn av StarWarsFilms.name var regissør av StarWarsFilms.director

Merk at vi tidligere har erklært kolonnene med val fordi de er uforanderlige metadata. Nå, i stedet, erklærer vi enhetsegenskapene med var, fordi de er mutable spor i en databaserad.

8.2. Sette inn data

For å sette inn en rad i en tabell, oppretter vi ganske enkelt en ny forekomst av enhetsklassen vår ved å bruke den statiske fabrikkmetoden ny i en transaksjon:

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

Merk at operasjoner mot databasen utføres lat; de utstedes bare når varm hurtigbuffer er spylt. Til sammenligning kaller Hibernate den varme hurtigbufferen a økt.

Dette skjer automatisk når det kreves; Eksempel: Første gang vi leser den genererte identifikatoren, kjører Exposed lydløst innleggssetningen:

assertEquals (1, theLastJedi.id.value) // Lesing av ID fører til en spyling

Sammenlign denne oppførselen med sett inn metode fra avsnitt 7.1., som umiddelbart utsteder en uttalelse mot databasen. Her jobber vi på et høyere abstraksjonsnivå.

8.3. Oppdatere og slette objekter

For å oppdatere en rad, tilordner vi bare egenskapene:

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

Mens du skal slette et objekt vi kaller slett på den:

theLastJedi.delete ()

Som med ny, oppdateringen og operasjonene utføres lat.

Oppdateringer og slettinger kan bare utføres på et tidligere lastet objekt. Det er ingen API for massive oppdateringer og slettinger. I stedet må vi bruke API-et på lavere nivå som vi har sett i avsnitt 7. Likevel kan de to API-ene brukes sammen i samme transaksjon.

8.4. Spørring

Med DAO API kan vi utføre tre typer spørsmål.

For å laste alle objektene uten forhold bruker vi den statiske metoden alle:

val filmer = StarWarsFilm.all ()

For å laste et enkelt objekt etter ID kaller vi findById:

val theLastJedi = StarWarsFilm.findById (1)

Hvis det ikke er noe objekt med den IDen, findById returnerer null.

Til slutt, i det generelle tilfellet, bruker vi finne med et hvor uttrykk:

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

8.5. Mange-til-en-foreninger

Akkurat som sammenføyninger er en viktig funksjon i relasjonsdatabaser, kartleggingen av koblinger til referanser er et viktig aspekt av en ORM. Så la oss se hva Exposed har å tilby.

Anta at vi vil spore vurderingen av hver film av brukerne. Først definerer vi to ekstra tabeller:

objektbrukere: IntIdTable () {val name = varchar ("name", 50)} object UserRatings: IntIdTable () {val value = long ("value") val film = reference ("film", StarWarsFilms) val user = reference ("bruker", brukere)}

Deretter skriver vi de tilsvarende enhetene. La oss utelate Bruker enhet, som er triviell, og flytter rett til Bruker vurdering klasse:

klasse UserRating (id: EntityID): IntEntity (id) {ledsagerobjekt: IntEntityClass (UserRatings) var verdi av UserRatings.value var film av StarWarsFilm referertOn UserRatings.film var bruker av User referertOn UserRatings.user}

Merk deg spesielt referert På infix metode kaller på egenskaper som representerer assosiasjoner. Mønsteret er følgende: a var erklæring, av den refererte enheten, referert På referansekolonnen.

Egenskaper som er erklært på denne måten oppfører seg som vanlige egenskaper, men verdien er det tilknyttede objektet:

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

8.6. Valgfrie foreninger

Foreningene vi har sett i forrige avsnitt er obligatoriske, det vil si at vi alltid må spesifisere en verdi.

Hvis vi ønsker en valgfri tilknytning, må vi først erklære kolonnen som ugyldig i tabellen:

val bruker = referanse ("bruker", Brukere) .nullable ()

Så bruker vi valgfritt Referert På i stedet for referert På i enheten:

var bruker av bruker optionalReferencedOn UserRatings.user

På den måten, den bruker eiendommen vil være ugyldig.

8.7. En-til-mange foreninger

Vi vil kanskje også kartlegge motsatt side av foreningen. En vurdering handler om en film, det er det vi modellerer i databasen med en utenlandsk nøkkel; følgelig har en film en rekke rangeringer.

For å kartlegge rangeringer av en film, legger vi ganske enkelt til en eiendom på "den ene" siden av foreningen, det vil si filmenheten i vårt eksempel:

klasse StarWarsFilm (id: EntityID): Entity (id) {// Andre egenskaper elided val ratings av UserRating referrersOn UserRatings.film}

Mønsteret ligner mønsteret for mange-til-en-forhold, men det bruker det henvisere På. Den definerte eiendommen er en Iterabel, slik at vi kan krysse det med for hver:

theLastJedi.ratings.forEach {...}

Merk at, i motsetning til vanlige egenskaper, har vi definert rangeringer med val. Egenskapen er uforanderlig, vi kan bare lese den.

Verdien på eiendommen har ikke noe API for mutasjon også. Så, for å legge til en ny vurdering, må vi lage den med en referanse til filmen:

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

Så, filmen rangeringer listen inneholder den nylig tilføyde vurderingen.

8.8. Mange-til-mange foreninger

I noen tilfeller kan det hende vi trenger en mange-til-mange-forening. La oss si at vi vil legge til en referanse Skuespillere bord til StarWarsFilm klasse:

objekt Skuespillere: IntIdTable () {val firstname = varchar ("firstname", 50) val lastname = varchar ("lastname", 50)} class Actor (id: EntityID): IntEntity (id) {companion object: IntEntityClass (Actors) var fornavn av Actors.firstname var etternavn av Actors.lastname}

Etter å ha definert tabellen og enheten, trenger vi en annen tabell for å representere tilknytningen:

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

Tabellen har to kolonner som begge er utenlandske nøkler, og som også utgjør en sammensatt primærnøkkel.

Til slutt kan vi koble tilknytningstabellen med StarWarsFilm enhet:

klasse StarWarsFilm (id: EntityID): IntEntity (id) {ledsagerobjekt: IntEntityClass (StarWarsFilms) // Andre egenskaper elided var skuespillere av Actor via StarWarsFilmActors}

I skrivende stund er det ikke mulig å opprette en enhet med en generert identifikator og inkludere den i en mange-til-mange-tilknytning i samme transaksjon.

Vi må faktisk bruke flere transaksjoner:

// Opprett først filmen val film = transaksjon {StarWarsFilm.new {name = "The Last Jedi" sequelId = 8 regissør = "Rian Johnson" r}} // Opprett deretter skuespilleren val actor = transaksjon {Actor.new {firstname = "Daisy" lastname = "Ridley"}} // Til slutt kobler du de to sammen transaksjonen {film.actors = SizedCollection (listOf (skuespiller))}

Her har vi brukt tre forskjellige transaksjoner for enkelhets skyld. Imidlertid ville to ha vært tilstrekkelig.

9. Konklusjon

I denne artikkelen har vi gitt en grundig oversikt over det eksponerte rammeverket for Kotlin. For ytterligere informasjon og eksempler, se Exposed wiki.

Implementeringen av alle disse eksemplene og kodebiter finner du i GitHub-prosjektet.


$config[zx-auto] not found$config[zx-overlay] not found