페이지 매김된 결과 작업: 스캔 및 쿼리 - AWS SDK for Java 2.x

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

페이지 매김된 결과 작업: 스캔 및 쿼리

DynamoDB 향상된 클라이언트 API의 scan, querybatch 메서드는 하나 이상의 페이지가 포함된 응답을 반환합니다. 페이지에는 하나 이상의 항목이 포함되어 있습니다. 코드는 페이지별로 응답을 처리하거나 개별 항목을 처리할 수 있습니다.

동기 클라이언트가 반환하는 페이지 단위 응답은 PageIterable객체를 반환하고, DynamoDbEnhancedAsyncClient 비동기 DynamoDbEnhancedClient 클라이언트가 반환하는 응답은 객체를 반환합니다. PagePublisher

이 단원에서는 페이지를 매긴 결과 처리를 살펴보고 스캔 및 쿼리 API를 사용하는 예제를 제공합니다.

테이블 스캔

SDK의 scan 메서드는 동일한 이름의 DynamoDB 작업에 해당합니다. DynamoDB 향상된 클라이언트 API는 동일한 옵션을 제공하지만 익숙한 객체 모델을 사용하고 페이지 매김을 자동으로 처리합니다.

먼저, 동기 매핑 클래스인 의 scan 메서드를 살펴보면서 PageIterable 인터페이스를 살펴봅니다. DynamoDbTable

동기식 API를 사용

다음 예제는 표현식을 사용하여 반환되는 항목을 필터링하는 scan 메서드를 보여줍니다. ProductCatalog는 앞서 설명한 모델 객체입니다.

설명 줄 2 다음에 표시되는 필터링 표현식은 반환되는 ProductCatalog 항목을 가격 값이 8.00에서 80.00 사이인 항목으로 제한합니다.

또한 이 예에서는 설명 줄 1 뒤에 표시된 attributesToProject 방법을 사용하여 isbn 값을 제외합니다.

scan메서드는 설명 줄 3번 다음에 PageIterable 객체pagedResults, 를 반환합니다. PageIterablestream 메서드는 페이지를 처리하는 데 사용할 수 있는 java.util.Stream 객체를 반환합니다. 이 예제는 페이지 수를 세고 기록합니다.

이 예제는 주석 줄 4부터 시작하여 ProductCatalog 항목 액세스의 두 가지 변형을 보여줍니다. 코멘트 라인 4a 이후의 버전은 각 페이지를 스트리밍하고 각 페이지의 항목을 정렬하고 기록합니다. 주석 4b 이후의 버전은 페이지 이터레이션을 건너뛰고 항목에 직접 액세스합니다.

PageIterable 인터페이스는 두 개의 상위 인터페이스 java.lang.IterableSdkIterable로 결과를 처리하는 다양한 방법을 제공합니다. IterableforEach, iteratorspliterator 메서드를 가져오고 SdkIterablestream 메서드를 가져옵니다.

public static void scanSync(DynamoDbTable<ProductCatalog> productCatalog) { Map<String, AttributeValue> expressionValues = Map.of( ":min_value", numberValue(8.00), ":max_value", numberValue(80.00)); ScanEnhancedRequest request = ScanEnhancedRequest.builder() .consistentRead(true) // 1. the 'attributesToProject()' method allows you to specify which values you want returned. .attributesToProject("id", "title", "authors", "price") // 2. Filter expression limits the items returned that match the provided criteria. .filterExpression(Expression.builder() .expression("price >= :min_value AND price <= :max_value") .expressionValues(expressionValues) .build()) .build(); // 3. A PageIterable object is returned by the scan method. PageIterable<ProductCatalog> pagedResults = productCatalog.scan(request); logger.info("page count: {}", pagedResults.stream().count()); // 4. Log the returned ProductCatalog items using two variations. // 4a. This version sorts and logs the items of each page. pagedResults.stream().forEach(p -> p.items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach( item -> logger.info(item.toString()) )); // 4b. This version sorts and logs all items for all pages. pagedResults.items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach( item -> logger.info(item.toString()) ); }

비동기 API 사용

비동기 scan 메서드는 결과를 PagePublisher 객체로 반환합니다. PagePublisher 인터페이스에는 응답 페이지를 처리하는 데 사용할 수 있는 두 가지 subscribe 메서드가 있습니다. 한 가지 subscribe 메서드는 org.reactivestreams.Publisher 상위 인터페이스에서 가져온 것입니다. 이 첫 번째 옵션을 사용하여 페이지를 처리하려면 subscribe 메서드에 Subscriber 인스턴스를 전달하세요. 다음 예제는 subscribe 메서드 사용을 보여 줍니다.

두 번째 subscribe 방법은 인터페이스에서 가져온 것입니다. SdkPublisher subscribe의 이 버전에서는 Subscriber가 아닌 Consumer를 받아들입니다. 이 subscribe 메서드 변형은 다음 두 번째 예제에 나와 있습니다.

다음 예제는 이전 예제와 동일한 필터 표현식을 사용하는 scan 메서드의 비동기 버전을 보여줍니다.

주석 줄 3번 다음에 DynamoDbAsyncTable.scanPagePublisher 객체를 반환합니다. 다음 줄에서 코드는 org.reactivestreams.Subscriber 인터페이스의 인스턴스를 만들고, ProductCatalogSubscriber는 주석 줄 4 뒤 PagePublisher를 구독합니다.

Subscriber 객체는 ProductCatalogSubscriber 클래스 예제의 주석 줄 8 다음에 있는 onNext 메서드의 각 페이지에서 ProductCatalog 항목을 수집합니다. 항목은 전용 List 변수에 저장되며 ProductCatalogSubscriber.getSubscribedItems() 메서드를 사용하여 호출 코드에서 액세스할 수 있습니다. 이는 주석 줄 5 이후에 호출됩니다.

목록이 검색되면 코드는 모든 ProductCatalog 항목을 가격별로 정렬하고 각 항목을 기록합니다.

CountDownLatchin ProductCatalogSubscriber 클래스는 모든 항목이 목록에 추가될 때까지 호출 스레드를 차단한 다음 5번 설명 줄 이후에 계속합니다.

public static void scanAsync(DynamoDbAsyncTable productCatalog) { ScanEnhancedRequest request = ScanEnhancedRequest.builder() .consistentRead(true) .attributesToProject("id", "title", "authors", "price") .filterExpression(Expression.builder() // 1. :min_value and :max_value are placeholders for the values provided by the map .expression("price >= :min_value AND price <= :max_value") // 2. Two values are needed for the expression and each is supplied as a map entry. .expressionValues( Map.of( ":min_value", numberValue(8.00), ":max_value", numberValue(400_000.00))) .build()) .build(); // 3. A PagePublisher object is returned by the scan method. PagePublisher<ProductCatalog> pagePublisher = productCatalog.scan(request); ProductCatalogSubscriber subscriber = new ProductCatalogSubscriber(); // 4. Subscribe the ProductCatalogSubscriber to the PagePublisher. pagePublisher.subscribe(subscriber); // 5. Retrieve all collected ProductCatalog items accumulated by the subscriber. subscriber.getSubscribedItems().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach(item -> logger.info(item.toString())); // 6. Use a Consumer to work through each page. pagePublisher.subscribe(page -> page .items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach(item -> logger.info(item.toString()))) .join(); // If needed, blocks the subscribe() method thread until it is finished processing. // 7. Use a Consumer to work through each ProductCatalog item. pagePublisher.items() .subscribe(product -> logger.info(product.toString())) .exceptionally(failure -> { logger.error("ERROR - ", failure); return null; }) .join(); // If needed, blocks the subscribe() method thread until it is finished processing. }
private static class ProductCatalogSubscriber implements Subscriber<Page<ProductCatalog>> { private CountDownLatch latch = new CountDownLatch(1); private Subscription subscription; private List<ProductCatalog> itemsFromAllPages = new ArrayList<>(); @Override public void onSubscribe(Subscription sub) { subscription = sub; subscription.request(1L); try { latch.await(); // Called by main thread blocking it until latch is released. } catch (InterruptedException e) { throw new RuntimeException(e); } } @Override public void onNext(Page<ProductCatalog> productCatalogPage) { // 8. Collect all the ProductCatalog instances in the page, then ask the publisher for one more page. itemsFromAllPages.addAll(productCatalogPage.items()); subscription.request(1L); } @Override public void onError(Throwable throwable) { } @Override public void onComplete() { latch.countDown(); // Call by subscription thread; latch releases. } List<ProductCatalog> getSubscribedItems() { return this.itemsFromAllPages; } }

다음 코드 조각 예제는 주석 줄 6을 다음에 Consumer를 받아들이는 PagePublisher.subscribe 메서드의 버전을 사용합니다. Java 람다 매개변수는 각 항목을 추가로 처리하는 페이지를 사용합니다. 이 예에서는 각 페이지가 처리되고 각 페이지의 항목이 정렬된 후 기록됩니다.

// 6. Use a Consumer to work through each page. pagePublisher.subscribe(page -> page .items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach(item -> logger.info(item.toString()))) .join(); // If needed, blocks the subscribe() method thread until it is finished processing.

PagePublisheritems 메서드는 모델 인스턴스를 언래핑하여 코드가 항목을 직접 처리할 수 있도록 합니다. 이 접근 방법은 다음 코드 조각에 나와 있습니다.

// 7. Use a Consumer to work through each ProductCatalog item. pagePublisher.items() .subscribe(product -> logger.info(product.toString())) .exceptionally(failure -> { logger.error("ERROR - ", failure); return null; }) .join(); // If needed, blocks the subscribe() method thread until it is finished processing.

테이블 쿼리

DynamoDbTable 클래스의 query() 메서드는 기본 키 값을 기반으로 항목을 찾습니다. @DynamoDbPartitionKey 주석과 선택적 @DynamoDbSortKey 주석은 데이터 클래스의 기본 키를 정의하는 데 사용됩니다.

query() 메서드에는 제공된 값과 일치하는 항목을 찾는 파티션 키 값이 필요합니다. 테이블에서 정렬 키도 정의하는 경우 추가 비교 조건으로 해당 값을 쿼리에 추가하여 결과를 세부적으로 조정할 수 있습니다.

결과 처리를 제외하고 query()의 동기 버전과 비동기 버전은 동일하게 작동합니다. PagePublisher API와 마찬가지로 query API는 동기 호출의 경우 PageIterable를 반환하고 비동기 호출의 경우 scan를 반환합니다. 이전에는 스캔 단원에서 PageIterablePagePublisher의 사용에 대해 설명했습니다.

Query 메서드 예제

다음 query() 메서드 코드 예제에서는 MovieActor 클래스를 사용합니다. 데이터 클래스는 파티션 키의 movie 속성과 정렬 키의 actor 속성으로 구성된 복합 기본 키를 정의합니다.

또한 클래스는 acting_award_year라는 글로벌 보조 인덱스를 사용한다는 신호를 보냅니다. 데이터 클래스는 파티션 키의 actingaward 속성과 정렬 키의 actingyear 속성으로 구성된 복합 기본 키를 정의합니다. 이 항목의 뒷부분에서 인덱스를 만들고 사용하는 방법을 보여줄 때는 acting_award_year 인덱스를 참조하겠습니다.

package org.example.tests.model; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; import java.util.Objects; @DynamoDbBean public class MovieActor implements Comparable<MovieActor> { private String movieName; private String actorName; private String actingAward; private Integer actingYear; private String actingSchoolName; @DynamoDbPartitionKey @DynamoDbAttribute("movie") public String getMovieName() { return movieName; } public void setMovieName(String movieName) { this.movieName = movieName; } @DynamoDbSortKey @DynamoDbAttribute("actor") public String getActorName() { return actorName; } public void setActorName(String actorName) { this.actorName = actorName; } @DynamoDbSecondaryPartitionKey(indexNames = "acting_award_year") @DynamoDbAttribute("actingaward") public String getActingAward() { return actingAward; } public void setActingAward(String actingAward) { this.actingAward = actingAward; } @DynamoDbSecondarySortKey(indexNames = {"acting_award_year", "movie_year"}) @DynamoDbAttribute("actingyear") public Integer getActingYear() { return actingYear; } public void setActingYear(Integer actingYear) { this.actingYear = actingYear; } @DynamoDbAttribute("actingschoolname") public String getActingSchoolName() { return actingSchoolName; } public void setActingSchoolName(String actingSchoolName) { this.actingSchoolName = actingSchoolName; } @Override public String toString() { final StringBuffer sb = new StringBuffer("MovieActor{"); sb.append("movieName='").append(movieName).append('\''); sb.append(", actorName='").append(actorName).append('\''); sb.append(", actingAward='").append(actingAward).append('\''); sb.append(", actingYear=").append(actingYear); sb.append(", actingSchoolName='").append(actingSchoolName).append('\''); sb.append('}'); return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MovieActor that = (MovieActor) o; return Objects.equals(movieName, that.movieName) && Objects.equals(actorName, that.actorName) && Objects.equals(actingAward, that.actingAward) && Objects.equals(actingYear, that.actingYear) && Objects.equals(actingSchoolName, that.actingSchoolName); } @Override public int hashCode() { return Objects.hash(movieName, actorName, actingAward, actingYear, actingSchoolName); } @Override public int compareTo(MovieActor o) { if (this.movieName.compareTo(o.movieName) != 0){ return this.movieName.compareTo(o.movieName); } else { return this.actorName.compareTo(o.actorName); } } }

다음 항목에 대한 쿼리를 따르는 코드 예제.

MovieActor{movieName='movie01', actorName='actor0', actingAward='actingaward0', actingYear=2001, actingSchoolName='null'} MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'} MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'} MovieActor{movieName='movie01', actorName='actor3', actingAward='actingaward3', actingYear=2001, actingSchoolName='null'} MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'} MovieActor{movieName='movie02', actorName='actor0', actingAward='actingaward0', actingYear=2002, actingSchoolName='null'} MovieActor{movieName='movie02', actorName='actor1', actingAward='actingaward1', actingYear=2002, actingSchoolName='actingschool1'} MovieActor{movieName='movie02', actorName='actor2', actingAward='actingaward2', actingYear=2002, actingSchoolName='actingschool2'} MovieActor{movieName='movie02', actorName='actor3', actingAward='actingaward3', actingYear=2002, actingSchoolName='null'} MovieActor{movieName='movie02', actorName='actor4', actingAward='actingaward4', actingYear=2002, actingSchoolName='actingschool4'} MovieActor{movieName='movie03', actorName='actor0', actingAward='actingaward0', actingYear=2003, actingSchoolName='null'} MovieActor{movieName='movie03', actorName='actor1', actingAward='actingaward1', actingYear=2003, actingSchoolName='actingschool1'} MovieActor{movieName='movie03', actorName='actor2', actingAward='actingaward2', actingYear=2003, actingSchoolName='actingschool2'} MovieActor{movieName='movie03', actorName='actor3', actingAward='actingaward3', actingYear=2003, actingSchoolName='null'} MovieActor{movieName='movie03', actorName='actor4', actingAward='actingaward4', actingYear=2003, actingSchoolName='actingschool4'}

다음 코드는 두 QueryConditional인스턴스를 정의합니다. QueryConditionals키 값 (파티션 키만 사용하거나 정렬 키와 조합하여 사용) 을 사용하며 DynamoDB 서비스 API의 키 조건식과 일치합니다. 이 예제는 주석 줄 1 다음에 파티션 값이 movie01인 항목과 일치하는 keyEqual 인스턴스를 정의합니다.

또한 이 예제는 주석 줄 2 뒤에 actingschoolname이 없는 항목을 필터링하는 필터 표현식을 정의합니다.

이 예제는 설명 줄 3번 다음에 코드가 메서드에 전달하는 QueryEnhancedRequest인스턴스를 보여줍니다. DynamoDbTable.query() 이 객체는 SDK가 DynamoDB 서비스에 대한 요청을 생성하는 데 사용하는 주요 조건과 필터를 결합합니다.

public static void query(DynamoDbTable movieActorTable) { // 1. Define a QueryConditional instance to return items matching a partition value. QueryConditional keyEqual = QueryConditional.keyEqualTo(b -> b.partitionValue("movie01")); // 1a. Define a QueryConditional that adds a sort key criteria to the partition value criteria. QueryConditional sortGreaterThanOrEqualTo = QueryConditional.sortGreaterThanOrEqualTo(b -> b.partitionValue("movie01").sortValue("actor2")); // 2. Define a filter expression that filters out items whose attribute value is null. final Expression filterOutNoActingschoolname = Expression.builder().expression("attribute_exists(actingschoolname)").build(); // 3. Build the query request. QueryEnhancedRequest tableQuery = QueryEnhancedRequest.builder() .queryConditional(keyEqual) .filterExpression(filterOutNoActingschoolname) .build(); // 4. Perform the query. PageIterable<MovieActor> pagedResults = movieActorTable.query(tableQuery); logger.info("page count: {}", pagedResults.stream().count()); // Log number of pages. pagedResults.items().stream() .sorted() .forEach( item -> logger.info(item.toString()) // Log the sorted list of items. );

다음은 메서드 실행 결과입니다. 출력에는 movie01movieName 값이 있는 항목이 표시되고 null과 같은 actingSchoolName이 있는 항목은 표시되지 않습니다.

2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:46 - page count: 1 2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'} 2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'} 2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}

이전에 주석 줄 3 다음에 표시된 다음 쿼리 요청 변형에서는 코드가 keyEqual QueryConditional를 주석 줄 1a 뒤에 정의된 sortGreaterThanOrEqualTo QueryConditional로 대체합니다. 다음 코드는 또한 필터 표현식을 제거합니다.

QueryEnhancedRequest tableQuery = QueryEnhancedRequest.builder() .queryConditional(sortGreaterThanOrEqualTo)

이 테이블에는 복합 기본 키가 있으므로 모든 QueryConditional 인스턴스에는 파티션 키 값이 필요합니다. sort...로 시작하는 QueryConditional 메서드는 정 렬 키가 필요함을 나타냅니다. 결과는 정렬되지 않습니다.

다음 출력은 쿼리 결과를 표시합니다. 쿼리는 movieName 값이 movie01과 같은 항목을 반환하고 actor2보다 크거나 같은 actorName 값을 가진 항목만 반환합니다. 필터가 제거되었으므로 쿼리는 actingSchoolName 속성 값이 없는 항목을 반환합니다.

2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:46 - page count: 1 2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'} 2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor3', actingAward='actingaward3', actingYear=2001, actingSchoolName='null'} 2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}