Skip to content

Commit 816d89e

Browse files
Merge pull request #6 from classmethod/feature/fix-issue-2-add-slice
add SliceableRepository
2 parents c9e298c + 56b0195 commit 816d89e

File tree

17 files changed

+2188
-0
lines changed

17 files changed

+2188
-0
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# spar-wings-spring-data-chunk
2+
3+
spring-data-mirage と組み合わせて、Mirage と Spring を連携します。
4+
5+
## SQL 発行
6+
7+
用途によって、interface を実装します。代表的な interface は以下の通りです。
8+
9+
* WritableRepository
10+
* INSERT / UPDATE / DELETE を発行する
11+
* ReadableRepository
12+
* SELECT 文を発行する
13+
* `@Id` アノテーションを付与したカラムに対する SELECT 文を発行できます
14+
* ChunkableRepository
15+
* Chunk 形式で結果を受け取る SELECT 文を発行する
16+
* ReadableRepository も含まれます
17+
* SliceableRepository
18+
* Slice 形式で結果を受け取る SELECT 文を発行する
19+
* ReadableRepository も含まれます
20+
21+
用意する sql ファイルは spring-data-mirage を参照してください。
22+
23+
### Chunk とは?
24+
25+
ある集合に対する部分集合を表すリソースです。[参考](https://d1sraz2ju3uqe4.cloudfront.net/section2/example/resource/Chunk.html)
26+
OFFSET を使用しないページネーションを提供します。後ろの部分集合を取得する際もレスポンスが落ちません。
27+
28+
前提条件として `@Id` を付与したカラムで大小比較を行い、ソートを行います。
29+
その為、ORDER BY 句に `@Id` 以外のカラムを指定できません。
30+
31+
### Slice とは?
32+
33+
ある集合に対する部分集合を表すリソースです。
34+
OFFSET を使用するページネーションを提供します。
35+
36+
後ろの部分集合を取得する際はレスポンスが悪化しますが ORDER BY 句に任意のカラムを指定可能です。
37+
38+
**集合の件数が少ない、または、先頭から数ページだけ参照することでユースケースが満たせる場合に限り**こちらを使用しても構いません。
39+
40+
Slice の時に発行する SELECT 文は以下のイメージです。
41+
42+
```sql
43+
SELECT
44+
*
45+
FROM
46+
some_table
47+
WHERE
48+
-- デフォルトの絞り込み条件制御
49+
-- パラメータ有無で絞り込み条件制御
50+
ORDER BY
51+
sort_column /*$direction*/ASC
52+
53+
/*BEGIN*/
54+
LIMIT
55+
/*IF offset != null*/
56+
/*offset*/0,
57+
/*END*/
58+
59+
/*IF size != null*/
60+
/*size*/10
61+
/*END*/
62+
/*END*/
63+
```
64+
65+
Repository の Sliceable パラメータで自動で設定するのは、
66+
67+
* `offset`
68+
* `size`
69+
* `direction`
70+
71+
です。
72+
73+
ソート順は、ユニークになるように指定してください。
74+
例えば、`ORDER BY create_at ASC` にした場合、create_at が同じ値のレコードのソート順は不定の為、Sliceable で全ての値を取得することは保証できません。
75+
以下のように
76+
`ORDER BY create_at ASC, xxx_code ASC`
77+
ソート条件にユニークキーを含めるようにしてください(xxx_code が当該 table のユニークキーの前提です)。
78+
79+
先頭から offset までの読み飛ばし件数が多くなることで API のパフォーマンス劣化を引き起こす可能性が高くなる為、
80+
部分集合の先頭から 2000 件を超えて取得できないように制限します。
81+
具体的には Sliceable パラメータの内容が `page_number * size + size > 2000` の場合、不正リクエストとみなし、
82+
83+
* Controller の引数に Sliceable を設定した時には HttpBadRequestException
84+
* Repository メソッド呼び出し時には InvalidSliceableException
85+
* Controller の引数に Sliceable を設定しない場合はハンドリングをしてください
86+
87+
を throw します。
88+
89+
### INDEX 設計
90+
91+
MySQL の場合、
92+
`ORDER BY create_at ASC, xxx_code ASC`
93+
のソートに INDEX を効かせる為には、`create_at, xxx_code` の複合 INDEX が必要になります。`create_at` だけではソートに INDEX は効きません。
94+
また、MySQL の場合、ソート条件に ASC と DESC が混在していると INDEX が効きません。設計時に留意してください。
95+
(ただし、ソート対象のレコードが少ない場合は INDEX を貼っても使用されないので考慮する必要はありません)
96+
絞り込み条件も存在する場合、適切な INDEX 設計を実施する必要があります。
97+
98+
## Controller で部分集合を取得するリクエストを受け取る
99+
100+
WebMvcConfigurer の実装クラスで addArgumentResolvers を Override し、
101+
102+
* ChunkableHandlerMethodArgumentResolver
103+
* Chunk のリクエストを受け取る場合
104+
* SliceableHandlerMethodArgumentResolver
105+
* Slice のリクエストを受け取る場合
106+
107+
インスタンスを add してください。
108+
109+
イメージは以下の通りです。
110+
111+
```java
112+
@Configuration
113+
public class WebMvcConfiguration implements WebMvcConfigurer {
114+
115+
@Override
116+
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
117+
argumentResolvers.add(new ChunkableHandlerMethodArgumentResolver());
118+
argumentResolvers.add(new SliceableHandlerMethodArgumentResolver());
119+
}
120+
}
121+
```
122+
123+
Controller の引数 Chunkable / Sliceable に `@ChunkableDefault` / `@SliceableDefault` を付与することで、
124+
リクエストが未指定の時に defalut 値を設定したインスタンスを生成します。

spar-wings-spring-data-chunk/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ dependencies {
88
compile "org.springframework:spring-web"
99
compile "org.springframework:spring-context"
1010
compileOnly "org.springframework:spring-tx"
11+
compile project(":spar-wings-httpexceptions")
1112

1213
testCompile "com.fasterxml.jackson.core:jackson-databind"
1314
testCompile "com.jayway.jsonpath:json-path-assert:2.4.0"
15+
testCompile "org.skyscreamer:jsonassert:1.5.0"
16+
testCompile "org.assertj:assertj-core"
1417
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2015-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package jp.xet.sparwings.spring.data.exceptions;
17+
18+
import lombok.NoArgsConstructor;
19+
20+
/**
21+
* 不正な Sliceable を渡した時の Exception
22+
*/
23+
@NoArgsConstructor
24+
@SuppressWarnings("serial")
25+
public class InvalidSliceableException extends RuntimeException {
26+
27+
/**
28+
* インスタンスを生成する。
29+
*
30+
* @param message 例外メッセージ
31+
* @param cause 起因例外
32+
*/
33+
public InvalidSliceableException(String message, Throwable cause) {
34+
super(message, cause);
35+
}
36+
37+
/**
38+
* インスタンスを生成する。
39+
*
40+
* @param message 例外メッセージ
41+
*/
42+
public InvalidSliceableException(String message) {
43+
super(message);
44+
}
45+
46+
/**
47+
* インスタンスを生成する。
48+
*
49+
* @param cause 起因例外
50+
*/
51+
public InvalidSliceableException(Throwable cause) {
52+
super(cause);
53+
}
54+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2015-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package jp.xet.sparwings.spring.data.model;
17+
18+
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.Map;
21+
import java.util.function.Function;
22+
import java.util.stream.Collectors;
23+
24+
import javax.xml.bind.annotation.XmlAttribute;
25+
import javax.xml.bind.annotation.XmlElement;
26+
import javax.xml.bind.annotation.XmlRootElement;
27+
28+
import lombok.AccessLevel;
29+
import lombok.AllArgsConstructor;
30+
import lombok.EqualsAndHashCode;
31+
import lombok.Getter;
32+
import lombok.NoArgsConstructor;
33+
import lombok.ToString;
34+
35+
import org.springframework.util.Assert;
36+
37+
import com.fasterxml.jackson.annotation.JsonIgnore;
38+
import com.fasterxml.jackson.annotation.JsonProperty;
39+
40+
import jp.xet.sparwings.spring.data.slice.Slice;
41+
42+
/**
43+
* Slice のレスポンス.
44+
*
45+
* <p>Controller のレスポンスとして使用します。</p>
46+
*/
47+
@ToString
48+
@EqualsAndHashCode
49+
@XmlRootElement(name = "slicedEntities")
50+
public class SlicedResources<T> {
51+
52+
@Getter
53+
@XmlElement(name = "embedded")
54+
@JsonProperty("_embedded")
55+
private Map<String, Collection<T>> content;
56+
57+
@Getter
58+
@XmlElement(name = "page")
59+
@JsonProperty("page")
60+
private SliceMetadata metadata;
61+
62+
63+
/**
64+
* Creates a {@link SlicedResources} instance with {@link Slice}.
65+
*
66+
* @param key must not be {@code null}.
67+
* @param slice The {@link Slice}
68+
* @param wrapperFunction function coverts {@code U} to {@code T}
69+
*/
70+
public <U> SlicedResources(String key, Slice<U> slice, Function<U, T> wrapperFunction) {
71+
this(key, slice.stream()
72+
.map(wrapperFunction)
73+
.collect(Collectors.toList()), new SliceMetadata(slice));
74+
}
75+
76+
/**
77+
* Creates a {@link SlicedResources} instance with {@link Slice}.
78+
*
79+
* @param key must not be {@code null}.
80+
* @param slice The {@link Slice}
81+
*/
82+
public SlicedResources(String key, Slice<T> slice) {
83+
this(key, slice.getContent(), new SliceMetadata(slice));
84+
}
85+
86+
/**
87+
* Creates a {@link SlicedResources} instance with content collection.
88+
*
89+
* @param key must not be {@code null}.
90+
* @param content The contents
91+
* @since 0.11
92+
*/
93+
public SlicedResources(String key, Collection<T> content) {
94+
this(key, content, new SliceMetadata(content.size(), null, false));
95+
}
96+
97+
/**
98+
* Creates a {@link SlicedResources} instance with iterable and metadata.
99+
*
100+
* @param key must not be {@code null}.
101+
* @param content must not be {@code null}.
102+
* @param metadata must not be {@code null}.
103+
* @since 0.11
104+
*/
105+
public SlicedResources(String key, Collection<T> content, SliceMetadata metadata) {
106+
Assert.notNull(key, "The key must not be null");
107+
Assert.notNull(content, "The content must not be null");
108+
Assert.notNull(metadata, "The metadata must not be null");
109+
this.content = Collections.singletonMap(key, content);
110+
this.metadata = metadata;
111+
}
112+
113+
114+
/**
115+
* Value object for pagination metadata.
116+
*/
117+
@ToString
118+
@EqualsAndHashCode
119+
@AllArgsConstructor
120+
@NoArgsConstructor(access = AccessLevel.PACKAGE)
121+
public static class SliceMetadata {
122+
123+
@XmlAttribute
124+
@JsonProperty("size")
125+
@Getter(onMethod = @__(@JsonIgnore))
126+
private long size;
127+
128+
@XmlAttribute
129+
@JsonProperty("number")
130+
@Getter(onMethod = @__(@JsonIgnore))
131+
private Integer pageNumber;
132+
133+
@XmlAttribute
134+
@JsonProperty("has_next_page")
135+
@Getter(onMethod = @__(@JsonIgnore))
136+
private Boolean hasNextSlice;
137+
138+
139+
/**
140+
* インスタンスを生成する。
141+
*
142+
* @param slice 当該 Slice
143+
*/
144+
public SliceMetadata(Slice<?> slice) {
145+
this(slice.getContent().size(), slice.getPageNumber(), slice.hasNext());
146+
}
147+
}
148+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2015-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package jp.xet.sparwings.spring.data.repository;
17+
18+
import java.io.Serializable;
19+
20+
import org.springframework.dao.DataAccessException;
21+
import org.springframework.data.repository.NoRepositoryBean;
22+
23+
import jp.xet.sparwings.spring.data.exceptions.InvalidSliceableException;
24+
import jp.xet.sparwings.spring.data.slice.Slice;
25+
import jp.xet.sparwings.spring.data.slice.Sliceable;
26+
27+
/**
28+
* Repository interface to retrieve slice of entities.
29+
*
30+
* @param <E> the domain type the repository manages
31+
* @param <ID> the type of the id of the entity the repository manages
32+
*/
33+
@NoRepositoryBean
34+
public interface SliceableRepository<E, ID extends Serializable>extends ReadableRepository<E, ID> {
35+
36+
/**
37+
* Returns a {@link Slice} of entities meeting the slicing restriction provided in the {@code Sliceable} object.
38+
*
39+
* @param sliceable slicing information
40+
* @return a slice of entities
41+
* @throws DataAccessException データアクセスエラーが発生した場合
42+
* @throws NullPointerException 引数に{@code null}を与えた場合
43+
* @throws InvalidSliceableException 不正な Sliceable だった場合
44+
*/
45+
Slice<E> findAll(Sliceable sliceable);
46+
47+
}

0 commit comments

Comments
 (0)