JAX-RS(Jersey)のFilter(RequestFilter)を使う
JAX-RSのフィルタを使うが、前回書いた通り認可処理でとりあえず使うだけなので、RequestFilterが中心。
参考
Chapter 10. Filters and Interceptors
環境
- Java8
- Tomcat8
- Jersey2.22.1
とりあえずFilter使う
ContainerRequestFilter
リクエストフィルタはContainerRequestFilterを使用する。
@Provider public class LoginFilter implements ContainerRequestFilter { @Inject private SessionDto sessionDto; @Override public void filter(ContainerRequestContext requestContext) throws IOException { System.out.println("loginfilter: " + this.sessionDto.getUserDto()); if (this.sessionDto.getUserDto() == null) { requestContext.abortWith( Response.status(Response.Status.UNAUTHORIZED) .entity("Not Logged In.") .build() ); } } }
SessionDtoは@SessionScopedなクラス。SessionDtoにUserDtoが無ければ401(UNAUTHORIZED)を返す。
Jerseyの設定2(web.xmlとかApplicationクラスとか) - edgegram の(1)パターンで動かしているので、Filterクラスを読みこませるために@Providerを付けている。
JerseyのドキュメントにあるFilterサンプルには@Providerが付いていないが、Applicationでクラス指定している前提なのだろう。
リソースクラス
@Path("/") @RequestScoped public class Users { @Inject private SessionDto sessionDto; @Path("/user") @GET @Produces(MediaType.APPLICATION_JSON) public UserDto user() { UserDto dto = this.sessionDto.getUserDto(); return dto; } }
セッションに格納されているUserDtoをJSONで返すだけ。
テスト
ブラウザで/userにアクセスすると「Not Logged In.」と表示される。
ブラウザの開発者ツールでステータスを確認するとちゃんと401になっている。
サーバーのコンソールには「loginfilter: null」が出力されている。
Name binding
次はUserDtoがセッションに格納されている時にFilterを通るか確認。
そのためにセッションにUserDtoを入れる擬似ログイン処理を追加する。
@Path("/") @RequestScoped public class Users { @Inject private SessionDto sessionDto; @Path("/user") @GET @Produces(MediaType.APPLICATION_JSON) public UserDto user() { UserDto dto = this.sessionDto.getUserDto(); return dto; } @Path("/login") @GET public Response login() { UserDto dto = new UserDto(); dto.setId(1); dto.setName("test"); ・・・ this.sessionDto.setUserDto(dto); return Response.ok().build(); } }
セッションにUserDtoを格納してステータス200を返す。
これで/login → /user の順にアクセスすれば、/userはUserDtoのJSONが返ってくるはず。
なのだが、Filterは全リソースに適用されるので、このままだと/loginにもFilterがかかって401が返される。
特定のリソースだけにFilterを適用させたい、という場合にName bindingを使う。
アノテーション
@NameBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface LoggedIn {}
まず@NameBindingなアノテーションを作る。実行時に判別させるので@Retention(RetentionPolicy.RUNTIME)。
ContainerRequestFilter with NameBinding
@Provider @LoggedIn public class LoginFilter implements ContainerRequestFilter { @Inject private SessionDto sessionDto; @Override public void filter(ContainerRequestContext requestContext) throws IOException { System.out.println("loginfilter: " + this.sessionDto.getUserDto()); if (this.sessionDto.getUserDto() == null) { requestContext.abortWith( Response.status(Response.Status.UNAUTHORIZED) .entity("Not Logged In.") .build() ); } } }
Filterにアノテーションを付与する。
これでこのFilterは@LoggedInなリソースにしか効かなくなる。
リソースクラス with NameBinding
@Path("/") @RequestScoped public class Users { @Inject private SessionDto sessionDto; @Path("/user") @GET @Produces(MediaType.APPLICATION_JSON) @LoggedIn public UserDto user() { UserDto dto = this.sessionDto.getUserDto(); return dto; } @Path("/login") @GET public Response login() { UserDto dto = new UserDto(); dto.setId(1); dto.setName("test"); this.sessionDto.setUserDto(dto); return Response.ok().build(); } }
/userのメソッドだけ@LoggedIn付与。
テスト2
/loginにアクセスするとステータス200が返ってくる。
その後/userにアクセスすると「{"id":1,"name":"test"}」が表示される。
動的バインディング
NameBindingは静的Binding。動的にBindingしたい場合はDynamicFeatureを使う。
使い方はJerseyドキュメント参照。FeatureContextにFilterやInterceptorをresiterすればいい。
優先順
同じフィルタ(今回だとContainerRequestFilter)が複数ある場合に優先順を指定できる。
というか指定しなければ順番は保証されないのでするべし。
Interceptorも同様。
優先順を指定するにはFilterクラスに@Priorityを付与する。
@Provider @LoggedIn @Priority(Priorities.AUTHENTICATION) public class LoginFilter implements ContainerRequestFilter { ・・・ }
value値はint型。 Prioritiesクラスにざっくりの固定値が定義されているので、それを使ってもいいし直接数値を設定してもいい。
Priorityの順序
FilterやInterceptorの処理順序はPriorityの数値順ではない。
Priorityの数値が大きいほどリソースに近い処理となる。
具体的に言うと、Request系処理は数値が小さい順に処理され、Response系処理は逆に大きい順に処理される。
特にResponse処理の優先順を指定する時は注意。
JAX-RS(Jersey) Filterを使う前に
システムの都合上、認証・認可で標準的手法(Basic, Form, Digest, etc...)が使えない。
そんなわけでフィルタを使ってゴニョゴニョすることになるが、実際に使う前に関連情報を整理。
参考
Chapter 10. Filters and Interceptors
JAX-RS 2.0 ことはじめ - Programming Studio
JAX-RSをやってみる (9) - Container Filters - - kinjouj.github.io
FilterとInterceptor
どちらもリソースの呼び出し前後に処理を注入できる。
サーバー側・クライアント側に分けられるが、とりあえず使うサーバー側のみ整理。
Filter
- Request, ResponseそれぞれのFilterがある
- 主にHTTPヘッダーなどの要求・応答パラメータの操作で使用する
Interceptor
- ReaderInterceptor, WriterInterceptorの2種類
- 主にHTTPボディなどの入出力ストリームの操作で使用する
- 順序としてはFilterの後に呼び出される
@PreMatching
- サーバー側Requestフィルタに使用する
- リソースクラス・メソッドの呼び出しが決定(Match)される前に処理される
- @PreMatchingを付けなければPostMatching(呼び出し決定後のフィルタ処理)
- ヘッダー操作して呼び出しリソースを変更したい、という場合に使う
処理順(サーバー側のみ)
JAX-RS 2.0 ことはじめ - Programming Studio
の図が分かりやすい。厳密には@PreMatchingのサーバーリクエストフィルタが入る。
- ContainerRequestFilter with @PreMatching
- 呼び出しリソースの決定 (Matching)
- ContainerRequestFilter without @PreMatching
- ReaderInterceptor
- リソースメソッド実行
- ContainerResponseFilter
- WriterInterceptor
というわけで、認可処理をゴニョゴニョするのであればContainerRequestFilterを使うことになりそう。
Jerseyの設定2(web.xmlとかApplicationクラスとか)
前回の続きでServlet3.xで動かす場合の設定。
Chapter 4. Application Deployment and Runtime Environments
Servlert3.x
pom.xml
前回にも書いたが、Servlet2.xと3.xでは使用するモジュールが異なる。
<dependencies> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-servlet</artifactId> </dependency> </dependencies>
2.xではjersey-container-servlet-coreを使用するが、3.xではjersey-container-servlet。
Applicationとweb.xmlの関係
公式ドキュメントにはApplication(のサブクラス)とweb.xmlの関係が表になっている。
条件(Application) | Jerseyの動き | servlet-name | web.xml | |
---|---|---|---|---|
(1) | サブクラス無し | Servlet追加 | javax.ws.rs.core.Application | servlet-mapping必要 |
(2) | サブクラスがServletでハンドリング | 何もしない | 定義済み | 不要 |
(3) | サブクラスがハンドリングされていない | Servlet追加 | サブクラス完全名 | @ApplicationPathが指定されていなければservlet-mapping必要 |
(1)パターン
Applicationサブクラスを作らない場合、web.xml必須。
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <servlet> <servlet-name>javax.ws.rs.core.Application</servlet-name> </servlet> <servlet-mapping> <servlet-name>javax.ws.rs.core.Application</servlet-name> <url-pattern>/webapi/*</url-pattern> </servlet-mapping> </web-app>
servlet > servlet-nameにjavax.ws.rs.core.Applicationを指定する。
servlet-mappingを指定して、servlet-nameとurl-patternを指定する。
この場合、@Pathや@Providerが付いたクラスを自動でJAX-RSアプリケーションとして読み込んでくれる。便利。
(xmlだけで言えば)Jersey依存していないので、Jersey独自機能とか使わなければこれで問題なし。
余談:前回の自動生成されたweb.xmlをそのまま使うとschemaLocationでxmlがチェックされてxmlがエラーになるので注意。
web-appタグを公式ドキュメントと同じにするか、「2.5」「2_5」となっている箇所を「3.0」「3_0」に書き換える。
(2)パターン
Application(or ResourceConfig)のサブクラスを作って@ApplicationPathを指定すれば、web.xml不要。
@ApplicationPath("webapi") public class MyApplication extends ResourceConfig { public MyApplication() { packages("mypackage.test.resources"); } }
@ApplicationPathは、web.xmlで指定していたurl-patternと同様。
Applicationのサブクラス with @ApplicationPathを作れば、web.xmlは空でいい。
<web-app> </web-app>
更にmavenのプラグインを使えば、web.xmlのファイルが無くてもOKになる。
(war作成時にエラーにならない)
<plugins> ... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.3</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> ... </plugins>
failOnMissingWebXmlをfalseにする。
(3)パターン
Applicationのサブクラスがハンドリングされない場合、というのが具体的にどんな時なのかが分からない。
web.xmlにServlet定義されている場合(If the web.xml contains a Servlet definition)、でいいのかな?
とりあえずハンドリングされない場合は(3)パターンを使う。
public class MyApplication extends ResourceConfig { public MyApplication() { packages("mypackage.test.resources"); } }
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <servlet> <servlet-name>mypackage.test.MyApplication</servlet-name> </servlet> <servlet-mapping> <servlet-name>mypackage.test.MyApplication</servlet-name> <url-pattern>/webapi/*</url-pattern> </servlet-mapping> </web-app>
servlet-nameにApplicationサブクラスの完全修飾名を指定する。
@ApplicationPathを指定していればservlet-mappingは不要。
@ApplicationPath未指定であればservlet-mapping必要。
Jerseyの設定1(web.xmlとかApplicationクラスとか)
Jerseyの設定がバージョンによって違うが、webにある情報が色々混在していたりバージョンが不明だったりするので整理。
DeltaSpikeといいJerseyといい、結局は公式ドキュメントを読み込まないといけなくなる。英語力無いのが辛いところ。
Chapter 4. Application Deployment and Runtime Environments
環境
- Jersey2.22.1
- Java8
- Tomcat8
- Eclipse Luna(4.4.2)
テスト用プロジェクト
EclipseでJersey(JAX-RS)を始める - Qiita
こちらを参考にプロジェクトを作成。
Servlet2.5
自動作成されるpom.xmlやweb.xmlは、Servlet2.5(以上)の設定になっている様子。
Servlet2.5はTomcat6になるので今更使うことはないが軽く整理。
Servlet3.xは次回。
web.xml
<?xml version="1.0" encoding="UTF-8"?> <!-- This web.xml file is not required when using Servlet 3.0 container, see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html --> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>Jersey Web Application</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>jersey.config.server.provider.packages</param-name> <param-value>mypackage.test</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey Web Application</servlet-name> <url-pattern>/webapi/*</url-pattern> </servlet-mapping> </web-app>
プロジェクト作成して自動生成されたweb.xml。サーブレットとしてJerseyを動かす設定になっている。
もしサーブレットフィルタとして動かす場合は、filter関連のタグを使う。
<filter> <filter-name>Jersey Web Application</filter-name> <filter-class>org.glassfish.jersey.filter.ServletContainer</filter-class> <init-param> <param-name>jersey.config.server.provider.packages</param-name> <param-value>mypackage.test</param-value> </init-param> <load-on-startup>1</load-on-startup> </filter> <filter-mapping> <filter-name>Jersey Web Application</filter-name> <url-pattern>/webapi/*</url-pattern> </filter-mapping>
<url-pattern>/webapi/*</url-pattern>はWebリソースを提供するパスを指定する。
この場合、Eclipseプロジェクトがtestとして作っているので、各リソースへは「http://localhost:8080/test/webapi/・・・」というURLになる。
param-nameで指定しているjersey.config.server.provider.packagesはリソースクラスの読み込み先を指定する。
param-valueで指定したパッケージ内のクラスがスキャンされる。デフォルトではサブパッケージもスキャン対象。コンマ区切りで複数指定可。
サブパッケージをスキャン対象にしたくない場合は、recursiveをfalseにする。
<init-param> <param-name>jersey.config.server.provider.packages</param-name> <param-value>mypackage.test</param-value> </init-param> <init-param> <param-name>jersey.config.server.provider.scanning.recursive</param-name> <param-value>false</param-value> </init-param>
リソースクラスを個別に指定することもでき、その場合はjersey.config.server.provider.classnamesを使う。
<init-param> <param-name>jersey.config.server.provider.classnames</param-name> <param-value>mypackage.test.MyResource</param-value> </init-param>
こちらもコンマ区切りで複数クラス指定可。
packagesとclassnamesは両方指定可能のようなので、通常はpackagesを使用して特殊なものだけclassnamesを使う、という使い方になるはず。
(普通はpackagesだけで収まるようにすると思うけど)
pom.xml
dependenciesだけ抜き出し
<dependencies> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-servlet-core</artifactId> <!-- use the following artifactId if you don't need servlet 2.x compatibility --> <!-- artifactId>jersey-container-servlet</artifactId --> </dependency> <!-- uncomment this to get JSON support <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-moxy</artifactId> </dependency> --> </dependencies>
コメントアウトされている通り、servlet2.x に適合させる必要がなければ
余談:moxyの方はJerseyデフォルトのJSONバインドモジュール。JSONリクエストをPOJOで受け取りたいとか、POJOを戻り値にしたい(JSON変換されてレスポンス)、という時に使う。
javax.ws.rs.core.Application
ここまではリソースクラスの読み込み先などをweb.xmlで設定していたが、実はこれは簡易版(?)で本来はjavax.ws.rs.core.Applicationを継承したクラスを作成して、そのクラスで設定をするものらしい。
たぶんApplicationクラスを作成するのはJAX-RSの仕様で、Jerseyを使うのであればorg.glassfish.jersey.servlet.ServletContainerがそのあたりを吸収してweb.xmlで書けるようにしてくれている、のかな?
とりあえずApplicationクラスを作成。
public class MyApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> s = new HashSet<Class<?>>(); s.add(MyResource.class); return s; } }
リソースクラスのclassを設定する。つまり上述のweb.xmlでclassnamesを使っているようなもの。
これは面倒なので、JerseyではApplicationを拡張(extends)したResourceConfigクラスが用意されている。
public class MyApplication extends ResourceConfig { public MyApplication() { packages("mypackage.test;mypackage.test2"); property(ServerProperties.PROVIDER_SCANNING_RECURSIVE, false); } }
ResourceConfigだとパッケージ指定ができて、セミコロン区切りで複数指定もできる。
Jersey独自設定はResourceConfigを使うことになるので、Jersey使うのであればResourceConfigを最初から使っておけばいい。
PROVIDER_SCANNING_RECURSIVEは名前で分かる通り、上記web.xmlで指定していたjersey.config.server.provider.scanning.recursiveに該当する。
Application(or ResourceConfig)を使う場合、web.xmlはApplicationを読み込むだけの設定になる。
<servlet> <servlet-name>Jersey Web Application</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>mypackage.test.MyApplication</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey Web Application</servlet-name> <url-pattern>/webapi/*</url-pattern> </servlet-mapping>
公式ドキュメントにもわざわざ注釈があるが、param-nameに指定する値は「javax.ws.rs.Application」である。
作成したApplicationクラスの親クラスは「javax.ws.rs.core.Application」なのでcore 1つ分違う。ややこしい。
web.xmlをコピペした人はハマらない、注意深い人ほどハマる、たぶん。
Servlet2.5の方が意外と長くなったので、Servlet3.0の方は分けて書く。
DeltaSpike Data Module(使いそうなところだけ)まとめ4
Transactions
・・・DeltaSpike JPAも使うつもりだけど、完全に理解できていない。。。
特に指定せずに1トランザクション内で異なるRepositoryの更新ができるなら問題ないけど。。。
とりあえず
「the repository will figure out if a transaction is needed or not」
(リポジトリはトランザクションが必要かどうか理解します)
とあるからよろしくやってくれる、、、はず。。。
トランザクションを完全に理解しないのはかなり怖いけど、必要になったらログを見ながら調査予定。
Extensions
リポジトリに独自インターフェースを足して拡張できる。その際、独自インターフェースを実装するimplクラスにDelegateQueryHandlerを付けてね。
という理解。
サンプルではQueryDslライブラリを使えるように拡張している。
・・・QueryDslって何?
QuerydslでJPAが思ったよりも捗る - 水まんじゅう
なるほど、S2JDBCは経験済みだからすぐに理解。でもDeltaSpike Dataが同じような機能を実装してくれている。(後述)
というわけで詳細はスキップ。
Mapping
戻り値をEntityではなくDTOなんかで返せるようにできる。
よくお世話になってくるはず。
public class PersonMapper implements QueryInOutMapper<Person> { @Override public Object mapResult(Person result) { return (変換後オブジェクト); } @Override public Object mapResultList(List<Simple> result) { return (変換後オブジェクトのリスト); } @Override public boolean mapsParameter(Object parameter) { // ? } @Override public Object mapParameter(Object parameter) { // ? } }
QueryInOutMapper
・・・いやまて。mapResultList()はListを受けてListを返す?
List件数が膨大になるとOutOfMemoryになるけど、、、iterator的な機能は?
・・・そもそもJPA自体にそういう仕組みが無いのか。ページングをちゃんとしろ、ということなのだろう。
全件CSV出力とかする場合は、1000件づつ取ってくるとか工夫しないといけない。
それはさておき、クラスを作ったらRepositoryで@MappingConfigを使って指定する。
@Repository(forEntity = Person.class) @MappingConfig(PersonDtoMapper.class) public interface PersonRepository extends EntityRepository<PersonDto, Long> {}
簡易マッピング(Simple Mappings)
正直、mapsParameter()とmapParameter()は何をするのかよく分からない。
それに実際にやるのはEntity←→DTOの相互変換ぐらいなので、その場合は簡易マッピングを使う。たぶんこちらを使うことになる。
public class PersonMapper extends SimpleQueryInOutMapperBase<Person, PersonDto> { @Override protected Object getPrimaryKey(PersonDto dto) { return dto.getId(); } @Override protected PersonDto toDto(Person entity) { return parsonDto; } @Override protected Person toEntity(Person entity, PersonDto dto) { return entity; } }
SimpleQueryInOutMapperBaseはQueryInOutMapperを実装した抽象クラスで、QueryInOutMapperのよく分からないメソッド達を隠蔽してくれている。
getPrimaryKey()はDTO側でプライマリキーとなるプロパティの値を返すようにする。
toDto()とtoEntity()は読んで字のごとく。
toEntity()の第一引数がEntityになっているが、ソースを見るとEntityManager.find()したEntityを、find()結果が無ければnewのオブジェクトを渡している様子。
必ず引数のEntityを操作して返すこと。(自前でnewとかしない)
JPA Criteria API Support
JPAにCriteriaというAPIがある。が、かなり複雑(みたい)。
(実際に触ったことがないが、色々調べる限り使いたくない。上述のQuerydslなんてものが作られるのが使いにくい証拠)
DeltaSpike DataでもQuerydslのような機能が提供されている。
@Repository(forEntity = Person.class) public abstract class PersonRepository implements CriteriaSupport<Person> { public List<Person> findAdultFamilyMembers(String name, Integer minAge) { return criteria() .like(Person_.name, "%" + name + "%") .gtOrEq(Person_.age, minAge) .eq(Person_.validated, Boolean.TRUE) .orderDesc(Person_.age) .getResultList(); } }
「流れるようなインターフェース」というやつでしょうか。多分見て想像がつくSQLが実行される。
これについてはSeasar2(S2JDBC)を使っていたので問題なし。DTOでも返せそう。なのでスキップ。
Auditing
・・・監査?
サンプルでは登録日(Date created)と更新日(Date updated)を更新するような例が載っている。
@PrePersistや@PreUpdate使うのとあまり違いは無い気がする。
(Auditingの方はオブジェクトの作成・更新のタイミングで差し込まれる? でもそこまで厳密なタイムスタンプいらない)
というわけでスキップ。
まとめ終わり。
DeltaSpike Data Module(使いそうなところだけ)まとめ3
Query Annotations
@QueryでJPQLが使える。
Queryアノテーション使う(Using Query Annotations)
- Repositoryに書く場合
public interface PersonRepository extends EntityRepository<Person, Long> { @Query("select count(p) from Person p where p.age > ?1") Long countAllOlderThan(int minAge); }
- Entityの@NamedQueriesを呼び出す場合
@Entity @NamedQueries({ @NamedQuery(name = Person.BY_MIN_AGE, query = "select count(p) from Person p where p.age > ?1 order by p.age asc") }) public class Person { public static final String BY_MIN_AGE = "person.byMinAge"; } @Repository public interface PersonRepository extends EntityRepository<Person, Long> { @Query(named = Person.BY_MIN_AGE) Long countAllOlderThan(int minAge); }
メソッド引数の順番に、?1, ?2・・・とパラメータがセットされる。パラメータの番号が1始まりなのはPreparedStatementからの伝統(超私見)。
Repositoryを使うのなら、EntityのNamedQueryにクエリを書くよりRepositoryに@Queryで書くほうがいいと思われる。Repositoryにクエリが集約するので。
- 名前形式パラメータ
JPQLのパラメータを名前形式にする場合はメソッドパラメータに@QueryParamを使う。
public interface PersonRepository extends EntityRepository<Person, Long> { @Query("select count(p) from Person p where p.age > :age") Long countAllOlderThan(@QueryParam("age") int age); }
メソッド引数が長くなるけどこっちのほうが分かりやすいか?
@QueryParamを使わないといけない理由はあえて注釈がついている。たぶんコンパイルするとメソッド引数名は保持されないから。引数名がそのまま残るのなら、JPQLのパラメータと引数名を揃えればOK、になるのだろうが。
- SQL(not JPQL)を使う
@Query(value = "SELECT * FROM PERSON_TABLE p WHERE p.AGE > ?1", isNative = true) List<Person> findAllOlderThan(int minAge);
isNativeをtrueにするとSQLを書ける。DB固有のクエリとか書くハメになったら。
@Queryのオプション(Annotation Options)
@Query(named = Person.BY_MIN_AGE, max = 10, lock = LockModeType.PESSIMISTIC_WRITE) List<Person> findAllForUpdate(int minAge);
maxはLIMIT指定、lockはロックモード・・・あれ? OFFSETは? ソース見ても無さそう。。。
いやQueryResult(後述)使えばできるんだけど、OFFSETはつけてよApacheさん。
SQLクエリオプション(Query Options)
SQLクエリのオプションを指定する場合、QueryResultを使う。
@Repository public interface PersonRepository extends EntityRepository<Person, Long> { @Query("select p from Person p where p.age between ?1 and ?2") QueryResult<Person> findAllByAge(int minAge, int maxAge); }
戻り値はListやEntityではなくQueryResult
List<Person> result = personRepository.findAllByAge(18, 65) .orderAsc(Person_.lastName) .orderDesc(Person_.age) .lockMode(LockModeType.WRITE) .hint("org.hibernate.timeout", Integer.valueOf(10)) .getResultList();
ソートやLIMIT、OFFSET、ロックモード等を指定できる。最後にgetResultList()でリスト取得。
・・・というかsortAsc()のPerson_.lastNameって何?
JavaEE使い方メモ(JPA その4 - クライテリアAPI) - Qiita
なるほど。メタモデルというのね。コンパイルでミスを防げるのか、便利。
でもJPQL書いてる時点で防げない部分は出てくるよね。。。
閑話休題
ちなみにString引数メソッドも用意されていて、プロパティ名をStringで指定できる。
あと本家ドキュメントはorderBy系メソッド名が間違えていて、sortAsc(), sortDesc()になっているがorderAsc(), orderDesc()が正しい。
ページング(Pagination)
QueryResult<Person> paged = personRepository.findByAge(age) .maxResults(10) .firstResult(50); QueryResult<Person> paged = personRepository.findByAge(age) .withPageSize(10) .toPage(5); int totalPages = paged.countPages();
LIMIT, OFFSETを指定するパターンと、ページサイズ, ページを指定するパターンの2つ使えるらしい。お好みで。
ただプロジェクト内で統一しておかないと後々面倒。
複数データ操作(Bulk Operations)
@Repository public interface PersonRepository extends EntityRepository<Person, Long> { @Modifying @Query("update Person as p set p.classifier = ?1 where p.classifier = ?2") int updateClassifier(Classifier current, Classifier next); }
1件づつの更新や削除ではなく、まとめて更新処理するクエリには@Modifyingを付ける、ということ?
とりあえず試してみる。
@Query("delete from User as u where u.name = ?1") public abstract int deleteUsers(String name);
@Modifyingを付けずに実行してみるとエラーが出て怒られた。
「You cannot call getSingleResult() on this query. It is the incorrect query type.」
@Modifyingを付けたらDELETE成功。複数更新/削除クエリには@Modifying必須。
クエリ結果オプション(Optional Query Results)
JPAではgetSingleResult()の結果が0件か複数件になるとException(RuntimeException)になる。
・・・そうなんだ。
複数件はともかく0件のためにtry-catchはどうなんだ。まあnullチェックするから大差ないと言えなくもない。
DeltaSpkie Dataではオプションで変えれるらしい。
0 or 1件結果(Zero or One Result)
0件の場合はnullを返してくれるオプション。
Person findOptionalBySsn(String ssn);
@Query(named = Person.BY_NAME, singleResult = SingleResultType.OPTIONAL)
Person findByName(String firstName, String lastName);
メソッド式の場合は接頭語に「findOptionalBy」を付ける。
@Queryの場合は「singleResult = SingleResultType.OPTIONAL」を付ける。
複数件ヒットしてしまったらNonUniqueResultException。
なんでも来い(Any Result)
0件の場合はnull、複数件の場合は先頭行を返すオプション。
何て嬉しい、、、いや嬉しいのか?
間違ったクエリがもみ消されることになりかねない。よっぽど意図的に使わないと危険。
Person findAnyByLastName(String lastName);
@Query(named = Person.BY_NAME, singleResult = SingleResultType.ANY)
Person findByName(String firstName, String lastName);
メソッド式の場合は接頭語に「findAnyBy」を付ける。
@Queryの場合は「singleResult = SingleResultType.ANY」を付ける。
Exceptionは発生しない。
※2015/11/30 追記
JPQLやNamedQueryを使用しない場合、つまりメソッド式の場合でもクエリ結果オプションが使えるみたい。
@Query(singleResult = SingleResultType.OPTIONAL) List<Person> findByNameLikeAndAgeBetweenAndGender(String name, int minAge, int maxAge, Gender gender);
ただ不満なのが、@Queryがメソッドにしか指定できない。
1つのメソッドだけオプション指定忘れてNoResultException、とかなる可能性がある。
Repository全体に適用できる(ElementType.TYPE)もあると統一できて楽だけど、まあ、ちゃんとテストしろという話か。
DeltaSpike Data Module(使いそうなところだけ)まとめ2
Query Method Expressions
メソッド名をちゃんと(分かりやすく)付ければコメントいらない。分かりやすいメソッド名でクエリを作ってしまおう。ということらしい。
メソッド式を使う(Using Method Expressions)
@Entity public class Person { @Id @GeneratedValue private Long id; private String name; private Integer age; private Gender gender; } @Repository public interface PersonRepository extends EntityRepository<Person, Long> { List<Person> findByNameLikeAndAgeBetweenAndGender(String name, int minAge, int maxAge, Gender gender); }
- 戻り値はEntity(1件) or List
(複数)のどちらか - メソッド名は「findBy」で始める
- キャメル形式で「プロパティ名 + 比較演算子」を繋げる
- 比較演算子は「Equal」「Like」etc... SQLで馴染みのもの
- 複数条件の場合、「And」 or 「Or」で繋げる
- 引数は比較演算子に応じて追加する (Betweenはminとmaxの2つ、IsNull, IsNotNullは0、他は引数1つを追加)
サンプルの場合、
WHERE name like (引数name) AND BETWEEN (引数minAge) AND (引数maxAge) AND gender = (引数gender)
という条件のSQLを実行してくれるらしい。・・・わざわざSQLを書かなくてもいいとか素敵過ぎ。 genderがオブジェクトになっているので、実際はgenderの主キーが検索条件になるはず。
ソート(Query Ordering)
OrderBy + プロパティ名 + 「Asc or Desc」を付けるだけ。もちろん複数指定可。
List<Person> findByLastNameLikeOrderByAgeAscLastNameDesc(String lastName);
ORDER BY age ASC, lastName DESC
ネストプロパティ(Nested Properties)
ネストプロパティはアンダースコア「_」で指定する。
List<Person> findByCompany_companyName(String companyName);
WHERE company.companyName = (引数companyName)
クエリオプション(Query Options)
主にページングで。
List<Person> findByNameLike(String name, @FirstResult int start, @MaxResults int pageSize);
LIMIT pageSize OFFSET start
メソッド接頭語(Method Prefix)
メソッド接頭語「findBy」がお気に召さない? 安心して下さい、変えれますよ。
@Repository(methodPrefix = "fetchWith") public interface PersonRepository extends EntityRepository<Person, Long> { List<Person> fetchWithNameLike(String name, @FirstResult int start, @MaxResults int pageSize); }