-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from classmethod/feature/fix-issue-2-add-slice
add SliceableRepository
- Loading branch information
Showing
17 changed files
with
2,188 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 値を設定したインスタンスを生成します。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
...hunk/src/main/java/jp/xet/sparwings/spring/data/exceptions/InvalidSliceableException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
148 changes: 148 additions & 0 deletions
148
...s-spring-data-chunk/src/main/java/jp/xet/sparwings/spring/data/model/SlicedResources.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
...data-chunk/src/main/java/jp/xet/sparwings/spring/data/repository/SliceableRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
} |
Oops, something went wrong.