Spring Data JDBCを拡張してみる その1 - クエリを受け取れるメソッドを増やす
ここまでSpring Dataの自作を行ってきましたが、Spring DataやSpring Data JDBCの知識がついてきたおかげで、Spring Data JDBCそのものを拡張することもできそうだなと分かってきました。
元々あるものは再実装せずにそのまま使ったほうが良いに決まってるので、Spring Data JDBCに乗っかった上で好きな機能を追加してみることにします。
TL;DR
- いままでのエントリーをきちんと読んでなくても、今回から話が変わるのできっと読めると思うよ
- Spring Data JDBCの拡張は難しくなかったよ
- 作ったものはGitHubに置いてあるよ(リンク)
- 作る様子を動画でもライブ配信したよ
やりたいこと
先にゴールを明確にしておきます。
- Spring Data JDBCに機能追加して
query(String query, Object... args)
メソッドを追加できること - Spring Data JDBCを使ったうえで
@SqlFile
アノテーションで指定したSQLファイルを読み込んで実行できること
今回は1つ目を実現するところまで説明します。
1. Spring Data JDBCの拡張ポイント
まずはSpring Data JDBCをどうやって拡張できるかという方向性の確認です。
入口の EnableJdbcRepositories
に拡張ポイントがある
Spring Data JDBCの @EnableJdbcRepositories
アノテーションに、次のような定義があります。
public @interface EnableJdbcRepositories { Class<?> repositoryFactoryBeanClass() default JdbcRepositoryFactoryBean.class; }
RepositoryFactoryを提供するクラスを指定できるもので、デフォルトでは JdbcRepositoryFactoryBean
を利用するようになっています。RepositoryFactoryはRepositoryを作成する入口となるクラスですから、これを自作のものに置き換えればいくらでもカスタマイズできることが分かります。
RepositoryBaseを置き換えるだけならもっと簡単
ちなみに @EnableJdbcRepositories
アノテーションにはもう一つカスタマイズしやすいポイントがあります。
public @interface EnableJdbcRepositories { Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class; }
RepositoryBaseのクラスです。Spring Data JDBCでは、RepositoryBaseは SimpleJdbcRepository
が利用されますが、このアノテーションで指定することで、それを別のものに置き換えることが可能だということです。SimpleJdbcRepository
を継承してメソッドを追加したものを提供することもできるということですね。
ただこの repositoryBaseClass
だけを指定してもRepository自身のコンストラクタの引数を変えることはできませんし、前回実装した QueryLookupResolver
を使った独自アノテーションを読み取るような処理もできず、カスタマイズできる範疇に限界があります。なので今回はこの部分は使わず、上で説明した repositoryFactoryBeanClass
を使うことにします。
2. Spring Data JDBCの独自拡張を実装する
それでは実際にライブラリを作っていきましょう。
Spring Data JDBCに依存したモジュールを作成する
これまではSpring Dataを自作するために spring-data-commons
というSpring Dataの共通ライブラリと spring-data-relational
というRDBMS用の共通ライブラリと spring-jdbc
というJDBCアクセスライブラリの3つに依存したモジュールを作成していました。
今回はそれとは別に spring-data-jdbc
だけに依存したモジュールを作成します。pom.xml
に入れる依存はこれだけです。
<groupId>ninja.cero.data.sql</groupId> <artifactId>data-jdbc-ext</artifactId> <dependencies> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jdbc</artifactId> <version>3.0.0</version> </dependency> </dependencies>
共通Repositoryインターフェイスを作る
まずは今回作るSpring Data JDBC拡張のRepositoryの共通インターフェイスとして JdbcExtRepository
を作成します。
public interface JdbcExtRepository <T, ID> extends CrudRepository<T,ID>, PagingAndSortingRepository<T,ID>, QueryByExampleExecutor<T> { List<T> query(String query, Object... args); }
前回のエントリーでも少し振れましたが、Spring Data JPAではこのように基底リポジトリインターフェイスを集約した JpaRepository
というインターフェイスを提供しています。Spring Data JDBCではこのようなインターフェイスは提供していませんが、あった方が便利だし拡張もしやすいので、まずはこれを作りました。
そして独自拡張となる query
メソッドを定義して、Spring Data JDBCの基本機能に加えて query
メソッドを使えるように定義しておきます。
Repositoryの実装クラスを作る
続いて、この共通インターフェイスの実装クラスを作成します。実装クラスは spring-data-jdbc
が提供する SimpleJdbcRepository
を継承したうえで、先ほど作成した JdbcExtRepository
の実装として提供します。
public class JdbcExtDefaultRepository<T, ID> extends SimpleJdbcRepository<T, ID> implements JdbcExtRepository<T, ID> { public List<T> query(String query, Object... args) { return null; } }
この query
メソッドの中身をきちんと実装すれば、アプリケーション側のRepositoryを作成する際に JdbcExtRepository
インターフェイスを継承するだけで、この query
メソッドが使えるようになるという仕組みです。早速、実装してみましょう。
public class JdbcExtDefaultRepository<T, ID> extends SimpleJdbcRepository<T, ID> implements JdbcExtRepository<T, ID> { JdbcOperations jdbcOperations; EntityRowMapper<T> entityRowMapper; public JdbcExtDefaultRepository(JdbcOperations jdbcOperations, JdbcAggregateOperations entityOperations, RelationalPersistentEntity<T> entity, JdbcConverter converter) { super(entityOperations, entity, converter); this.jdbcOperations = jdbcOperations; this.entityRowMapper = new EntityRowMapper<>(entity, converter); } @Override public List<T> query(String query, Object... args) { return jdbcOperations.query(query, entityRowMapper, args); } }
3分間クッキングばりに一気にできあがりました。中核となる部分は query
メソッドの実装ですね。
return jdbcOperations.query(query, entityRowMapper, args);
この処理を実行するために必要となるインスタンスたちをコンストラクタで受け取るようにします。具体的には次のものになります。
JdbcOperations
のquery
メソッドを呼び出すためのJdbcOperations
EntityRowMapper
を作るためのRelationalPersistentEntity
とJdbcConverter
JdbcOperations
は実際には JdbcTemplate
なのですが、Spring Data JDBCでは JdbcTemplate
も NamedParameterJdbcTemplate
もいずれもインターフェイスである JdbcOprations
や NamedParameterOperations
のまま扱っているため、それに合わせるようにしました。
また今回の継承元にした SimpleJdbcRepository
のコンストラクタ引数は JdbcAggregateOperations
RelationalPersistentEntity
JdbcConverter
の3つでしたが、それに加えて JdbcOperations
が必要となるため、コンストラクタに追加した形です。
ちなみに前回までの独自実装では RecordMapper
という自作の RowMapper
を利用していましたが、ここでは spring-data-jdbc
が提供する EntityRowMapper
を利用するようにしました。こういうクラスを利用できるのも、独自Spring Dataを作らずに、Spring Data JDBCの拡張として作るメリットの一つですね。
どうあれこれでベースとなるRepositoryクラスはできあがりました。
Repositoryを提供するRepositoryFactoryを作る
続いて、いま作成した JdbcExtDefaultRepository
のインスタンスを生成するためのFactoryクラスを作成します。これも spring-data-jdbc
の JdbcRepositoryFactory
クラスを継承して作成します。
public class JdbcExtRepositoryFactory extends JdbcRepositoryFactory { @Override protected Object getTargetRepository(RepositoryInformation repositoryInformation) { return null; } @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata repositoryMetadata) { return JdbcExtDefaultRepository.class; } }
まず一つ目のポイントが getRepositoryBaseClass
でいま作成した JdbcExtDefaultRepository.class
を返すことです。継承元である JdbcRepositoryFactory
ではここで SimpleJdbcRepository.class
を返しており、要するにRepositoryの実体クラスを指定するわけです。この getRepositoryBaseClass
を書き換えることでその実体クラスを変更できるということです。
ちなみに冒頭の方で @EnableJdbcRepositories
アノテーションで repositoryBaseClass
を指定すれば基底クラスを変更できると書きましたが、そちらで指定されていた場合は、ここの getRepositoryBaseClass
で返すクラスよりも優先して使われるようです。
RepositoryFactoryの getTargetRepository
を実装する
続いて getTargetRepository
を実装しましょう。
@Override protected Object getTargetRepository(RepositoryInformation repositoryInformation) { JdbcAggregateTemplate template = new JdbcAggregateTemplate(publisher, context, converter, accessStrategy); if (entityCallbacks != null) { template.setEntityCallbacks(entityCallbacks); } RelationalPersistentEntity<?> persistentEntity = context .getRequiredPersistentEntity(repositoryInformation.getDomainType()); return getTargetRepositoryViaReflection(repositoryInformation, jdbcOperations, template, persistentEntity, converter); }
ここは継承元の JdbcRepositoryFactory
の実装とほぼ同じなのですが、最後の getTargetRepositoryViaReflection
のところで引数に jdbcOperations
を増やしています。ここで指定することで JdbcExtDefaultRepository
のコンストラクタに JdbcOperations
のインスタンスを渡すことができるのです。
RepositoryFactoryのコンストラクタ群を実装する
ちなみにここまでで説明していませんでしたが JdbcExtRepositoryFactory
のフィールドやコンストラクタは次のように実装しています。
public class JdbcExtRepositoryFactory extends JdbcRepositoryFactory { private final RelationalMappingContext context; private final JdbcConverter converter; private final ApplicationEventPublisher publisher; private final DataAccessStrategy accessStrategy; private final NamedParameterJdbcOperations operations; private EntityCallbacks entityCallbacks; public JdbcExtRepositoryFactory(DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations) { super(dataAccessStrategy, context, converter, dialect, publisher, operations); this.publisher = publisher; this.context = context; this.converter = converter; this.accessStrategy = dataAccessStrategy; this.operations = operations; } }
継承元である JdbcRepositoryFactory
の各フィールドが protected
になっていればそれをそのまま使いたかったんですが、残念ながらすべて private
であるために、このクラスでもフィールドとして保持するようにしています。しかも JdbcRepositoryFactory
はイミュータブルでも何でもなく、後からフィールドが書き換えられる可能性があるので、その辺りの対応のためにセッターを書いたりする必要もありました。
@Override public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; super.setEntityCallbacks(entityCallbacks); }
もうちょいSpring Data JDBC自身が拡張しやすい形になってくれていれば嬉しかったのですけどね。
どうあれこれで、Factoryも作成できました。
RepositoryFactoryを提供するRepositoryFactoryBeanを作る
続いて、いま作成した JdbcExtRepositoryFactory
のインスタンスを生成するためのFactoryBeanクラスを作成します。これは spring-data-jdbc
の JdbcRepositoryFactoryBean
クラスを参考にしながら・・・全コピペで作成しました。
コピペした理由は後で説明するとして、コピペしたうえで修正した部分は次の箇所のみです。
public class JdbcExtRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> implements ApplicationEventPublisherAware { @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcExtRepositoryFactory(dataAccessStrategy, mappingContext, converter, dialect, publisher, operations); jdbcRepositoryFactory.setQueryMappingConfiguration(queryMappingConfiguration); jdbcRepositoryFactory.setEntityCallbacks(entityCallbacks); jdbcRepositoryFactory.setBeanFactory(beanFactory); return jdbcRepositoryFactory; } }
具体的には new JdbcRepositoryFactory
を new JdbcExtRepositoryFactory
にしただけです。
たったそれだけのためにクラスの全コードをコピペしなければならなかった理由は、上で説明した JdbcRepositoryFactory
と同じで、すべてのフィールドが private
かつミュータブルなので、いつどのように値が書き換えられるか分からないためです。@EnableJdbcRepositories
アノテーションでカスタマイズ可能な部分なんだから、もうちょいカスタマイズしやすい形にしてくれよという気持ちになります。
さすがにコピペしてしまうとメンテナンス性に疑問もありますから、リフレクションでprivateフィールドにアクセスしてしまった方が良いのかな、いやでもそんなことするとGraalVMでAOTコンパイルする時にまた面倒なことになるよな、なんて過剰に将来性を考えたりしつつ、とりあえずは全コピペする方向にしました。
全フィールドをprotectedにするとか、FactoryBeanだけでなくFactoryをカスタマイズ可能にする、みたいなことをSpring Data JDBCの開発者にお願いしたら通るもんなんですかね。もうちょっと真面目に開発・メンテナンスするつもりになったら、issueでも立てて相談してみます。
どうあれほぼほぼコピペですが、JdbcExtRepositoryFactoryBean
まで作成できました。これで作るものは完成です。
3. サンプルアプリケーションから動かしてみる
それでは、ここまでで作った箇所をサンプルアプリケーションから利用してみます。
pom.xmlの変更
まずはpom.xmlで今回作成した data-jdbc-ext
モジュールに依存するようにします。
<dependency> <groupId>ninja.cero.data.sql</groupId> <artifactId>data-jdbc-ext</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
アプリケーション起動クラスの変更
アプリケーションの起動クラスにSpring Data JDBC標準の @EnableJdbcRepositories
を付けたうえで今回作成した JdbcExtRepositoryFactoryBean
を指定します。
@SpringBootApplication @EnableJdbcRepositories(repositoryFactoryBeanClass = JdbcExtRepositoryFactoryBean.class) public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
これでSpring Data JDBCの元々のFactoryBeanである JdbcRepositoryFactoryBean
の代わりに自作の JdbcExtRepositoryFactoryBean
が使われるようになります。
Repositoryクラスの変更
リポジトリではインターフェイスに今回作成した JdbcExtRepository
を指定します。
@Repository public interface EmpRepository extends JdbcExtRepository<Emp, Long> { }
これでSpring Dataが提供するメソッド群に加えて JdbcExtRepository
が提供する query
メソッドが使えるようになります。
Controllerクラスの変更
Controllerでは3つのエンドポイントを作りました。Spring Dataが標準で提供する findAll
と findById
を利用するものと、JdbcExtRepository
が提供する query
を利用するものです。
@RestController public class EmpController { EmpRepository empRepository; @GetMapping("/") Iterable<Emp> findAll() { return empRepository.findAll(); } @GetMapping("/{id}") Optional<Emp> findById(@PathVariable Long id) { return empRepository.findById(id); } @GetMapping("/search/{name}") List<Emp> search(@PathVariable String name) { return empRepository.query("select * from emp where name like '%' || ? || '%'", name); } }
この3つのエンドポイントがすべて機能すれば、Spring Data JDBCの標準機能と、今回追加した独自拡張の両方がきちんと動いているということになります。
起動して試してみる
いざ、起動して試してみましょう!
% curl localhost:8080/
[{"id":1,"name":"Nakamoto"},{"id":2,"name":"Kikuchi"},{"id":3,"name":"Mizuno"},{"id":4,"name":"Sayashi"},{"id":5,"name":"Fujihira"},{"id":6,"name":"Okazaki"}]
% curl localhost:8080/2
{"id":2,"name":"Kikuchi"}
% curl localhost:8080/search/ka
[{"id":1,"name":"Nakamoto"},{"id":6,"name":"Okazaki"}]
全件検索、IDによる検索、そして「ka」という文字列を含んだ検索のすべてが正常に動作しました!!🎊🎊🎊🎉🎉🎉🥳🥳🥳🎉🎉🎉🎊🎊🎊
そりゃまぁ動くだろうと思って作ってはいますが、実際に動いてみると嬉しいもんですね。
まとめ
今回作ったものを振り返ると、次のようになります。
- Repositoryの共通インターフェイスとなる
JdbcExtRepository
- そのデフォルト実装となる
JdbcExtDefaultRepository
- そのインスタンスを生成する
JdbcExtRepositoryFactory
- さらにそのインスタンスを生成する
JdbcExtRepositoryFactoryBean
- そのBeanを
@EnableJdbcRepositories
アノテーションで指定して使えるようにする
たったこれだけですので、いったん分かってしまえば独自Spring Data JDBC拡張するのはさほど難しくないと言えると思います。
今回作ったコードはすべてGitHubに置いています。
また今回は実装している様子をYoutubeでライブ配信しました。
配信の動画を後から見て楽しいものだとは思わないのですが、僕がどんなことを考えながら実装しているかが分かると思いますし、一緒にコードを読みながら勉強してもらっても良いと思いますので、今後もちょくちょくライブ配信してみようかなと思います。
そんなわけで、好評価・チャンネル登録、よろしくお願いします!(お願いしません)