작년에 저는 Kotlin을 배우기 시작하면서 Java 애플리케이션을 변환하는 것이 얼마나 쉬운지 놀랐습니다. IntelliJ와 몇 가지 다른 IDE는 자동 변환을 위한 훌륭한 도구를 제공하며, 몇 가지 조정만 하면 훨씬 간결하고 오류가 적은 코드를 완성할 수 있습니다.
그래서 제가 가장 좋아하는 새로운 조합을 보여주기 위해 샘플 애플리케이션을 만들기로 했습니다: Kotlin입니다, 스프링 부트, 스프링 데이터, 카우치베이스:
사용자 프로필 서비스 만들기
여기에서 전체 프로젝트를 복제할 수 있습니다:
https://github.com/couchbaselabs/try-cb-kotlin
메인 클래스를 만드는 것부터 시작하겠습니다:
1 2 3 4 5 6 |
@스프링 부팅 애플리케이션 열기 클래스 Kotlin데모애플리케이션 재미 메인(args: 배열<문자열>) { 스프링 애플리케이션.실행(Kotlin데모애플리케이션::클래스.자바, *args) } |
참고: 클래스는 다음과 같아야 합니다. 열기 그렇지 않으면 다음과 같은 오류가 발생합니다:
1 2 3 4 |
org.스프링 프레임워크.콩.공장.구문 분석.빈 정의 파싱 예외: 구성 문제: @구성 클래스 'KotlinDemoApplication' may not be final. 제거 의 final 수정자 에 계속. 위반 리소스: com.카우치베이스.Kotlin데모애플리케이션 에서 org.스프링 프레임워크.콩.공장.구문 분석.실패 빠른 문제 리포터.오류(실패 빠른 문제 리포터.자바:70) ~[봄-콩-4.3.13.RELEASE.jar:4.3.13.릴리스] 에서 org.스프링 프레임워크.컨텍스트.주석.구성 클래스.유효성 검사(구성 클래스.자바:214) ~[봄-컨텍스트-4.3.13.RELEASE.jar:4.3.13.릴리스] |
다음은 사용자 엔티티로, 다음과 매우 유사합니다. 자바:
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 |
@문서 클래스 사용자(): 기본 엔티티() { 생성자(id: 문자열, 이름: 문자열, 주소: 주소, 환경설정: 목록<기본 설정>, 보안 역할: 목록<문자열>): 이(){ 이.id = id; 이.이름 = 이름; 이.주소 = 주소; 이.환경설정 = 환경설정; 이.보안 역할 = 보안 역할; } @Id var id: 문자열? = null @NotNull var 이름: 문자열? = null @필드 var 주소: 주소? = null @필드 var 환경설정: 목록<기본 설정> = 빈 목록() @필드 var 보안 역할: 목록<문자열> = 빈 목록() } |
- @Document: 엔티티를 정의하는 카우치베이스의 어노테이션은 다음과 유사합니다. 엔티티 를 추가합니다. 카우치베이스는 자동으로 다음과 같은 프로퍼티를 추가합니다. _class 를 문서에 추가하여 문서 유형으로 사용하세요.
- @Id: 문서의 핵심
- @Field: JPA의 주석과 유사한 Couchbase의 주석은 다음과 같습니다. 칼럼. 이 기능은 필수는 아니지만 사용을 권장합니다.
Couchbase에서 어트리뷰트 매핑은 매우 간단합니다. JSON의 해당 구조에 직접 매핑됩니다:
- 간단한 속성: JSON으로 간단하게 매핑할 수 있습니다:
1 2 3 4 |
{ "id": "user::1", "name": "데니스 로사" } |
- 배열: 예상할 수 있듯이 다음과 같은 배열은 보안 역할 은 JSON 배열로 변환됩니다:
1 2 3 |
{ "보안 역할": ["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":"데니스 로사", "주소":{ "거리 이름":"어딘가의 거리", "houseNumber":"42", "postalCode":"81234", "city":"Munich", "country":"DE" }, "환경설정":[ { "name":"lang", "value":"EN" } ], "보안 역할":[ "admin", "user" ] } |
리포지토리
이제 리포지토리가 어떤 모습일지 살펴보겠습니다:
1 2 3 4 5 6 7 8 9 10 11 12 |
@N1qlPrimaryIndexed @인덱싱된 보기(디자인 문서 = "user") 인터페이스 사용자 저장소 : 카우치베이스 페이징 및 정렬 저장소<사용자, 문자열> { 재미 findByName(이름: 문자열): 목록<사용자> @쿼리("#{#n1ql.selectEntity} 여기서 #{#n1ql.filter} 및 모든 기본 설정 IN " + "환경 설정 만족 preference.name = $1 END") 재미 찾기 사용자 기본 설정 이름(이름: 문자열): 목록<사용자> @쿼리("#{#n1ql.selectEntity} 여기서 #{#n1ql.filter} 및 meta().id = $1 및 ARRAY_CONTAINS(securityRoles, $2)") 재미 hasRole(userId: 문자열, 역할: 문자열): 사용자 } |
- @N1qlPrimaryIndexed: 이 주석 는 현재 리포지토리와 연결된 버킷에 N1QL 기본 인덱스가 있는지 확인합니다.
- @ViewIndexed: 이 주석 를 사용하면 디자인 문서의 이름과 보기 이름은 물론 사용자 지정 맵과 축소 기능을 정의할 수 있습니다.
아래에서 볼 수 있듯이 다음과 같이 모든 Spring 데이터 키워드 를 사용하여 다음과 같이 데이터베이스를 쿼리할 수 있습니다. FindBy, 사이, IsGreaterThan, 좋아요, 존재등
1 |
재미 findByName(이름: 문자열): 목록<사용자> |
리포지토리가 확장 중입니다. 카우치베이스 페이징 및 정렬 저장소를 추가하여 쿼리의 페이지 매김을 지정할 수 있습니다. 페이지 가능 매개변수를 추가하세요. 더 강력한 쿼리를 작성해야 하는 경우 N1QL을 사용할 수도 있습니다:
1 2 3 4 5 |
@쿼리("#{#n1ql.selectEntity} 여기서 #{#n1ql.filter} 및 모든 기본 설정 IN " + "환경 설정 만족 preference.name = $1 END") 재미 찾기 사용자 기본 설정 이름(이름: 문자열): 목록<사용자> @쿼리("#{#n1ql.selectEntity} 여기서 #{#n1ql.filter} 및 meta().id = $1 및 ARRAY_CONTAINS(securityRoles, $2)") 재미 hasRole(userId: 문자열, 역할: 문자열): 사용자 |
위의 쿼리에는 더 작게 만들기 위해 몇 가지 구문 설탕이 있습니다:
- #(#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 |
@서비스 클래스 사용자 서비스 { @자동 유선 lateinit var 사용자 저장소: 사용자 저장소; 재미 findByName(이름: 문자열): 목록<사용자> = 사용자 저장소.findByName(이름) 재미 findById(userId: 문자열) = 사용자 저장소.findOne(userId) 재미 저장(@유효 사용자: 사용자) = 사용자 저장소.저장(사용자) 재미 찾기 사용자 기본 설정 이름(이름: 문자열): 목록<사용자> = 사용자 저장소.찾기 사용자 기본 설정 이름(이름) 재미 hasRole(userId: 문자열, 역할: 문자열): 부울 { 반환 사용자 저장소.hasRole(userId, 역할) != null } /** * 임시 쿼리의 예 */ 재미 주소로 사용자 찾기(거리 이름: 문자열 = "", 숫자: 문자열 = "", 우편번호: 문자열 = "", 도시: 문자열 = "", 국가: 문자열 = ""): 목록<사용자> { var 쿼리 = "SELECT meta(b).id as id, b.* FROM " + getBucketName() + " b WHERE b._class = '" + 사용자::클래스.자바.getName() + "' " 만약 (!거리 이름.isNullOrBlank()) 쿼리 += " 및 b.address.streetName = '$streetName' " 만약 (!숫자.isNullOrBlank()) 쿼리 += " 및 b.address.houseNumber = '$number' " 만약 (!우편번호.isNullOrBlank()) 쿼리 += " 및 b.address.postalCode = '$postalCode' " 만약 (!도시.isNullOrBlank()) 쿼리 += " 및 b.address.city = '$city' " 만약 (!국가.isNullOrBlank()) 쿼리 += " 및 b.address.country = '$country' " val 매개변수 = N1qlParams.빌드().일관성(스캔 일관성.REQUEST_PLUS).adhoc(true) val paramQuery = N1qlQuery.매개변수화(쿼리, JsonObject.create(), 매개변수) 반환 사용자 저장소.getCouchbaseOperations().findByN1QLProjection(paramQuery, 사용자::클래스.자바) } 재미 getBucketName() = 사용자 저장소.getCouchbaseOperations().카우치베이스 버킷 가져오기().버킷 관리자().정보().이름() } |
컨트롤러
마지막으로 휴식을 통해 서비스를 테스트할 컨트롤러도 추가해 보겠습니다:
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 @요청 매핑("/api/user") 클래스 사용자 컨트롤러 { @자동 유선 lateinit var 사용자 서비스: 사용자 서비스 @GetMapping(값 = "/{id}") 재미 findById(@PathParam("id") id: 문자열) = 사용자 서비스.findById(id) @GetMapping(값 = "/preference") 재미 findPreference(@요청 매개변수("name") 이름: 문자열): 목록<사용자> { 반환 사용자 서비스.찾기 사용자 기본 설정 이름(이름) } @GetMapping(값 = "/find") 재미 사용자 이름 찾기(@요청 매개변수("name") 이름: 문자열): 목록<사용자> { 반환 사용자 서비스.findByName(이름) } @포스트 매핑(값 = "/저장") 재미 사용자 이름 찾기(@RequestBody 사용자: 사용자) = 사용자 서비스.저장(사용자) @GetMapping(값 = "/findByAddress") 재미 주소로 찾기(@요청 매개변수("거리 이름", 기본값 = "") 거리 이름: 문자열, @요청 매개변수("숫자", 기본값 = "") 숫자: 문자열, @요청 매개변수("postalCode", 기본값 = "") 우편번호: 문자열, @요청 매개변수("city", 기본값 = "") 도시: 문자열, @요청 매개변수("country", 기본값 = "") 국가: 문자열): 목록<사용자> { 반환 사용자 서비스.주소로 사용자 찾기(거리 이름, 숫자, 우편번호, 도시, 국가); } } |
Kotlin으로 통합 테스트 작성
통합 테스트를 올바르게 실행하려면 데이터베이스의 자격 증명을 다음에서 구성하는 것을 잊지 마세요. application.properties file:
1 2 3 4 |
봄.카우치베이스.부트스트랩-호스트=localhost 봄.카우치베이스.버킷.이름=테스트 봄.카우치베이스.버킷.비밀번호=일부 비밀번호 봄.데이터.카우치베이스.자동-색인=true |
여기에서 테스트가 어떻게 진행되는지 확인할 수 있습니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@테스트 재미 testComposedAddress() { val 주소1 = 주소("street1", "1", "0000", "산토 안드레", "br") val 주소2 = 주소("street1", "2", "0000", "산토 안드레", "br") val 주소3 = 주소("street2", "12", "1111", "munich", "de") 사용자 서비스.저장(사용자(USER_1, "user1", 주소1, 빈 목록(), 빈 목록())) 사용자 서비스.저장(사용자("user::2", "user2", 주소2, 빈 목록(), 빈 목록())) 사용자 서비스.저장(사용자("user::3", "user3", 주소3, 빈 목록(), 빈 목록())) var 사용자 = 사용자 서비스.주소로 사용자 찾기(거리 이름 = "street1") assertThat(사용자, hasSize<모든>(2)) 사용자 = 사용자 서비스.주소로 사용자 찾기(거리 이름 = "street1", 숫자= "1") assertThat(사용자, hasSize<모든>(1)) 사용자 = 사용자 서비스.주소로 사용자 찾기(국가 = "de") assertThat(사용자, hasSize<모든>(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>잭슨-모듈-코틀린</artifactId> <version>2.9.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>스프링-부팅-스타터-데이터-카우치베이스</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>스프링-부팅-스타터-데이터-휴식</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>봄-부팅-스타터-테스트</artifactId> <scope>테스트</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>햄크레스트 라이브러리</artifactId> <version>1.3</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>스프링 데이터 카우치베이스</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>테스트</scope> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>코틀린-메이븐-올로오픈</artifactId> <version>1.2.41</version> </dependency> </dependencies> |
전체 내용을 볼 수 있습니다. pom.xml 여기.