No ano passado, comecei a aprender Kotlin e fiquei surpreso com a facilidade de converter um aplicativo Java. O IntelliJ e alguns outros IDEs oferecem boas ferramentas para conversão automática e, com alguns ajustes, você pode obter um código muito mais conciso e menos propenso a erros.
Por isso, decidi criar um aplicativo de amostra para mostrar minha nova combinação favorita: Kotlin, Spring Boot, Spring Data e Couchbase:
Criação de um serviço de perfil de usuário
Você pode clonar o projeto inteiro aqui:
https://github.com/couchbaselabs/try-cb-kotlin
Vamos começar criando nossa classe principal:
1 2 3 4 5 6 |
@SpringBootApplication aberto classe Aplicativo KotlinDemo diversão principal(argumentos: Matriz<Cordas>) { SpringApplication.executar(Aplicativo KotlinDemo::classe.java, *argumentos) } |
Observação: Sua classe deve ser aberto Caso contrário, você receberá o seguinte erro:
1 2 3 4 |
org.estrutura de mola.feijões.fábrica.análise.BeanDefinitionParsingException: Configuração problema: @Configuração classe 'KotlinDemoApplication' pode não ser final. Remover o final modificador para continuar. Infração recurso: com.couchbase.Aplicativo KotlinDemo em org.estrutura de mola.feijões.fábrica.análise.FailFastProblemReporter.erro(FailFastProblemReporter.java:70) ~[mola-feijões-4.3.13.RELEASE.jar:4.3.13.RELEASE] em org.estrutura de mola.contexto.anotação.Classe de configuração.validar(Classe de configuração.java:214) ~[mola-contexto-4.3.13.RELEASE.jar:4.3.13.RELEASE] |
Aqui está nossa entidade de usuário, que é muito semelhante a o de Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@Documento classe Usuário(): BasicEntity() { construtor(id: Cordas, nome: Cordas, endereço: Endereço, preferências: Lista<Preferência>, funções de segurança: Lista<Cordas>): este(){ este.id = id; este.nome = nome; este.endereço = endereço; este.preferências = preferências; este.funções de segurança = funções de segurança; } @Id var id: Cordas? = nulo @Não nulo var nome: Cordas? = nulo @Campo var endereço: Endereço? = nulo @Campo var preferências: Lista<Preferência> = emptyList() @Campo var funções de segurança: Lista<Cordas> = emptyList() } |
- @Document: A anotação do Couchbase que define uma entidade, semelhante a @Entidade em JPA. O Couchbase adicionará automaticamente uma propriedade chamada _classe no documento para usá-lo como o tipo de documento.
- @Id: A chave do documento
- @Campo: As anotações do Couchbase, semelhantes às do JPA @Coluna. Não é obrigatório, mas recomendamos usá-lo.
O mapeamento de atributos no Couchbase é muito simples. Eles serão mapeados diretamente para a estrutura correspondente no JSON:
- Propriedades simples: Mapeamento direto para JSON:
1 2 3 4 |
{ "id": "user::1", "name" (nome): "Denis Rosa" } |
- Matrizes: Como é de se esperar, matrizes como funções de segurança serão convertidos em matrizes JSON:
1 2 3 |
{ "securityRoles": ["admin", "usuário"] } |
- Entidades aninhadas: Você odeia mapear @ManyToOne relacionamentos? Eu também. Como estamos usando um banco de dados de documentos, não há mais necessidade de escrever esses relacionamentos; as entidades aninhadas também são traduzidas diretamente para JSON.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{ "id":"user::1", "name" (nome):"Denis Rosa", "endereço":{ "streetName":"Uma rua em algum lugar", "houseNumber" (número da casa):"42", "postalCode" (código postal):"81234", "cidade":"Munique", "país":"DE" }, "preferências":[ { "name" (nome):"lang", "valor":"EN" } ], "securityRoles":[ "admin", "usuário" ] } |
Repositórios
Agora, vamos dar uma olhada em como será o nosso repositório:
1 2 3 4 5 6 7 8 9 10 11 12 |
@N1qlPrimaryIndexed @ViewIndexed(designDoc = "usuário") interface Repositório de usuários : CouchbasePagingAndSortingRepository<Usuário, Cordas> { diversão findByName(nome: Cordas): Lista<Usuário> @Consulta("#{#n1ql.selectEntity} where #{#n1ql.filter} and ANY preference IN " + " preferências SATISFIES preferência.nome = $1 END") diversão findUsersByPreferenceName(nome: Cordas): Lista<Usuário> @Consulta("#{#n1ql.selectEntity} where #{#n1ql.filter} and meta().id = $1 and ARRAY_CONTAINS(securityRoles, $2)") diversão hasRole(userId: Cordas, função: Cordas): Usuário } |
- @N1qlPrimaryIndexed: Este anotação garante que o bucket associado ao repositório atual terá um índice primário N1QL
- @ViewIndexed: Isso anotação permite que você defina o nome do documento de design e o nome da visualização, bem como um mapa personalizado e uma função de redução.
Como você pode ver abaixo, é possível aproveitar todos os Palavras-chave do Spring Data para consultar o banco de dados, como Encontrar, Entre, IsGreaterThan, Como, Existeetc.
1 |
diversão findByName(nome: Cordas): Lista<Usuário> |
O repositório está ampliando CouchbasePagingAndSortingRepositoryque permite que você pagine suas consultas simplesmente adicionando um Paginável param no final da definição do método. Se precisar escrever consultas mais avançadas, você também pode usar o N1QL:
1 2 3 4 5 |
@Consulta("#{#n1ql.selectEntity} where #{#n1ql.filter} and ANY preference IN " + " preferências SATISFIES preferência.nome = $1 END") diversão findUsersByPreferenceName(nome: Cordas): Lista<Usuário> @Consulta("#{#n1ql.selectEntity} where #{#n1ql.filter} and meta().id = $1 and ARRAY_CONTAINS(securityRoles, $2)") diversão hasRole(userId: Cordas, função: Cordas): Usuário |
As consultas acima têm algumas sugestões de sintaxe para torná-las menores:
- #(#n1ql.bucket): O uso desta sintaxe evita a codificação do nome do bucket em sua consulta
- #{#n1ql.selectEntity}: açúcar-sintaxe para SELECT * FROM #(#n1ql.bucket):
- #{#n1ql.filter}: syntax-sugar para filtrar o documento por tipo, o que tecnicamente significa class = 'myPackage.MyClassName' (_classe é o atributo adicionado automaticamente ao documento para definir seu tipo quando você está trabalhando com o Couchbase no Spring Data )
- #{#n1ql.fields} será substituído pela lista de campos (por exemplo, para uma cláusula SELECT) necessária para reconstruir a entidade.
- #{#n1ql.delete} será substituído pela instrução delete from.
- #{#n1ql.returning} será substituído pela cláusula de retorno necessária para a reconstrução da entidade.
Serviços
Nosso serviço basicamente encaminha solicitações para o nosso repositório, mas se você precisar escrever consultas ad-hoc, este é o lugar certo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@Serviço classe Serviço de usuário { @Com fio automático lateinit var userRepository: Repositório de usuários; diversão findByName(nome: Cordas): Lista<Usuário> = userRepository.findByName(nome) diversão findById(userId: Cordas) = userRepository.findOne(userId) diversão salvar(@Válido usuário: Usuário) = userRepository.salvar(usuário) diversão findUsersByPreferenceName(nome: Cordas): Lista<Usuário> = userRepository.findUsersByPreferenceName(nome) diversão hasRole(userId: Cordas, função: Cordas): Booleano { retorno userRepository.hasRole(userId, função) != nulo } /** * Exemplo de consultas ad hoc */ diversão findUserByAddress(streetName: Cordas = "", número: Cordas = "", código postal: Cordas = "", cidade: Cordas = "", país: Cordas = ""): Lista<Usuário> { var consulta = "SELECT meta(b).id as id, b.* FROM " + getBucketName() + " b WHERE b._class = '" + Usuário::classe.java.getName() + "' " se (!streetName.isNullOrBlank()) consulta += " e b.address.streetName = '$streetName' " se (!número.isNullOrBlank()) consulta += " e b.address.houseNumber = '$number' " se (!código postal.isNullOrBlank()) consulta += " e b.address.postalCode = '$postalCode' " se (!cidade.isNullOrBlank()) consulta += " e b.address.city = '$city' " se (!país.isNullOrBlank()) consulta += " e b.address.country = '$country' " val parâmetros = N1qlParams.construir().consistência(Consistência de varredura.REQUEST_PLUS).adhoc(verdadeiro) val paramQuery = N1qlQuery.parametrizado(consulta, JsonObject.criar(), parâmetros) retorno userRepository.getCouchbaseOperations().findByN1QLProjection(paramQuery, Usuário::classe.java) } diversão getBucketName() = userRepository.getCouchbaseOperations().getCouchbaseBucket().gerenciador de balde().informações().nome() } |
Controladores
Por fim, vamos adicionar também um controlador para testar nossos serviços via rest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@RestController @RequestMapping("/api/user") classe Controlador de usuário { @Com fio automático lateinit var userService: Serviço de usuário @GetMapping(valor = "/{id}") diversão findById(@PathParam("id") id: Cordas) = userService.findById(id) @GetMapping(valor = "/preferência") diversão findPreference(@RequestParam("name" (nome)) nome: Cordas): Lista<Usuário> { retorno userService.findUsersByPreferenceName(nome) } @GetMapping(valor = "/find") diversão findUserByName(@RequestParam("name" (nome)) nome: Cordas): Lista<Usuário> { retorno userService.findByName(nome) } @PostMapping(valor = "/save") diversão findUserByName(@Corpo da solicitação usuário: Usuário) = userService.salvar(usuário) @GetMapping(valor = "/findByAddress") diversão findByAddress(@RequestParam("streetName", defaultValue = "") streetName: Cordas, @RequestParam("número", defaultValue = "") número: Cordas, @RequestParam("postalCode" (código postal), defaultValue = "") código postal: Cordas, @RequestParam("cidade", defaultValue = "") cidade: Cordas, @RequestParam("país", defaultValue = "") país: Cordas): Lista<Usuário> { retorno userService.findUserByAddress(streetName, número, código postal, cidade, país); } } |
Escrevendo testes de integração com Kotlin
Para executar os testes de integração corretamente, não se esqueça de configurar as credenciais de seu banco de dados na seção application.properties file:
1 2 3 4 |
mola.couchbase.bootstrap-anfitriões=localhost mola.couchbase.balde.nome=teste mola.couchbase.balde.senha=somePassword mola.dados.couchbase.automático-índice=verdadeiro |
Aqui, você pode ver a aparência de nossos testes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Teste diversão testComposedAddress() { val endereço1 = Endereço("street1", "1", "0000", "santo andre", "br") val endereço2 = Endereço("street1", "2", "0000", "santo andre", "br") val endereço3 = Endereço("street2", "12", "1111", "munique", "de") userService.salvar(Usuário(USUÁRIO_1, "user1", endereço1, emptyList(), emptyList())) userService.salvar(Usuário("user::2", "user2", endereço2, emptyList(), emptyList())) userService.salvar(Usuário("user::3", "user3", endereço3, emptyList(), emptyList())) var usuários = userService.findUserByAddress(streetName = "street1") AssertThat(usuários, hasSize<Qualquer>(2)) usuários = userService.findUserByAddress(streetName = "street1", número= "1") AssertThat(usuários, hasSize<Qualquer>(1)) usuários = userService.findUserByAddress(país = "de") AssertThat(usuários, hasSize<Qualquer>(1)) } |
Dependências de Kotlin e Maven
O Kotlin está evoluindo rapidamente, portanto, lembre-se de usar as versões mais recentes de cada dependência:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> <version>1.2.41</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> <version>1.2.41</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> <version>2.9.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-couchbase</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>teste de inicialização de mola</artifactId> <scope>teste</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>biblioteca de hamcrest</artifactId> <version>1.3</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>base de dados de mola</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>teste de kotlin</artifactId> <version>${kotlin.version}</version> <scope>teste</scope> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>1.2.41</version> </dependency> </dependencies> |
Você pode visualizar todo o pom.xml aqui.