Desenvolvendo um site de gerenciamento financeiro #4

UML para Entidades
Olá e bem vindo de volta.
Na postagem de hoje, continuarei o desenvolvimento de um site de gerenciamento financeiro. Para aqueles que me encontraram agora, isto não é um tutorial comum. Isto é uma espécie de log de minhas atividades e ideias enquanto eu desenvolvo esta aplicação. Você encontrará aqui informações básicas e avançadas e elas virão lentamente porque, mais que tudo, eu quero registrar meus pensamentos e praticar minha escrita.
Nossas entidades
Na minha última postagem eu nos guio na criação de um projeto Ktor. Se você quer dar uma olhada, está aqui e na postagem anterior a esta, criei um UML simples representando os estágios iniciais de nossas classes. Também aqui se você quiser dar uma olhada.
Hoje nos vamos criar as classes em nosso projeto, seguindo o que foi desenhado no UML. Como eu sei que você não quer ler para sempre, vou criar apenas as 3 classes principais do projeto e me certificar que o Ebean as reconheça e que consiga as salvar para o banco de dados. Para isto, usarei um simples teste que também nos dará uma visão inicial de TDD.

Você se lembra disto, certo? Isto é uma representação das nossas classes principais: Person e Company. Praticamente tudo em nossa aplicação será relacionado de alguma forma com estas classes.
Como eu quero demostrar alguma abstração neste projeto. Eu desenhei as classes como uma dependência de BaseEntity que guardará todas as propriedades que foram compartilhadas pelas duas classes. Alguns ORMs chamam entidades como a BaseEntity de MappedSuperClass. Depedendo do ORM usado, nós podemos criar uma tabela única para guardar todas as pessoas e empresas ou duas tabelas, praticamente idênticas para cada entidade.
Eu vou com a estratégia de tabela única neste exemplo.

Diferentemente de Java, Kotlin permite que criemos diversas classes no mesmo arquivo. Para simplificar o projeto, agruparei as entidades Person, Company e BaseEntity no mesmo arquivo.
Para isto, criei o arquivo chamado BaseEntityRelated.kt no meu pacote de modelos. O conteúdo deste arquivo é este:
package com.marcelustrojahn.models
import io.ebean.Finder
import java.time.LocalDate
import javax.persistence.*
@Entity
@Inheritance
abstract class BaseEntity(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null,
var name: String = "",
var enabled: Boolean = true,
var addressStreet: String = "",
var addressNumber: Int = 0,
var addressDistrict: String = "",
var addressCity: String = "",
var addressState: String = "",
var addressZip: String = "",
var birth: LocalDate = LocalDate.MIN,
@Version
var version: Long = 0
)
@Entity
class Person(
var cpf: String? = "",
var rg: String? = "",
) : BaseEntity()
@Entity
class Company(
var officialName: String? = "",
var cnpj: String? = "",
var ie: String? = "",
var im: String? = ""
) : BaseEntity()
Aqueles que já são familiarizados com Kotlin devem ter notado que eu não estou usando data classes. Muitos desenvolvedores gostam de usá-las (inclusive eu) já que elas provem facilidades como toString() e equals() automaticamente. Não estou usando-as porque o Ebean também altera as classes e estas alterações acabam conflitando com as data classes. Eu já passei por problemas bem estranhos relacionados a isto então, por enquanto, não as usarei.
Ok, então explicarei um pouco estas classes.
BaseEntity detém todas as propriedades que são compartilhadas por Person e Company. O @Id e @GeneratedValue são anotações que dizem ao Ebean que a coluna id será a chave primária e que ela será auto-incrementada.
A anotação @Version diz para o Ebean que aquela coluna será usada para o versionamento dos registros no banco de dados, ou seja, a cada vez que houver uma tentativa de se salvar um registro a versão será verificada. Se a versão for diferente, alguém já alterou este registro antes de você e a operação falhará.
Preparando o Teste
Vamos ter certeza que nossas entidades estão corretamente configuradas, para tal, criaremos um arquivo chamado application.properties que o Ebean usará para consultar sua configuração. Este arquivo será criado em /src/test/resources. Criando o arquivo neste local estaremos nos certificando que ele só será usado durante nossos testes. Mais tarde, criaremos outra versão deste mesmo arquivo para produção que será armazenada em outro diretório do nosso projeto.
Este é o nosso /src/test/resources/application.properties
ebean.packages=com.marcelustrojahn.models
ebean.db.ddl.generate=true
ebean.db.ddl.run=true
datasource.db.username=sa
datasource.db.password=
datasource.db.databaseUrl=jdbc:h2:file:~/monies_db_test;AUTO_SERVER=true
datasource.db.databaseDriver=org.h2.Driver
Explicarei rapidamente:
- ebean.packages diz para o Ebean onde encontrar meus modelos.
- as opções de DDL dizem para o Ebean começar meu banco de dados do zero a cada teste.
- username, password e driver são auto-explicativos.
- databaseUrl diz para o Ebean como conectar no banco de dados. Para os testes, usarei um banco de dados local H2. Eu gosto de usá-lo como arquivo ao invés de temporário na memória pois assim posso inspecioná-lo em caso de algum problema. AUTO_SERVER=true permite que eu consiga conectar no banco de dados com outro cliente mesmo que o arquivo esteja em uso.
O Teste
O arquivo do será criado em /src/test/kotlin. Ele poderia ser armazenado em qualquer lugar aqui dentro mas gosto de colocá-lo em um pacote com o mesmo nome da classe que estou testando. Então, neste caso, nosso arquivo de teste está em /src/test/kotlin/com/marcelustrojahn/models/BaseEntityTests.kt
package com.marcelustrojahn.models
import io.ebean.Ebean
import io.kotlintest.matchers.shouldBe
import io.kotlintest.matchers.shouldNotBe
import io.kotlintest.specs.FreeSpec
class BaseEntityTests: FreeSpec() {
init {
// remember the database will always be empty in the beggining of tests
// so the id will be 1
"should be able to save a Person" {
val person = Person().apply { name = "John" }
Ebean.save(person)
person.id shouldNotBe null // id is filled on save
}
"should be able to select the Person from the database" {
val person = Ebean.find(Person::class.java).where().idEq(1).findOne()
person shouldNotBe null
}
"should be able to update the Person" {
val person = Ebean.find(Person::class.java).where().idEq(1).findOne()
person?.name = "Jack"
Ebean.save(person)
Ebean.find(Person::class.java).where().idEq(1).findOne()?.name shouldBe "Jack"
}
"should be able to delete the Person" {
Ebean.delete(Person::class.java, 1)
Ebean.find(Person::class.java).where().idEq(1).findOne() shouldBe null
}
}
}
Este é um teste bem básico. Eu só quero ter certeza de que o Ebean consegue falar com o banco de dados. Como você pode ver, a sintaxe do kotlintest é muito fácil de entender eu eu gosto muito. Aqui eu testo se eu consigo criar um novo registro, selecionar, atualizar e deletá-lo. Quando progredirmos no projeto, mostrarei formas melhores de interagir com o banco de dados usando o Ebean.
Este é o resultado dos testes:

No próximo post, mostrarei como podemos melhorar nossos modelos tornando a pesquisa no banco de dados ainda mais fácil com o Ebean.
Até breve!