서버측 Kotlin은 GraphiQL을 위한 DGS 프레임워크의 동작을 확인합니다

62888 단어 GraphQLKotlintech

개시하다


코틀린에서 그래픽QL을 사용하는 방법DGS framework을 찾아보니 네스트된 질의가 어떻게 실행되는지 확인하고 싶어 만져봤다.
저는 지금 Kotlin과GraphiQL에 대해 공부하고 있습니다.

DGS 프레임워크 정보


DGS framework는 넷플릭스 오픈소스화[1] 스프링부트에서 동작하는 그래픽QL의 모드 우선 프레임워크다.넷플릭스 사내에서는 정식 근무[2][3]인 것 같다.

DGS 프레임워크 가져오기


개발 환경에는 IntelliJ IDEA가 사용됩니다.GraphiQL 플러그인DGS 플러그인가 설치되어 있습니다.
DGS 설치 등은 공식 홈페이지Getting Started에 따라 진행됐다.기본적으로 문서의 패턴과 코드에 따라 이동하지만 코드는 패키지와 import을 추가해야 합니다.또한spring-boot-devtools 설정을 통해 자동으로 재부팅할 수 있어 편리하다[4].
일시적으로 이렇게 이동할 수 있지만 문서에도
Note that we have a Codegen plugin that can do this automatically, but in this guide we'll manually write the classes.
위에서 말한 바와 같이 코드를 자동으로 생성하는 플러그인이 하나 있는데 이 덕분에 모델을 우선적으로 실현할 수 있다. 그래서 다음에 나는 이 플러그인을 설치했다. 그러나 이 설정에서 아래의 몇 가지는 이미 끝났다.
  • Kotlin으로 플러그인 설정 쓰기
  • Resolver 샘플 코드가 생성되지 않음
  • 1.Kotlin으로 플러그인 설정하는 방법 몰라


    문서에는 플러그인 설정 방법이 적혀 있지만 Groovy로 기술한 것으로 보인다. Gradle 설정이 Groovy를 사용한다면 그대로 이동했을 수도 있고 (실제로 시도하지 않았을 수도 있음) Kotlin을 사용할 때Gradle 설정도 Kotlin으로 기술했다.그냥 가만히 있어.코틀린을 어떻게 쓰는지 모르겠지만 샘플로 소개된 창고을 보니 설정된 부분[5]과 창고 문제에 대해 비슷한 대화[6]가 있더라고요. 그래서 이런 설정을 참고해서 움직였어요.구체적인 내용은 다음과 같다.schemaPaths 기본 설정은 문제가 없는 것 같아서 문서에 설정되었지만 아래는 설정하지 않았습니다.
    tasks.withType<com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask> {
        packageName = "com.example.demo.generated"
        generateClient = true
    }
    

    2. Resolver의 샘플 코드가 생성되지 않았습니다.


    문서
    generateJava generates the data fetchers and places them in build/generated-examples.
    이렇게 쓰여 있지만 움직여도 샘플 코드가 생성되지 않아 찾아보니 언어가 코톨린이 되면 생성되지 않는다[7].플러그인 설정에 항목language이 있기 때문에 지정java 후 실행하여 샘플 코드를 생성했습니다.생성된 샘플 코드를 확인하면 문서에도
    NOTE: generateJava does NOT add the data fetchers that it generates to your project’s sources. These fetchers serve mainly as a basic boilerplate code that require further implementation from you.
    위에 쓰여 있지만 간단한 코드라 Kottlin에서 이동하지 않아도 문제없다.또한 문서에 생성된 디렉터리generated-examples라고 쓰여 있지만 생성된 디렉터리generated/sources/dgs-codegen-generated-examples는 경로를 설정할 수 있는 설정 항목exampleOutputDir도 설정에 없다.

    구체적인 동작의 확인


    DGS 프레임워크의 이동 방법을 알았기 때문에 구체적인 동작을 확인하기 위해 다음과 같은 행위의 코드를 적었다.한마디로 도서 관리 앱이다.
  • 토큰을 얻기 위해 로그인 가능
  • 로그오프 가능
  • 사용자는 이미 등록된 책을 빌릴 수 있다
  • 사용자는 빌려온 책을 반납할 수 있다
  • 사용자는 자신이 빌려온 책의 일람표를 얻을 수 있다
  • 사용자는 자신이 과거에 빌린 책의 일람표를 얻을 수 있다
  • 본 차트
  • 도서 대출을 받을 수 있는 사용자
  • 다만 반환값의 코드가 실제로 복잡해지면 어떻게 될지 모르기 때문에 일부러 이를 순환 인용 모델로 삼고 데이터의 저장 방법도 이벤트만 보존한 뒤 수치를 계산한다.실제 코드는 다음과 같습니다.이동하는 코드는 전체적으로 GitHub에 있다.
    type Query {
        me(token: String!): User!
        books: [Book!]!
    }
    
    type Mutation {
        signIn(id: ID!): String!
        signOut(token: String!): Boolean!
        rentBook(token: String!, id: ID!): Book!
        returnBook(token: String!, id: ID!): Book!
    }
    
    type User {
        id: ID!
        name: String!
        has: [Book!]!
        history: [Book!]!
    }
    
    type Book {
        id: ID!
        title: String!
        rentedBy: User
    }
    
    package com.example.demo
    
    import org.springframework.boot.autoconfigure.SpringBootApplication
    import org.springframework.boot.runApplication
    
    @SpringBootApplication
    class DemoApplication
    
    fun main(args: Array<String>) {
        runApplication<DemoApplication>(*args)
    }
    
    data class User(val id: String, val name: String)
    data class Book(val id: String, val title: String)
    data class SignedInUser(val id: String, val token: String)
    data class RentEvent(val userId: String, val bookId: String, val rentedAt: Int)
    data class ReturnEvent(val userId: String, val bookId: String, val returnedAt: Int)
    
    object Resource {
        val users = listOf(User("1", "user 1"), User("2", "user 2"))
        val books = listOf(Book("1", "book 1"), Book("2", "book 2"), Book("3", "book 3"))
    
        val signedInUsers = mutableListOf<SignedInUser>()
        val rentEvents = mutableListOf<RentEvent>()
        val returnEvents = mutableListOf<ReturnEvent>()
    }
    
    package com.example.demo.datafetchers
    
    import com.example.demo.Resource
    import com.example.demo.SignedInUser
    import com.example.demo.generated.types.Book
    import com.example.demo.generated.types.User
    import com.netflix.graphql.dgs.*
    import java.security.SecureRandom
    
    @DgsComponent
    class UsersDataFetcher {
        @DgsQuery
        fun me(@InputArgument token: String): User {
            val userId = Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
            val user = Resource.users.find { it.id == userId }!!
    
            return User(user.id, user.name, listOf(), listOf())
        }
    
        @DgsData(parentType = "User")
        fun has(dfe: DgsDataFetchingEnvironment): List<Book> {
            val user = dfe.getSource<User>()
    
            return Resource.rentEvents
                .filter { it.userId == user.id }
                .groupBy { it.bookId }
                .map { rentEventsByBookId ->
                    rentEventsByBookId.value.maxByOrNull { it.rentedAt }!!
                }
                .filter { rentEvent ->
                    Resource.returnEvents.find { it.userId == user.id && it.bookId == rentEvent.bookId && it.returnedAt >= rentEvent.rentedAt } == null
                }
                .map { it.bookId }
                .fold(listOf<String>()) { bookIds, bookId -> bookIds + bookId }
                .let { bookIds -> Resource.books.filter { bookIds.contains(it.id) } }
                .map { Book(it.id, it.title) }
        }
    
        @DgsData(parentType = "User")
        fun history(dfe: DgsDataFetchingEnvironment): List<Book> {
            val user = dfe.getSource<User>()
    
            return Resource.rentEvents
                .filter { it.userId == user.id }
                .map { rentEvent -> Resource.books.find { it.id == rentEvent.bookId }!! }
                .map { Book(it.id, it.title) }
        }
    
        @DgsMutation
        fun signIn(@InputArgument id: String): String {
            val user = Resource.users.find { it.id == id } ?: throw IllegalArgumentException(id)
    
            val token = SecureRandom().nextDouble().toBigDecimal().toPlainString()
            Resource.signedInUsers.add(SignedInUser(user.id, token))
    
            return token
        }
    
        @DgsMutation
        fun signOut(@InputArgument token: String): Boolean {
            Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
            Resource.signedInUsers.removeIf { it.token == token }
    
            return true
        }
    }
    
    package com.example.demo.datafetchers
    
    import com.example.demo.RentEvent
    import com.example.demo.Resource
    import com.example.demo.ReturnEvent
    import com.example.demo.generated.types.Book
    import com.example.demo.generated.types.User
    import com.netflix.graphql.dgs.*
    
    
    @DgsComponent
    class BooksDataFetcher {
        @DgsQuery
        fun books(): List<Book> {
            return Resource.books.map { Book(it.id, it.title) }
        }
    
        @DgsData(parentType = "Book")
        fun rentedBy(dfe: DgsDataFetchingEnvironment): User? {
            val book = dfe.getSource<Book>()
    
            val lastRentEvent = Resource.rentEvents.filter { it.bookId == book.id }.maxByOrNull { it.rentedAt }
            val lastReturnEvent = Resource.returnEvents.filter { it.bookId == book.id }.maxByOrNull { it.returnedAt }
    
            return lastRentEvent?.let {
                if (lastReturnEvent == null || lastRentEvent.rentedAt > lastReturnEvent.returnedAt) {
                    Resource.users
                        .find { it.id == lastRentEvent.userId }
                        ?.let { User(it.id, it.name, listOf(), listOf()) }
                } else {
                    null
                }
            }
        }
    
        @DgsMutation
        fun rentBook(@InputArgument token: String, @InputArgument id: String): Book {
            val userId = Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
            val book = Resource.books.find { it.id == id } ?: throw IllegalArgumentException(id)
    
            val lastRentEvent = Resource.rentEvents.filter { it.bookId == book.id }.maxByOrNull { it.rentedAt }
            val lastReturnEvent = Resource.returnEvents.filter { it.bookId == book.id }.maxByOrNull { it.returnedAt }
    
            if (lastRentEvent != null && lastReturnEvent == null || lastRentEvent != null && lastReturnEvent != null && lastRentEvent.rentedAt > lastReturnEvent.returnedAt) {
                throw IllegalArgumentException(id)
            }
    
            Resource.rentEvents.add(RentEvent(userId, id, (System.currentTimeMillis() / 1000).toInt()))
    
            return Book(book.id, book.title)
        }
    
        @DgsMutation
        fun returnBook(@InputArgument token: String, @InputArgument id: String): Book {
            val userId = Resource.signedInUsers.find { it.token == token }?.id ?: throw IllegalArgumentException(token)
            val book = Resource.books.find { it.id == id } ?: throw IllegalArgumentException(id)
    
            val lastRentEvent = Resource.rentEvents.filter { it.bookId == book.id }.maxByOrNull { it.rentedAt }
            val lastReturnEvent = Resource.returnEvents.filter { it.bookId == book.id }.maxByOrNull { it.returnedAt }
    
            if (lastRentEvent == null || lastReturnEvent != null && lastRentEvent.rentedAt <= lastReturnEvent.returnedAt) {
                throw IllegalArgumentException(id)
            }
    
            Resource.returnEvents.add(ReturnEvent(userId, id, (System.currentTimeMillis() / 1000).toInt()))
    
            return Book(book.id, book.title)
        }
    }
    
    에는 DGS 프레임워크의 모조 사용 방법여기.에 대한 설명이 있다.또한 네스트된 필드의 처리는 여기.에 설명되어 있습니다.중첩 필드에는 교차 값을 직접 사용하고 확장 유형을 사용하며 값을 가지는 모드가 있습니다. (모드에 없는 값은 응답할 때 버려집니다.)이번에는 가장 간단하게 지나온 값을 직접 사용하자.설치 과정에서 플러그인 필드를 쉽게 찾을 수 없지만, 목록이 모드의 비목록 필드로 되돌아갔습니다. 형식이 맞지 않아 버려졌을 수도 있습니다.합쳐서 정상적으로 작동할 수 있어요.
    이 코드에 대해
    {
      books {
        id
      }
    }
    
    이런 조회
    {
      "data": {
        "books": [
          {
            "id": "1"
          },
          {
            "id": "2"
          },
          {
            "id": "3"
          }
        ]
      }
    }
    
    등 데이터가 되돌아온다.로그인해서 많이 빌렸어요.
    query Me($token: String!) {
      me(token: $token) {
        id
        name
        has {
          id
          title
          rentedBy {
            id
          }
        }
        history {
          id
          title
          rentedBy {
            id
            name
            has {
              id
              title
              rentedBy {
                name
              }
            }
          }
        }
      }
    }
    
    이런 조회
    {
      "data": {
        "me": {
          "id": "1",
          "name": "user 1",
          "has": [
            {
              "id": "2",
              "title": "book 2",
              "rentedBy": {
                "id": "1"
              }
            }
          ],
          "history": [
            {
              "id": "1",
              "title": "book 1",
              "rentedBy": null
            },
            {
              "id": "2",
              "title": "book 2",
              "rentedBy": {
                "id": "1",
                "name": "user 1",
                "has": [
                  {
                    "id": "2",
                    "title": "book 2",
                    "rentedBy": {
                      "name": "user 1"
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
    
    등 데이터가 되돌아온다.이 때 다음과 같은 순서로 처리가 실행되었다.
  • me of UsersDataFetcher
  • has of UsersDataFetcher
  • rentedBy of BooksDataFetcher
  • history of UsersDataFetcher
  • rentedBy of BooksDataFetcher
  • rentedBy of BooksDataFetcher
  • has of UsersDataFetcher
  • rentedBy of BooksDataFetcher
  • 끼워 넣은 필드마다 잘 호출된 것 같습니다.또한Query뿐만 아니라 Mutation의 경우도parentType에 따라 처리된다.has: [Book!]!와 같은 필수 유형은 어떻게 해야 좋을까요? 어쨌든 빈 목록을 설정한 다음에 플러그인 필드에 값을 설정하면 그 값으로 덮어씁니다.
    이번엔 그 정도는 아니지만 N+1 질문 문서도 있기 때문에 대처할 수 있다고 생각한다.

    끝말


    이번에 DGS 프레임워크를 사용해 실제로 모바일 코드를 통해 어떤 동작을 하는지 확인했다.서버 쪽 그래픽QL 라이브러리를 만져본 적이 없어서 실제로 어떻게 돌아가는지 상상도 못 했는데 이번에 한번 돌려봤는데 실현된 인상이 들어서 다행이에요.
    각주
    Open Sourcing the Netflix Domain Graph Service Framework: GraphQL for Spring Boot | by Netflix Technology Blog | Netflix TechBlog ↩︎
    Home - DGS Framework의 Q&A Is it production ready?섹션 ↩︎
    Netflix Open Sources Their Domain Graph Service Framework: GraphQL for Spring Boot ↩︎
    그대로 움직일 수 없기 때문에 설정IntelliJ IDEA 202.2 이후 Spring Boot DevTools의 LiveReload 설정↩︎
    https://github.com/Netflix/dgs-examples-kotlin/blob/56e7371ffad312a9d59f1318d04ab5426515e842/build.gradle.kts ↩︎
    https://github.com/Netflix/dgs-codegen/issues?q=generateJava ↩︎
    코드https://github.com/Netflix/dgs-codegen/blob/c790be670061de7544981bc2c72d81de92900116/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/CodeGen.kt를 추적한 후 Java↩︎만 생성

    좋은 웹페이지 즐겨찾기