Skip to content

Commit

Permalink
Merge pull request #6 from classmethod/feature/fix-issue-2-add-slice
Browse files Browse the repository at this point in the history
add SliceableRepository
  • Loading branch information
katagiri-kazumune authored Oct 25, 2020
2 parents c9e298c + 56b0195 commit 816d89e
Show file tree
Hide file tree
Showing 17 changed files with 2,188 additions and 0 deletions.
124 changes: 124 additions & 0 deletions spar-wings-spring-data-chunk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# spar-wings-spring-data-chunk

spring-data-mirage と組み合わせて、Mirage と Spring を連携します。

## SQL 発行

用途によって、interface を実装します。代表的な interface は以下の通りです。

* WritableRepository
* INSERT / UPDATE / DELETE を発行する
* ReadableRepository
* SELECT 文を発行する
* `@Id` アノテーションを付与したカラムに対する SELECT 文を発行できます
* ChunkableRepository
* Chunk 形式で結果を受け取る SELECT 文を発行する
* ReadableRepository も含まれます
* SliceableRepository
* Slice 形式で結果を受け取る SELECT 文を発行する
* ReadableRepository も含まれます

用意する sql ファイルは spring-data-mirage を参照してください。

### Chunk とは?

ある集合に対する部分集合を表すリソースです。[参考](https://d1sraz2ju3uqe4.cloudfront.net/section2/example/resource/Chunk.html)
OFFSET を使用しないページネーションを提供します。後ろの部分集合を取得する際もレスポンスが落ちません。

前提条件として `@Id` を付与したカラムで大小比較を行い、ソートを行います。
その為、ORDER BY 句に `@Id` 以外のカラムを指定できません。

### Slice とは?

ある集合に対する部分集合を表すリソースです。
OFFSET を使用するページネーションを提供します。

後ろの部分集合を取得する際はレスポンスが悪化しますが ORDER BY 句に任意のカラムを指定可能です。

**集合の件数が少ない、または、先頭から数ページだけ参照することでユースケースが満たせる場合に限り**こちらを使用しても構いません。

Slice の時に発行する SELECT 文は以下のイメージです。

```sql
SELECT
*
FROM
some_table
WHERE
-- デフォルトの絞り込み条件制御
-- パラメータ有無で絞り込み条件制御
ORDER BY
sort_column /*$direction*/ASC

/*BEGIN*/
LIMIT
/*IF offset != null*/
/*offset*/0,
/*END*/

/*IF size != null*/
/*size*/10
/*END*/
/*END*/
```

Repository の Sliceable パラメータで自動で設定するのは、

* `offset`
* `size`
* `direction`

です。

ソート順は、ユニークになるように指定してください。
例えば、`ORDER BY create_at ASC` にした場合、create_at が同じ値のレコードのソート順は不定の為、Sliceable で全ての値を取得することは保証できません。
以下のように
`ORDER BY create_at ASC, xxx_code ASC`
ソート条件にユニークキーを含めるようにしてください(xxx_code が当該 table のユニークキーの前提です)。

先頭から offset までの読み飛ばし件数が多くなることで API のパフォーマンス劣化を引き起こす可能性が高くなる為、
部分集合の先頭から 2000 件を超えて取得できないように制限します。
具体的には Sliceable パラメータの内容が `page_number * size + size > 2000` の場合、不正リクエストとみなし、

* Controller の引数に Sliceable を設定した時には HttpBadRequestException
* Repository メソッド呼び出し時には InvalidSliceableException
* Controller の引数に Sliceable を設定しない場合はハンドリングをしてください

を throw します。

### INDEX 設計

MySQL の場合、
`ORDER BY create_at ASC, xxx_code ASC`
のソートに INDEX を効かせる為には、`create_at, xxx_code` の複合 INDEX が必要になります。`create_at` だけではソートに INDEX は効きません。
また、MySQL の場合、ソート条件に ASC と DESC が混在していると INDEX が効きません。設計時に留意してください。
(ただし、ソート対象のレコードが少ない場合は INDEX を貼っても使用されないので考慮する必要はありません)
絞り込み条件も存在する場合、適切な INDEX 設計を実施する必要があります。

## Controller で部分集合を取得するリクエストを受け取る

WebMvcConfigurer の実装クラスで addArgumentResolvers を Override し、

* ChunkableHandlerMethodArgumentResolver
* Chunk のリクエストを受け取る場合
* SliceableHandlerMethodArgumentResolver
* Slice のリクエストを受け取る場合

インスタンスを add してください。

イメージは以下の通りです。

```java
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ChunkableHandlerMethodArgumentResolver());
argumentResolvers.add(new SliceableHandlerMethodArgumentResolver());
}
}
```

Controller の引数 Chunkable / Sliceable に `@ChunkableDefault` / `@SliceableDefault` を付与することで、
リクエストが未指定の時に defalut 値を設定したインスタンスを生成します。
3 changes: 3 additions & 0 deletions spar-wings-spring-data-chunk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ dependencies {
compile "org.springframework:spring-web"
compile "org.springframework:spring-context"
compileOnly "org.springframework:spring-tx"
compile project(":spar-wings-httpexceptions")

testCompile "com.fasterxml.jackson.core:jackson-databind"
testCompile "com.jayway.jsonpath:json-path-assert:2.4.0"
testCompile "org.skyscreamer:jsonassert:1.5.0"
testCompile "org.assertj:assertj-core"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.xet.sparwings.spring.data.exceptions;

import lombok.NoArgsConstructor;

/**
* 不正な Sliceable を渡した時の Exception
*/
@NoArgsConstructor
@SuppressWarnings("serial")
public class InvalidSliceableException extends RuntimeException {

/**
* インスタンスを生成する。
*
* @param message 例外メッセージ
* @param cause 起因例外
*/
public InvalidSliceableException(String message, Throwable cause) {
super(message, cause);
}

/**
* インスタンスを生成する。
*
* @param message 例外メッセージ
*/
public InvalidSliceableException(String message) {
super(message);
}

/**
* インスタンスを生成する。
*
* @param cause 起因例外
*/
public InvalidSliceableException(Throwable cause) {
super(cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.xet.sparwings.spring.data.model;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import org.springframework.util.Assert;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

import jp.xet.sparwings.spring.data.slice.Slice;

/**
* Slice のレスポンス.
*
* <p>Controller のレスポンスとして使用します。</p>
*/
@ToString
@EqualsAndHashCode
@XmlRootElement(name = "slicedEntities")
public class SlicedResources<T> {

@Getter
@XmlElement(name = "embedded")
@JsonProperty("_embedded")
private Map<String, Collection<T>> content;

@Getter
@XmlElement(name = "page")
@JsonProperty("page")
private SliceMetadata metadata;


/**
* Creates a {@link SlicedResources} instance with {@link Slice}.
*
* @param key must not be {@code null}.
* @param slice The {@link Slice}
* @param wrapperFunction function coverts {@code U} to {@code T}
*/
public <U> SlicedResources(String key, Slice<U> slice, Function<U, T> wrapperFunction) {
this(key, slice.stream()
.map(wrapperFunction)
.collect(Collectors.toList()), new SliceMetadata(slice));
}

/**
* Creates a {@link SlicedResources} instance with {@link Slice}.
*
* @param key must not be {@code null}.
* @param slice The {@link Slice}
*/
public SlicedResources(String key, Slice<T> slice) {
this(key, slice.getContent(), new SliceMetadata(slice));
}

/**
* Creates a {@link SlicedResources} instance with content collection.
*
* @param key must not be {@code null}.
* @param content The contents
* @since 0.11
*/
public SlicedResources(String key, Collection<T> content) {
this(key, content, new SliceMetadata(content.size(), null, false));
}

/**
* Creates a {@link SlicedResources} instance with iterable and metadata.
*
* @param key must not be {@code null}.
* @param content must not be {@code null}.
* @param metadata must not be {@code null}.
* @since 0.11
*/
public SlicedResources(String key, Collection<T> content, SliceMetadata metadata) {
Assert.notNull(key, "The key must not be null");
Assert.notNull(content, "The content must not be null");
Assert.notNull(metadata, "The metadata must not be null");
this.content = Collections.singletonMap(key, content);
this.metadata = metadata;
}


/**
* Value object for pagination metadata.
*/
@ToString
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public static class SliceMetadata {

@XmlAttribute
@JsonProperty("size")
@Getter(onMethod = @__(@JsonIgnore))
private long size;

@XmlAttribute
@JsonProperty("number")
@Getter(onMethod = @__(@JsonIgnore))
private Integer pageNumber;

@XmlAttribute
@JsonProperty("has_next_page")
@Getter(onMethod = @__(@JsonIgnore))
private Boolean hasNextSlice;


/**
* インスタンスを生成する。
*
* @param slice 当該 Slice
*/
public SliceMetadata(Slice<?> slice) {
this(slice.getContent().size(), slice.getPageNumber(), slice.hasNext());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.xet.sparwings.spring.data.repository;

import java.io.Serializable;

import org.springframework.dao.DataAccessException;
import org.springframework.data.repository.NoRepositoryBean;

import jp.xet.sparwings.spring.data.exceptions.InvalidSliceableException;
import jp.xet.sparwings.spring.data.slice.Slice;
import jp.xet.sparwings.spring.data.slice.Sliceable;

/**
* Repository interface to retrieve slice of entities.
*
* @param <E> the domain type the repository manages
* @param <ID> the type of the id of the entity the repository manages
*/
@NoRepositoryBean
public interface SliceableRepository<E, ID extends Serializable>extends ReadableRepository<E, ID> {

/**
* Returns a {@link Slice} of entities meeting the slicing restriction provided in the {@code Sliceable} object.
*
* @param sliceable slicing information
* @return a slice of entities
* @throws DataAccessException データアクセスエラーが発生した場合
* @throws NullPointerException 引数に{@code null}を与えた場合
* @throws InvalidSliceableException 不正な Sliceable だった場合
*/
Slice<E> findAll(Sliceable sliceable);

}
Loading

0 comments on commit 816d89e

Please sign in to comment.