Spring Data JDBC
タグバンガーズでは Spring Data JPA をデータベースアクセスに使用することが多いのですが、
今回は「シンプルに!」がコンセプトの Spring Data JDBC に触れて見たいと思います。
リリースされてから結構経ってしまいましたが早速いってみよう。
シンプル
JPA は便利で高機能です。それが故に複雑になりがちです。
複雑さを生むのは以下のような要素があるからだと言われます。
- 遅延ロード
- エンティティのプロキシ
- セッション / 1st レベルキャッシュ
- エンティティの監視
こんなに機能必要ない。そんなときは Spring Data JDBC を使いましょう
サンプルコード
以降記事内のコードは Github のレポジトリに公開しております。
https://github.com/DaiYamask/spring-data-jdbc-sample
Mavenの設定
依存関係に spring-data-jdbc を追加しましょう
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jdbc</artifactId> </dependency>
Entity
DDDに明るくないのですが、各プロパティは final で宣言し、イミュータブルな User オブジェクトを想定します。
public class User { @Id private final Long id; private final String name; private final Integer age; }
ファクトリーメソッドを提供しよう
オブジェクトの生成のために引数を持つコンストラクタが複数ある場合は `@PersistenceConstructor` を付与することで Spring Data に使ってほしいコンストラクタを伝えることができます。
@Id private final Long id; private final String name; private final Integer age; public User(String name, Integer age) { this(null, name, age); } @PersistenceConstructor public User(Long id, String name, Integer age) { this.id = id; this.name = name; this.age = age; }
ですが、 `@PersistenceConstructor` がなければ更によいとされています。そのためにはファクトリーメソッドを提供しましょう。
また、コンストラクタが減ることによるパフォーマンスの向上も期待できます。
public static User of(String name, Integer age) { return new User(null, name, age); }
@AllArgsConstructor を使う
今更ですが Lombok を使って、記述を減らしましょう。とドキュメントにも書いてあることを発見したのでおすすめしておきたいです。
@AllArgsConstructor public class User { @Id private final Long id; private final String name; private final Integer age; public static User of(String name, Integer age) { return new User(null, name, age); }
wither メソッドを追加しよう
プロパティをイミュータブルにしているため、 wither メソッドも必要です。
public User withId(Long id){ return new User(id, name, age); }
どういうことか簡単なテストで確認してみましょう。
@Test public void saveAndFindUser() { User user = User.of("タグバンガーズ太郎", 8); User savedUser = userRepository.save(user); Assertions.assertThat(user).isNotEqualTo(savedUser); }
つまり、 User を生成して永続化すると、新しい User が返されるので wither が必要ということになります。
ちなみに、以下のように @Wither をプロパティに付与すると更に記述をへらすことも可能です。
@Id @Wither private final Long id;
One to Many - 1 対 多 -
User は Nickname を複数持っている状況を考えます
public class Nickname { @Id private Long id; private String name; private Nickname(Long id, String name) { this.id = id; this.name = name; } public static Nickname of(String name) { return new Nickname(null, name); } }
User は Role を複数持てるので追加
public class User { @Id @Wither private final Long id; private final String name; private final Integer age; private List<Nickname> nicknames;
・
・
・
テストしてみる
@Test public void oneToOne() { Nickname nickname1 = Nickname.of("たろー"); Nickname nickname2 = Nickname.of("タグバン"); User user = User.of("タグバンガーズ太郎", 33, Arrays.asList(nickname1, nickname2)); User savedUser = userRepository.save(user); Assertions.assertThat(savedUser).isNotNull(); Optional<Role> role1 = roleRepository.findById(savedUser.getNicknames().get(0).getId()); Assertions.assertThat(role1.isPresent()).isTrue(); userRepository.delete(savedUser); Optional<Role> role2 = roleRepository.findById(savedUser.getNicknames().get(0).getId()); Assertions.assertThat(role2.isPresent()).isFalse(); }
ログを確認すると、保存して、さらに削除できていることがわかります。
ここで注目したいのは関連している Nickname もすべて削除されるということです。
Nickname は通常 User が消されれば消えても良さそうですが、 Role(権限)の用に他の User からも参照されるようなものはどうでしょうか。
この挙動では困ります。
Executing SQL update and returning generated keys Executing prepared SQL statement [INSERT INTO user (name, age) VALUES (?, ?)] Executing SQL update and returning generated keys Executing prepared SQL statement [INSERT INTO nickname (nickname, user_key, user) VALUES (?, ?, ?)] Executing SQL update and returning generated keys Executing prepared SQL statement [INSERT INTO nickname (nickname, user_key, user) VALUES (?, ?, ?)] Executing prepared SQL query Executing prepared SQL statement [SELECT nickname.id AS id, nickname.nickname AS nickname FROM nickname WHERE nickname.id = ?] Executing prepared SQL update Executing prepared SQL statement [DELETE FROM nickname WHERE user = ?] Executing prepared SQL update Executing prepared SQL statement [DELETE FROM user WHERE id = ?] Executing prepared SQL query Executing prepared SQL statement [SELECT nickname.id AS id, nickname.nickname AS nickname FROM nickname WHERE nickname.id = ?]
Many to Many - 多 対 多 -
Repository
簡単な CRUD を実装(?)してみましょう
public interface UserRepository extends CrudRepository<User, Long> { }
見慣れている人はなんの感動もないかもしれないですが、これだけで以下が自動で生成されます。
save や findByID などはよく使いますが、 exist、count もあるんだなと改めて感動。
<S extends T> S save(S var1); <S extends T> Iterable<S> saveAll(Iterable<S> var1); Optional<T> findById(ID var1); boolean existsById(ID var1); Iterable<T> findAll(); Iterable<T> findAllById(Iterable<ID> var1); long count(); void deleteById(ID var1); void delete(T var1); void deleteAll(Iterable<? extends T> var1); void deleteAll();
テスト
@Test public void save() { User user = User.of("タグバンガーズ太郎", 8); User savedUser = userRepository.save(user); Assertions.assertThat(savedUser).isNotNull(); Assertions.assertThat(user).isNotEqualTo(savedUser); } @Test public void saveAndFind() { User user = User.of("タグバンガーズ太郎", 8); User savedUser = userRepository.save(user); Optional<User> findUser = userRepository.findById(savedUser.getId()); Assertions.assertThat(findUser.get()).isNotNull(); Assertions.assertThat(findUser.get().getAge()).isEqualTo(8); Assertions.assertThat(findUser.get().getName()).isEqualTo("タグバンガーズ太郎"); } @Test public void saveAndDelete() { User user = User.of("タグバンガーズ太郎", 8); User savedUser = userRepository.save(user); userRepository.deleteById(savedUser.getId()); Iterable<User> users = userRepository.findAll(); Assertions.assertThat(users.iterator().hasNext()).isFalse(); } @Test public void saveAndCount() { User taro = User.of("タグバンガーズ太郎", 8); User hanako = User.of("タグバンガーズ花子", 10); userRepository.saveAll(Arrays.asList(taro, hanako)); long countUser = userRepository.count(); Assertions.assertThat(countUser).isEqualTo(2L); } @Test public void saveAndExist() { User user = User.of("タグバンガーズ太郎", 8); User savedUser = userRepository.save(user); boolean exists = userRepository.existsById(savedUser.getId()); Assertions.assertThat(exists).isTrue(); }
Spring Data REST
Spring Data REST もよく使うので確認しましょう。
CrudRepository は追加済なので、あとは Spring Data REST を依存関係を追加
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency>
User を追加
User user1 = User.of("タグバンガーズたろう", 19); User user2 = User.of("やまさきだい", 33); repository.save(user1); repository.save(user2);
Get してみる
curl http://localhost:8080/users { "_embedded" : { "users" : [ { "name" : "タグバンガーズたろう", "age" : 19, "_links" : { "self" : { "href" : "http://localhost:8080/users/1" }, "user" : { "href" : "http://localhost:8080/users/1" } } }, { "name" : "やまさきだい", "age" : 33, "_links" : { "self" : { "href" : "http://localhost:8080/users/2" }, "user" : { "href" : "http://localhost:8080/users/2" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/users" }, "profile" : { "href" : "http://localhost:8080/profile/users" } } }
QueryDSL未対応問題 (2019年4月4日現在)
public interface UserRepository extends QuerydslPredicateExecutor<User> { }
で、テストをしましたが、エラー。
java.lang.IllegalStateException: No query specified on findOne
Spring Data JDBC の実装が以下の通りで @Query アノテーションを期待されているので、やはり未対応。。
private String determineQuery() { String query = this.queryMethod.getAnnotatedQuery(); if (StringUtils.isEmpty(query)) { throw new IllegalStateException(String.format("No query specified on %s", this.queryMethod.getName())); } else { return query; } }
APT で生成
DBを参照して生成
jOOQ (じゅーく)を使ってみる
ATP ではなく データベースのスキーマ(DDL) ベースでメタモデルを作ってくれるようなのでこちらも試してみましょう
jOOQ の Config
@Configuration public class Config { @Autowired DataSource dataSource; @Bean public DataSourceConnectionProvider connectionProvider() { return new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(dataSource)); } @Bean DefaultDSLContext dsl() { return new DefaultDSLContext(configuration()); } public DefaultConfiguration configuration() { DefaultConfiguration jooqConfiguration = new DefaultConfiguration(); jooqConfiguration.set(connectionProvider()); return jooqConfiguration; } }
jOOQ の CustomRepository を作成
public interface JooqCustomUserRepository { List<User> findByName(String name); }
public class JooqCustomUserRepositoryImpl implements JooqCustomUserRepository { @Autowired private DSLContext dslContext; @Override public List<User> findByName(String name) { return this.dslContext.select().from(USER).where(USER.NAME.equal(name)) .fetchInto(User.class); } }
public interface UserRepository extends CrudRepository<User, Long>, JooqCustomUserRepository { }
テスト
@Test public void findByName() { User user1 = User.of("タグバンガーズ太郎", 8); userRepository.save(user1); User user2 = User.of("やまさきだい", 8); userRepository.save(user2); List<User> users = userRepository.findByName("タグバンガーズ太郎"); Assertions.assertThat(users.size()).isEqualTo(1); }
さいごに
対 JPA で見た場合出来ることが限られ、キャッシュ、遅延ロードなどなど便利だけど複雑性を生んでいる機能が省かれています。
シンプルって難しいよねという議論(シンプルとイージーは違うよねという話)が弊社では持ち切りですが、Spring Data JDBC の発展に期待しつつ。また次回。