작년에 저는 Kotlin을 배우기 시작하면서 Java 애플리케이션을 변환하는 것이 얼마나 쉬운지 놀랐습니다. IntelliJ와 몇 가지 다른 IDE는 자동 변환을 위한 훌륭한 도구를 제공하며, 몇 가지 조정만 하면 훨씬 간결하고 오류가 적은 코드를 완성할 수 있습니다.

그래서 제가 가장 좋아하는 새로운 조합을 보여주기 위해 샘플 애플리케이션을 만들기로 했습니다: Kotlin입니다, 스프링 부트, 스프링 데이터, 카우치베이스:
사용자 프로필 서비스 만들기
여기에서 전체 프로젝트를 복제할 수 있습니다:
https://github.com/couchbaselabs/try-cb-kotlin
메인 클래스를 만드는 것부터 시작하겠습니다:
|
1 2 3 4 5 6 |
@SpringBootApplication open class KotlinDemoApplication fun main(args: Array<String>) { SpringApplication.run(KotlinDemoApplication::class.java, *args) } |
참고: 클래스는 다음과 같아야 합니다. 열기 그렇지 않으면 다음과 같은 오류가 발생합니다:
|
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] |
다음은 사용자 엔티티로, 다음과 매우 유사합니다. 자바:
|
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: 엔티티를 정의하는 카우치베이스의 어노테이션은 다음과 유사합니다. 엔티티 를 추가합니다. 카우치베이스는 자동으로 다음과 같은 프로퍼티를 추가합니다. _class 를 문서에 추가하여 문서 유형으로 사용하세요.
- @Id: 문서의 핵심
- @Field: JPA의 주석과 유사한 Couchbase의 주석은 다음과 같습니다. 칼럼. 이 기능은 필수는 아니지만 사용을 권장합니다.
Couchbase에서 어트리뷰트 매핑은 매우 간단합니다. JSON의 해당 구조에 직접 매핑됩니다:
- 간단한 속성: JSON으로 간단하게 매핑할 수 있습니다:
|
1 2 3 4 |
{ "id": "user::1", "name": "Denis Rosa" } |
- 배열: 예상할 수 있듯이 다음과 같은 배열은 보안 역할 은 JSON 배열로 변환됩니다:
|
1 2 3 |
{ "securityRoles": ["admin", "user"] } |
- 중첩된 엔티티: 매핑을 싫어하시나요? @ManyToOne 관계에 대해 고민하시나요? 저도 마찬가지입니다. 문서 데이터베이스를 사용하고 있으므로 더 이상 이러한 관계를 작성할 필요가 없으며 중첩된 엔티티도 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" ] } |
리포지토리
이제 리포지토리가 어떤 모습일지 살펴보겠습니다:
|
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: 이 주석 는 현재 리포지토리와 연결된 버킷에 N1QL 기본 인덱스가 있는지 확인합니다.
- @ViewIndexed: 이 주석 를 사용하면 디자인 문서의 이름과 보기 이름은 물론 사용자 지정 맵과 축소 기능을 정의할 수 있습니다.
아래에서 볼 수 있듯이 다음과 같이 모든 Spring 데이터 키워드 를 사용하여 다음과 같이 데이터베이스를 쿼리할 수 있습니다. FindBy, 사이, IsGreaterThan, 좋아요, 존재등
|
1 |
fun findByName(name: String): List<User> |
리포지토리가 확장 중입니다. 카우치베이스 페이징 및 정렬 저장소를 추가하여 쿼리의 페이지 매김을 지정할 수 있습니다. 페이지 가능 매개변수를 추가하세요. 더 강력한 쿼리를 작성해야 하는 경우 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 |
위의 쿼리에는 더 작게 만들기 위해 몇 가지 구문 설탕이 있습니다:
- #(#n1ql.bucket): 이 구문을 사용하면 쿼리에서 버킷 이름을 하드코딩하지 않아도 됩니다.
- #{#n1ql.selectEntity}: 구문-설탕에 SELECT * FROM #(#n1ql.bucket):
- #{#n1ql.filter}: 구문-설탕을 사용하여 문서를 유형별로 필터링하는 것은 엄밀히 말하면 class = 'myPackage.MyClassName' (_class 는 스프링 데이터에서 카우치베이스로 작업할 때 문서에 자동으로 추가되어 유형을 정의하는 어트리뷰트입니다.)
- #{#n1ql.fields} 는 엔티티를 재구성하는 데 필요한 필드 목록(예: SELECT 절의 경우)으로 대체됩니다.
- #{#n1ql.delete} 는 다음에서 삭제 문으로 대체됩니다.
- #{#n1ql.반환} 는 엔티티 재구성에 필요한 반환 절로 대체됩니다.
서비스
저희 서비스는 기본적으로 요청을 리포지토리로 전달하지만, 임시 쿼리를 작성해야 하는 경우 여기로 오세요:
|
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() } |
컨트롤러
마지막으로 휴식을 통해 서비스를 테스트할 컨트롤러도 추가해 보겠습니다:
|
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); } } |
Kotlin으로 통합 테스트 작성
통합 테스트를 올바르게 실행하려면 데이터베이스의 자격 증명을 다음에서 구성하는 것을 잊지 마세요. 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 |
여기에서 테스트가 어떻게 진행되는지 확인할 수 있습니다:
|
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)) } |
Kotlin 및 Maven 종속성
Kotlin은 빠르게 발전하고 있으므로 각 종속 요소의 최신 버전을 사용해야 합니다:
|
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> |
전체 내용을 볼 수 있습니다. pom.xml 여기.