Guia do Framework Exposto Kotlin

Guia do Framework Exposto Kotlin

1. Introdução

Neste tutorial, veremos como consultar um banco de dados relacional usandoExposed.

Exposed é uma biblioteca de código-fonte aberto (licença Apache) desenvolvida pela JetBrains, que fornece uma API Kotlin idiomática para algumas implementações de banco de dados relacional, enquanto suaviza as diferenças entre os fornecedores de banco de dados.

Exposed can be used both as a high-level DSL over SQL and as a lightweight ORM (Object-Relational Mapping). Assim, cobriremos os dois usos ao longo deste tutorial.

2. Configuração da estrutura exposta

Exposed ainda não está no Maven Central, então temos que usar um repositório dedicado:


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

Em seguida, podemos incluir a biblioteca:


    org.jetbrains.exposed
    exposed
    0.10.4

Além disso, nas seções a seguir, mostraremos exemplos usando o banco de dados H2 na memória:


    com.h2database
    h2
    1.4.197

Podemos encontrar a versão mais recente deExposed no Bintray e a versão mais recente deH2 no Maven Central.

3. Conectando ao banco de dados

Definimos conexões de banco de dados com a classeDatabase:

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

Também podemos especificar umuser e umpassword como parâmetros nomeados:

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. Apenas salva os parâmetros de conexão para uso posterior.

3.1. Parâmetros adicionais

Se precisarmos fornecer outros parâmetros de conexão, usaremos uma sobrecarga diferente do métodoconnect que nos dá controle total sobre a aquisição de uma conexão de banco de dados:

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

Esta versão deconnect requer um parâmetro de fechamento. Exposed invokes the closure whenever it needs a new connection to the database.

3.2. Usando umDataSource

Se, em vez disso, nos conectarmos ao banco de dados usando umDataSource, como geralmente é o caso em aplicativos corporativos (por exemplo, para se beneficiar do pool de conexão), podemos usar a sobrecargaconnect apropriada:

Database.connect(datasource)

4. Abrindo uma transação

Cada operação de banco de dados em Exposed precisa de uma transação ativa.

O métodotransaction pega um encerramento e o invoca com uma transação ativa:

transaction {
    //Do cool stuff
}

Otransaction retorna o que quer que o fechamento retorne. Then, Exposed automatically closes the transaction when the execution of the block terminates.

4.1. Commit and Rollback

Quando o blocotransaction retorna com sucesso, Exposed confirma a transação. Quando, em vez disso, o encerramento é encerrado lançando uma exceção, a estrutura reverte a transação.

Também podemos confirmar ou reverter manualmente uma transação. O fechamento que fornecemos paratransaction é na verdade uma instância da classeTransaction graças à magia do Kotlin.

Assim, temos os métodoscommiterollback disponíveis:

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

4.2. Declarações de registro

Ao aprender a estrutura ou a depuração, podemos achar útil inspecionar as instruções e consultas SQL que o Exposed envia ao banco de dados.

Podemos adicionar facilmente esse logger à transação ativa:

transaction {
    addLogger(StdOutSqlLogger)
    //Do stuff
}

5. Definindo Tabelas

Normalmente, no Exposed não trabalhamos com strings e nomes SQL brutos. Instead, we define tables, columns, keys, relationships, etc., using a high-level DSL.

Representamos cada tabela com uma instância da classeTable:

object StarWarsFilms : Table()

Exposed calcula automaticamente o nome da tabela a partir do nome da classe, mas também podemos fornecer um nome explícito:

object StarWarsFilms : Table("STAR_WARS_FILMS")

5.1. Colunas

Uma tabela não tem sentido sem colunas. Definimos colunas como propriedades da nossa classe de tabela:

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

Omitimos os tipos por questões de brevidade, pois Kotlin pode deduzi-los para nós. De qualquer forma, cada coluna é do tipoColumn<T> e possui um nome, um tipo e possivelmente parâmetros de tipo.

5.2. Chaves Primárias

Como podemos ver no exemplo da seção anterior,we can easily define indexes and primary keys with a fluent API.

No entanto, para o caso comum de uma tabela com uma chave primária inteira, Exposed fornece classesIntIdTableeLongIdTable que definem a chave para nós:

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

Há também umUUIDTable;, além disso, podemos definir nossas próprias variantes subclassificandoIdTable.

5.3. Chaves estrangeiras

Chaves estrangeiras são fáceis de introduzir. Também nos beneficiamos da digitação estática, porque sempre nos referimos às propriedades conhecidas em tempo de compilação.

Suponha que desejemos rastrear os nomes dos atores que interpretam em cada filme:

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

Para evitar ter que escrever o tipo da coluna (neste caso,integer) quando ela pode ser derivada da coluna referenciada, podemos usar o métodoreference como uma abreviação:

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

Se a referência for à chave primária, podemos omitir o nome da coluna:

val filmId = reference("film_id", StarWarsFilms)

5.4. Criando tabelas

Podemos criar as tabelas conforme definido acima programaticamente:

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

As tabelas são criadas apenas se ainda não existirem. No entanto, não há suporte para migrações de banco de dados.

6. Consultas

Depois de definir algumas classes de tabela como mostramos nas seções anteriores, podemos fazer consultas ao banco de dados usando as funções de extensão fornecidas pela estrutura.

6.1. Selecionar tudo

Para extrair dados do banco de dados, usamos objetosQuery construídos a partir de classes de tabela. A consulta mais simples é aquela que retorna todas as linhas de uma determinada tabela:

val query = StarWarsFilms.selectAll()

Uma consulta é umIterable,, portanto, suportaforEach:

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

O parâmetro de fechamento, implicitamente chamado deit no exemplo acima, é uma instância da classeResultRow. Podemos vê-lo como um mapa digitado por coluna.

6.2. Selecionando um Subconjunto de Colunas

Também podemos selecionar um subconjunto das colunas da tabela, ou seja, realizar uma projeção, usando o métodoslice:

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

Usamosslice para aplicar uma função a uma coluna também:

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

Freqüentemente, ao usar funções agregadas, comocounteavg,, precisaremos de uma cláusula group by na consulta. Falaremos sobre o grupo na seção 6.5.

6.3. Filtrando com expressões Where

Exposed contains a dedicated DSL for where expressions, que são usados ​​para filtrar consultas e outros tipos de instruções. Esta é uma minilinguagem baseada nas propriedades da coluna que encontramos anteriormente e em uma série de operadores booleanos.

Esta é uma expressão where:

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

Seu tipo é complexo; é uma subclasse deSqlExpressionBuilder, que define operadores comolike, eq, and. Como podemos ver, é uma sequência de comparações combinadas com os operadoresandeor.

Podemos passar essa expressão para o métodoselect, que novamente retorna uma consulta:

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

Graças à inferência de tipo, não precisamos explicar o tipo complexo da expressão where quando ela é passada diretamente para o métodoselect como no exemplo acima.

Since where expressions are Kotlin objects, there are no special provisions for query parameters. Simplesmente usamos variáveis:

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

6.4. Filtragem Avançada

Os objetosQuery retornados porselect e suas variantes têm vários métodos que podemos usar para refinar a consulta.

Por exemplo, convém excluir linhas duplicadas:

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

Ou podemos querer retornar apenas um subconjunto das linhas, por exemplo, ao paginar os resultados para a interface do usuário:

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

Esses métodos retornam um novoQuery, portanto, podemos encadea-los facilmente.

6.5. Ordenar por e Grupo por

O métodoQuery.orderBy aceita uma lista de colunas mapeadas para um valorSortOrder indicando se a classificação deve ser crescente ou decrescente:

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

Enquanto o agrupamento por uma ou mais colunas, útil em particular ao usar funções de agregação (consulte a seção 6.2.), É obtido usando o métodogroupBy:

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

6.6. Junções

As junções são indiscutivelmente um dos pontos de venda dos bancos de dados relacionais. Nos casos mais simples, quando temos uma chave estrangeira e nenhuma condição de junção, podemos usar um dos operadores de junção internos:

(StarWarsFilms innerJoin Players).selectAll()

Aqui, mostramosinnerJoin, mas também temos junção esquerda, direita e cruzada disponíveis com o mesmo princípio.

Então, podemos adicionar condições de junção com uma expressão where; por exemplo, se não houver uma chave estrangeira e devemos realizar a junção explicitamente:

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

No caso geral, a forma completa de uma associação é a seguinte:

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

6.7. Alias

Graças ao mapeamento de nomes de coluna para propriedades, não precisamos de nenhum aliasing em uma junção típica, mesmo quando as colunas têm o mesmo nome:

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

Na verdade, no exemplo acima,StarWarsFilms.sequelId ePlayers.sequelId são colunas diferentes.

No entanto, quando a mesma tabela aparece mais de uma vez em uma consulta, podemos dar um alias a ela. Para isso usamos a funçãoalias:

val sequel = StarWarsFilms.alias("sequel")

Podemos então usar o alias um pouco como uma tabela:

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

No exemplo acima, podemos ver que o aliassequel é uma tabela que participa de uma junção. Quando queremos acessar uma de suas colunas, usamos a coluna da tabela com alias como chave:

sequel[StarWarsFilms.sequelId]

7. Afirmações

Agora que vimos como consultar o banco de dados, vamos ver como executar instruções DML.

7.1. Inserindo dados

To insert data, we call one of the variants of the insert function. Todas as variantes fecham:

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

Existem dois objetos notáveis ​​envolvidos no fechamento acima:

  • this (o próprio fechamento) é uma instância da classeStarWarsFilms; é por isso que podemos acessar as colunas, que são propriedades, por seus nomes não qualificados

  • it (o parâmetro de fechamento) é umInsertStatement; it é uma estrutura semelhante a um mapa com um slot para cada coluna inserir

7.2. Extraindo Valores de Coluna de Incremento Automático

Quando temos uma instrução de inserção com colunas geradas automaticamente (normalmente incremento automático ou sequências), podemos obter os valores gerados.

No caso típico, temos apenas um valor gerado e chamamosinsertAndGetId:

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

Se tivermos mais de um valor gerado, podemos lê-los pelo nome:

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. Atualizando dados

Agora podemos usar o que aprendemos sobre consultas e inserções para atualizar dados existentes no banco de dados. 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"
}

Podemos ver o uso de uma expressão where combinada com um fechamentoUpdateStatement. Na verdade,UpdateStatement eInsertStatement compartilham a maior parte da API e da lógica por meio de uma superclasse comum,UpdateBuilder,, que fornece a capacidade de definir o valor de uma coluna usando colchetes idiomáticos.

Quando precisamos atualizar uma coluna calculando um novo valor a partir do valor antigo, aproveitamos oSqlExpressionBuilder:

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

Este é um objeto que fornece operadores infixados (comoplus,minus e assim por diante) que podemos usar para construir uma instrução de atualização.

7.4. Exclusão de dados

Finalmente, podemos excluir dados com o métododeleteWhere:

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

8. A API DAO, um ORM leve

Até agora, usamos o Exposed para mapear diretamente de operações em objetos Kotlin para consultas e instruções SQL. Cada chamada de método, comoinsert, update, selecte assim por diante, resulta no envio imediato de uma string SQL para o banco de dados.

No entanto, o Exposed também possui uma API DAO de nível superior que constitui um ORM simples. Vamos agora mergulhar nisso.

8.1. Entidades

Nas seções anteriores, usamos classes para representar tabelas de banco de dados e para expressar operações sobre elas, usando métodos estáticos.

Indo um passo adiante, podemos definir entidades com base nessas classes de tabela, em que cada instância de uma entidade representa uma linha do banco de dados:

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
}

Vamos agora analisar a definição acima, peça por peça.

Na primeira linha, podemos ver que uma entidade é uma classe que estendeEntity. Possui um ID com um tipo específico, neste casoInt.

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

Em seguida, encontramos uma definição de objeto complementar. The companion object represents the entity class, that is, the static metadata defining the entity and the operations we can perform on it.

Além disso, na declaração do objeto complementar, conectamos a entidadeStarWarsFilm – singular, pois representa uma única linha à tabela,StarWarsFilms - plural, porque representa a coleção de todas as linhas.

companion object : EntityClass(StarWarsFilms)

Finalmente, temos as propriedades, implementadas como delegadas de propriedade nas colunas correspondentes da tabela.

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

Observe que anteriormente declaramos as colunas comval porque são metadados imutáveis. Agora, em vez disso, estamos declarando as propriedades da entidade comvar, porque eles são slots mutáveis ​​em uma linha do banco de dados.

8.2. Inserindo dados

Para inserir uma linha em uma tabela, simplesmente criamos uma nova instância de nossa classe de entidade usando o método de fábrica estáticonew em uma transação:

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. Para comparação, o Hibernate chama o cache quente desession.

Isso acontece automaticamente quando necessário; por exemplo, na primeira vez em que lemos o identificador gerado, Exposed executa silenciosamente a instrução insert:

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

Compare esse comportamento com o métodoinsert da seção 7.1., Que imediatamente emite uma instrução no banco de dados. Aqui, estamos trabalhando em um nível mais alto de abstração.

8.3. Atualização e exclusão de objetos

Para atualizar uma linha, simplesmente atribuímos às suas propriedades:

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

Enquanto para excluir um objeto, chamamosdelete nele:

theLastJedi.delete()

Tal como acontece comnew, a atualização e as operações são realizadas lentamente.

Updates and deletions can only be performed on a previously loaded object. Não há API para atualizações e exclusões massivas. Em vez disso, temos que usar a API de nível inferior que vimos na seção 7. Ainda assim, as duas APIs podem ser usadas juntas na mesma transação.

8.4. Consulta

Com a API DAO, podemos realizar três tipos de consultas.

Para carregar todos os objetos sem condições, usamos o método estáticoall:

val movies = StarWarsFilm.all()

Para carregar um único objeto por ID, chamamosfindById:

val theLastJedi = StarWarsFilm.findById(1)

Se não houver nenhum objeto com esse ID,findById retornanull.

Finalmente, no caso geral, usamosfind com uma expressão where:

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

8.5. Associações muitos-para-um

Assim como as junções são um recurso importante dos bancos de dados relacionais,the mapping of joins to references is an important aspect of an ORM. Então, vamos ver o que o Exposed tem a oferecer.

Suponha que desejemos acompanhar a classificação de cada filme pelos usuários. Primeiro, definimos duas tabelas adicionais:

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

Então, vamos escrever as entidades correspondentes. Vamos omitir a entidadeUser, que é trivial, e ir direto para a 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. O padrão é o seguinte: uma declaraçãovar,by a entidade referenciada,referencedOn a coluna de referência.

As propriedades declaradas dessa maneira se comportam como propriedades regulares, mas seu valor é o objeto associado:

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

8.6. Associações Opcionais

As associações que vimos na seção anterior são obrigatórias, ou seja, devemos sempre especificar um valor.

Se queremos uma associação opcional, devemos primeiro declarar a coluna como anulável na tabela:

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

Então, usaremosoptionalReferencedOn em vez dereferencedOn na entidade:

var user by User optionalReferencedOn UserRatings.user

Dessa forma, a propriedadeuser será anulável.

8.7. Associações Um-para-Muitos

Também podemos querer mapear o lado oposto da associação. Uma classificação é sobre um filme, é o que modelamos no banco de dados com uma chave estrangeira; conseqüentemente, um filme tem várias classificações.

Para mapear as classificações de um filme, simplesmente adicionamos uma propriedade ao lado “um” da associação, ou seja, a entidade do filme em nosso exemplo:

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

O padrão é semelhante ao de relacionamentos muitos para um, mas usareferrersOn. A propriedade assim definida é umIterable,, portanto, podemos percorrê-lo comforEach:

theLastJedi.ratings.forEach { ... }

Observe que, ao contrário das propriedades regulares, definimosratings comval.Indeed, the property is immutable, we can only read it.

O valor da propriedade também não possui API para mutação. Portanto, para adicionar uma nova classificação, devemos criá-la com uma referência ao filme:

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

Então, a listaratings do filme conterá a avaliação recém-adicionada.

8.8. Associações Muitos-para-Muitos

Em alguns casos, podemos precisar de uma associação muitos-para-muitos. Digamos que queremos adicionar uma tabela de referênciaActors à 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
}

Tendo definido a tabela e a entidade, precisamos de outra tabela para representar a associação:

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

A tabela possui duas colunas que são chaves estrangeiras e que também compõem uma chave primária composta.

Finalmente, podemos conectar a tabela de associação com a entidadeStarWarsFilm:

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

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

No momento em que este artigo foi escrito, não é possível criar uma entidade com um identificador gerado e incluí-lo em uma associação muitos-para-muitos na mesma transação.

De fato, temos que usar várias transações:

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

Aqui, usamos três transações diferentes por conveniência. No entanto, dois teriam sido suficientes.

9. Conclusão

Neste artigo, demos uma visão geral completa da estrutura exposta para Kotlin. Para obter informações adicionais e exemplos, consulteExposed wiki.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emthe GitHub project como um projeto Maven, portanto, deve ser fácil importar e executar como está.