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 open class KotlinDemoApplication fun main(args: Array<String>) { SpringApplication.run(KotlinDemoApplication::class.java, *args) } |
Observação: Sua classe deve ser aberto Caso contrário, você receberá o seguinte erro:
|
1 2 3 4 |
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Configuration class 'KotlinDemoApplication' may not be final. Remove the final modifier to continue. Offending resource: com.couchbase.KotlinDemoApplication at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:70) ~[spring-beans-4.3.13.RELEASE.jar:4.3.13.RELEASE] at org.springframework.context.annotation.ConfigurationClass.validate(ConfigurationClass.java:214) ~[spring-context-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 |
@Document class User(): BasicEntity() { constructor(id: String, name: String, address: Address, preferences: List<Preference>, securityRoles: List<String>): this(){ this.id = id; this.name = name; this.address = address; this.preferences = preferences; this.securityRoles = securityRoles; } @Id var id: String? = null @NotNull var name: String? = null @Field var address: Address? = null @Field var preferences: List<Preference> = emptyList() @Field var securityRoles: List<String> = 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": "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", "user"] } |
- 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":"Denis Rosa", "address":{ "streetName":"A Street Somewhere", "houseNumber":"42", "postalCode":"81234", "city":"Munich", "country":"DE" }, "preferences":[ { "name":"lang", "value":"EN" } ], "securityRoles":[ "admin", "user" ] } |
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 = "user") interface UserRepository : CouchbasePagingAndSortingRepository<User, String> { fun findByName(name: String): List<User> @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and ANY preference IN " + " preferences SATISFIES preference.name = $1 END") fun findUsersByPreferenceName(name: String): List<User> @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and meta().id = $1 and ARRAY_CONTAINS(securityRoles, $2)") fun hasRole(userId: String, role: String): User } |
- @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 |
fun findByName(name: String): List<User> |
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 |
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and ANY preference IN " + " preferences SATISFIES preference.name = $1 END") fun findUsersByPreferenceName(name: String): List<User> @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and meta().id = $1 and ARRAY_CONTAINS(securityRoles, $2)") fun hasRole(userId: String, role: String): User |
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 |
@Service class UserService { @Autowired lateinit var userRepository: UserRepository; fun findByName(name: String): List<User> = userRepository.findByName(name) fun findById(userId: String) = userRepository.findOne(userId) fun save(@Valid user: User) = userRepository.save(user) fun findUsersByPreferenceName(name: String): List<User> = userRepository.findUsersByPreferenceName(name) fun hasRole(userId: String, role: String): Boolean { return userRepository.hasRole(userId, role) != null } /** * Example of ad hoc queries */ fun findUserByAddress(streetName: String = "", number: String = "", postalCode: String = "", city: String = "", country: String = ""): List<User> { var query = "SELECT meta(b).id as id, b.* FROM " + getBucketName() + " b WHERE b._class = '" + User::class.java.getName() + "' " if (!streetName.isNullOrBlank()) query += " and b.address.streetName = '$streetName' " if (!number.isNullOrBlank()) query += " and b.address.houseNumber = '$number' " if (!postalCode.isNullOrBlank()) query += " and b.address.postalCode = '$postalCode' " if (!city.isNullOrBlank()) query += " and b.address.city = '$city' " if (!country.isNullOrBlank()) query += " and b.address.country = '$country' " val params = N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS).adhoc(true) val paramQuery = N1qlQuery.parameterized(query, JsonObject.create(), params) return userRepository.getCouchbaseOperations().findByN1QLProjection(paramQuery, User::class.java) } fun getBucketName() = userRepository.getCouchbaseOperations().getCouchbaseBucket().bucketManager().info().name() } |
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") class UserController { @Autowired lateinit var userService: UserService @GetMapping(value = "/{id}") fun findById(@PathParam("id") id: String) = userService.findById(id) @GetMapping(value = "/preference") fun findPreference(@RequestParam("name") name: String): List<User> { return userService.findUsersByPreferenceName(name) } @GetMapping(value = "/find") fun findUserByName(@RequestParam("name") name: String): List<User> { return userService.findByName(name) } @PostMapping(value = "/save") fun findUserByName(@RequestBody user: User) = userService.save(user) @GetMapping(value = "/findByAddress") fun findByAddress(@RequestParam("streetName", defaultValue = "") streetName: String, @RequestParam("number", defaultValue = "") number: String, @RequestParam("postalCode", defaultValue = "") postalCode: String, @RequestParam("city", defaultValue = "") city: String, @RequestParam("country", defaultValue = "") country: String): List<User> { return userService.findUserByAddress(streetName, number, postalCode, city, country); } } |
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 |
spring.couchbase.bootstrap-hosts=localhost spring.couchbase.bucket.name=test spring.couchbase.bucket.password=somePassword spring.data.couchbase.auto-index=true |
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 |
@Test fun testComposedAddress() { val address1 = Address("street1", "1", "0000", "santo andre", "br") val address2 = Address("street1", "2", "0000", "santo andre", "br") val address3 = Address("street2", "12", "1111", "munich", "de") userService.save(User(USER_1, "user1", address1, emptyList(), emptyList())) userService.save(User("user::2", "user2", address2, emptyList(), emptyList())) userService.save(User("user::3", "user3", address3, emptyList(), emptyList())) var users = userService.findUserByAddress(streetName = "street1") assertThat(users, hasSize<Any>(2)) users = userService.findUserByAddress(streetName = "street1", number= "1") assertThat(users, hasSize<Any>(1)) users = userService.findUserByAddress(country = "de") assertThat(users, hasSize<Any>(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>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <version>1.3</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-couchbase</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>kotlin-test</artifactId> <version>${kotlin.version}</version> <scope>test</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.