独自Spring Dataを作ってみる その2 - findAllを作る
前回のエントリーで自作Spring Dataの扉を開けましたので、今回は実際に全件検索、findAllが動くところまで進めてみます。
TL;DR
このエントリーは長文すぎて誰も読まないと思うので、先にまとめておきます。
- findAllだけ動く独自Spring DataとサンプルアプリケーションをGitHubに置いといたよ(リンク)
spring-data-relational
のSqlRenderer
StatementBuilder
Select
Table
Column
クラスなどを利用してSQLの文字列を作成することができるのが、ちょっと楽しかったよ- この先は
Spring Data JDBC
のコードリーディングしたメモみたいなものだよ - 「ここから先の長文を読む者は一切の希望を捨てよ」
1. Repositoryクラスのインスタンス生成の流れを追う
findAllについては提供したいものがSpring Data JDBCと完全に同じなので、Spring Data JDBCのコードを読みながら再実装します。
SimpleJdbcRepository
の findAll
実装を読む
spring-data-jdbc
の SimpleJdbcRepository
の findAll
の実装は次のようになっています。
@Override public Iterable<T> findAll() { return entityOperations.findAll(entity.getType()); }
ここで使われている entityOperations
と entity
は SimpleJdbcRepository
のインスタンス変数として定義されています。
private final JdbcAggregateOperations entityOperations; private final PersistentEntity<T, ?> entity; private final RelationalExampleMapper exampleMapper;
このうち JdbcAggregateOperations
は、spring-data-jdbc
が提供しているクラスで、クエリの実行を行っています。軽くソースを眺めてみましたが相当作り込んでいて目が回りました。メインとなる処理は JdbcTemplate
にSQL文字列を渡して実行するものですね。なのでここは目的を最短で達成するために、代わりに JdbcTemplate
を使いたいと思います。
PersistentEntity
と RelationalExampleMapper
は spring-data-commons
が提供しているクラスです。それぞれ保存・取得するエンティティの情報を扱うクラス(インターフェイス)と、検索条件を構築するための Exampler
を扱うためのクラスです。Exampler
については今は使わないのでいったん無視して、PersistentEntity
についてのみ考えることにします。
SimpleJdbcRepository
のインスタンス変数は誰が作って誰が渡しているのか?
これらのインスタンス変数ですが、いずれもコンストラクタで設定しています。
public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, PersistentEntity<T, ?> entity, JdbcConverter converter) { Assert.notNull(entityOperations, "EntityOperations must not be null"); Assert.notNull(entity, "Entity must not be null"); this.entityOperations = entityOperations; this.entity = entity; this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext()); }
entityOperations
と entity
についてはコンストラクタの引数で渡されたものをそのまま利用し、exampleMapper
はコンストラクタで渡された JdbcConverter
から作成しています。
このコンストラクタ、誰が呼び出しているのでしょうか。IntelliJで SimpleJdbcRepository
のコンストラクタをOpen Call Hierarchyしても 呼び出し元は見つかりません。しばらくここで彷徨ってしまったのですが、ふと思い出しました。前回実装した SqlRepositoryFactory
でRepositoryのインスタンスを作っているのでしたね。
こんなシンプルな実装にしていました。
@Override protected Object getTargetRepository(RepositoryInformation metadata) { return getTargetRepositoryViaReflection(metadata); }
ここで呼び出している getTargetRepositoryViaReflection
メソッドは、実際にはこんな実装になっています。
protected final <R> R getTargetRepositoryViaReflection(Class<?> baseClass, Object... constructorArguments) { return instantiateClass(baseClass, constructorArguments); }
見て分かる通り、可変長引数でコンストラクタに渡すオブジェクトを指定できるようです。
spring-data-jdbc
の JdbcRepositoryFactory
クラスの 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, template, persistentEntity, converter); }
やはり JdbcAggregateTemplate
と RelationalPersistentEntity
と converter
を引数に渡しています。これは SimpleJdbcRepository
のコンストラクタと一致していますので、ここでコンストラクタに渡していると考えて間違いありません。
ではこれを真似て実装し、、、たいところなのですが、ここで RelationalPersistentEntity
のインスタンスを生成するために context
というインスタンス変数が利用されています。これは JdbcRepositoryFactory
のインスタンス変数です。
private final RelationalMappingContext context;
次はこれを追う必要があります。
JdbcRepositoryFactory
のインスタンス変数は誰が作って誰が渡しているのか?
この context
を含むインスタンス変数は、いずれも JdbcRepositoryFactory
のコンストラクタで渡されています。
public JdbcRepositoryFactory(DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations) { // (略) this.context = context; this.converter = converter; this.dialect = dialect; this.accessStrategy = dataAccessStrategy; this.operations = operations; }
JdbcRepositoryFactory
のインスタンスを作っているのは JdbcRepositoryFactoryBean
ですから、そちらでインスタンス生成を行っている部分のコードを見直します。
@Override protected RepositoryFactorySupport doCreateRepositoryFactory() { JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(dataAccessStrategy, mappingContext, converter, dialect, publisher, operations); // (略) return jdbcRepositoryFactory; }
見ての通り、コンストラクタで変数を渡しています。このうち context
は第二引数ですから mappingContext
というオブジェクトがそれにあたります。これは JdbcRepositoryFactoryBean
のインスタンス変数です。
private RelationalMappingContext mappingContext;
次はこの mappingContext
を追っていくことになります。
JdbcRepositoryFactoryBean
の mappingContext
は誰が作って誰が渡しているのか?
この mappingContext
を設定している箇所ですが、次の部分しかありません。
@Autowired public void setMappingContext(RelationalMappingContext mappingContext) { Assert.notNull(mappingContext, "MappingContext must not be null"); super.setMappingContext(mappingContext); this.mappingContext = mappingContext; }
は? Autowired? 誰がインスタンスを作ってるの? 誰が呼び出してるの? っていうかフレームワークの中でもそういう事しちゃうの?
という感じで少し混乱しましたが、何にせよ @Autowired
しているということは、どこかでインスタンス生成をしているに違いありません。このコンストラクタを Open Call Hierarchy してみると、、、ありました。
AbstractJdbcConfiguration
で生成しているようです。このクラスを見てみましょう。
@Configuration(proxyBeanMethods = false) public class AbstractJdbcConfiguration implements ApplicationContextAware { (略) @Bean public JdbcMappingContext jdbcMappingContext(Optional<NamingStrategy> namingStrategy, JdbcCustomConversions customConversions, RelationalManagedTypes jdbcManagedTypes) { JdbcMappingContext mappingContext = new JdbcMappingContext(namingStrategy.orElse(DefaultNamingStrategy.INSTANCE)); mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); mappingContext.setManagedTypes(jdbcManagedTypes); return mappingContext; } (略) }
なるほどね。@Configuration
をつけたクラスを作り、その中に @Bean
をつけたメソッドでインスタンスを返せばSpringコンテナの管理下に入るので、それを @Autowired
させて使っているということですね。JdbcMappingContext
は RelationalMappingContext
を継承したものなので、ここで生成した JdbcMappingContext
のBeanが RelationalMappingContext
としてAutowiredされるようです。
AbstractJdbcConfiguration
には他にもいくつかのBean定義があり、それらを JdbcRepositoryFactoryBean
でAutowiredしているところがありました。そういうお作法でやっているのだと理解すれば、簡単な話ですね。また後で必要なインスタンスがAutowiredされているのを見たら、AbstractJdbcConfiguration
に戻ってくればインスタンスの生成を確認できそうです。
たどってたどって長くなりましたが mappingContext
についてはこれで解決です。これでRepositoryの作成に必要となるインスタンスは準備できそうです。
2. SQL組み立ての流れを追う
Repositoryクラスのインスタンス生成についての流れは大まかに把握できたので、次は findAll
の内部の実装がどうなっているかの確認です。
findAllの実行箇所を追う
再掲になりますが、SimpleDataJdbc
の findAll
の実装は次のようになっています。
@Override public Iterable<T> findAll() { return entityOperations.findAll(entity.getType()); }
この entityOperations.findAll
の実装は、JdbcAggregateTemplate
クラスで提供されています。
@Override public <T> Iterable<T> findAll(Class<T> domainType) { Assert.notNull(domainType, "Domain type must not be null"); Iterable<T> all = accessStrategy.findAll(domainType); return triggerAfterConvert(all); }
ここで accessStrategy.findAll
というメソッドを呼び出しています。これの実装は複数のクラスで提供されていますが、最もシンプルそうな DefaultDataAccessStrategy
クラスの findAll
メソッドを確認します。
@Override public <T> Iterable<T> findAll(Class<T> domainType) { return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType)); }
ようやくそれらしいコードがでてきました。
operations.query
は、引数で指定されたSQLを実行するもの。JdbcTemplate
の query
メソッドですね。
sql(domainType).getFindAll()
は、全件検索のSQLを組み立てるもの。
getEntityRowMapper(domainType)
は、ResultSet
をEntityにマッピングするための RowMapper
を提供するもの。これも JdbcTemplate
を使ったことがあれば馴染みのあるものです。
ということで最も分からないSQLの組み立て部分を追っていきます。
SQLの組み立てを追う
まずは sql
メソッドを確認してみます。
private SqlGenerator sql(Class<?> domainType) { return sqlGeneratorSource.getSqlGenerator(domainType); }
sql
メソッドはEntityに対応した SqlGenerator
クラスを返すもののようです。見るからにSQLをジェネレートしそうなクラス名です。
この呼び出し先である SqlGeneratorSource
クラスの getSqlGenerator
についても確認しておきます。
SqlGenerator getSqlGenerator(Class<?> domainType) { return CACHE.computeIfAbsent(domainType, t -> new SqlGenerator(context, converter, context.getRequiredPersistentEntity(t), dialect)); }
SqlGenerator
のインスタンスを作ってキャッシュしておく処理のようですね。とりあえず今のところは無視しましょう。
sql(domainType).getFindAll()
の処理に戻って SqlGenerator
の実装を確認します。
String getFindAll() {
return findAllSql.get();
}
生成済みである findAllSql
を返しているようです。なるほどね、findAllのようなSQLは特にコンテキストで内容が変わるものではないから、使ったものを使い回しているのでしょうね。
ではこの findAllSql
を確認しましょう。SqlGenrator
クラスのインスタンス変数として定義されています。
private final Lazy<String> findAllSql = Lazy.of(this::createFindAllSql);
Lazy
ということは遅延ローディングですね。初めて必要になって初めてSQL文を組み立てて、次回以降はそれを使い回す形だと理解できます。
ではここで呼んでいる createFindAllSql
を確認しましょう。そろそろ核心に近づいてきた気がします。
private String createFindAllSql() { return render(selectBuilder().build()); }
private String render(Select select) { return this.sqlRenderer.render(select); }
まだ核心ではありませんでした。sqlRenderer
に selectBuilder().build()
したものを渡すことで、SQL文を作成しているようです。
ここまではずっと spring-data-jdbc
のクラスを追ってきましたが、ここで使われている SqlRenderer
クラスや SelectBuilder
クラスは spring-data-relational
のクラスです。spring-data-relational
は spring-data-commons
と同じようにSpring Dataを作るための共通ライブラリのようなものですが、特にRDBMSに関連するクラス群を提供しているようです。
そういう立ち位置のため、自作のSpring Dataが spring-data-jdbc
に依存することはできませんが、spring-data-relational
なら依存して利用することができます。なのであまりコードを読み込まずに利用しても良いのですが、念のため実装を簡単に確認しておきましょう。
SqlRenderer
の render
メソッドは次のようになっています。
@Override public String render(Select select) { SelectStatementVisitor visitor = new SelectStatementVisitor(context); select.visit(visitor); return visitor.getRenderedPart().toString(); }
この内部実装はあまり真面目に追うつもりはありませんが、SelectStatementVisitor
に次のような実装を見つけました。
@Override public Delegation doLeave(Visitable segment) { if (segment instanceof Select) { Select select = (Select) segment; builder.append("SELECT "); if (select.isDistinct()) { builder.append("DISTINCT "); } builder.append(selectList); builder.append(selectRenderContext.afterSelectList().apply(select)); if (from.length() != 0) { builder.append(" FROM ").append(from); } builder.append(selectRenderContext.afterFromTable().apply(select)); if (join.length() != 0) { builder.append(' ').append(join); } if (where.length() != 0) { builder.append(" WHERE ").append(where); } CharSequence orderBy = orderByClauseVisitor.getRenderedPart(); if (orderBy.length() != 0) { builder.append(" ORDER BY ").append(orderBy); } builder.append(selectRenderContext.afterOrderBy(orderBy.length() != 0).apply(select)); return Delegation.leave(); } return Delegation.retain(); }
めちゃくちゃゴリゴリにSQL文字列を組み立てていますね。どうあれ SqlRender
というクラスが、引数で渡された Select
を元にSQL文を文字列として組み立てていることが理解できました。この処理を追うのはこれぐらいにして、戻りましょう。
SqlRenderer
は誰が作って誰が渡しているのか?
SqlRenderer
は SqlGenerator
のコンストラクタで作られています。
SqlGenerator(RelationalMappingContext mappingContext, JdbcConverter converter, RelationalPersistentEntity<?> entity, Dialect dialect) { // (略) this.renderContext = new RenderContextFactory(dialect).createRenderContext(); this.sqlRenderer = SqlRenderer.create(renderContext); // (略) }
これを見る限り dialect
と renderContext
さえあればインスタンス生成ができそうです。
SqlGenerator
のコンストラクタは、先ほども記載した SqlGeneratorSource
クラスの getSqlGenerator
メソッドで呼び出しています。
SqlGenerator getSqlGenerator(Class<?> domainType) { return CACHE.computeIfAbsent(domainType, t -> new SqlGenerator(context, converter, context.getRequiredPersistentEntity(t), dialect)); }
ここでコンストラクタ引数として指定している context
と dialect
は SqlGeneratorSource
のコンストラクタで指定されています。
public SqlGeneratorSource(RelationalMappingContext context, JdbcConverter converter, Dialect dialect) { // (略) this.context = context; this.converter = converter; this.dialect = dialect; }
どんどん遡っていきましょう。
SqlGeneratorSource
のインスタンスは JdbcRepositoryFactoryBean
で作っています。
SqlGeneratorSource sqlGeneratorSource = new SqlGeneratorSource(this.mappingContext, this.converter, this.dialect);
mappingContext
は先ほど @Autowired
されていたインスタンスですね。そうすると dialect
も同様でしょうかね。そう思って JdbcRepositoryFactoryBean
のコードを確認すると。
@Autowired public void setDialect(Dialect dialect) { Assert.notNull(dialect, "Dialect must not be null"); this.dialect = dialect; }
ビンゴ! ありました。ちなみにドラマに出てくるエンジニアはすぐ「ビンゴ!」と言いますが、実際にエンジニアが「ビンゴ」と発言することはありませんね。
@Autowired
されているということは、mappingContext
と同様に AbstractJdbcConfiguration
クラスで定義されていそうです。
@Bean public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { return DialectResolver.getDialect(operations.getJdbcOperations()); }
ビンゴ! ありました。またビンゴと言ってしまいました。
この DialectResolver
は spring-data-jdbc
のクラスでした。Dialect
の取得なんて共通処理なんだから spring-data-relation
にあるのかと思ってましたけど、ないんですね。
この Dialect
の取得処理をもう少し辿っていくと、次のようなメソッドに到達しました。
private static Dialect getDialect(Connection connection) throws SQLException { DatabaseMetaData metaData = connection.getMetaData(); String name = metaData.getDatabaseProductName().toLowerCase(Locale.ENGLISH); if (name.contains("hsql")) { return HsqlDbDialect.INSTANCE; } if (name.contains("h2")) { return H2Dialect.INSTANCE; } if (name.contains("mysql")) { return new JdbcMySqlDialect(getIdentifierProcessing(metaData)); } if (name.contains("mariadb")) { return new MariaDbDialect(getIdentifierProcessing(metaData)); } if (name.contains("postgresql")) { return JdbcPostgresDialect.INSTANCE; } if (name.contains("microsoft")) { return JdbcSqlServerDialect.INSTANCE; } if (name.contains("db2")) { return JdbcDb2Dialect.INSTANCE; } if (name.contains("oracle")) { return OracleDialect.INSTANCE; } LOG.info(String.format("Couldn't determine Dialect for \"%s\"", name)); return null; }
いや完全にゴリゴリに実装してますね、マジかよ。RDBへのコネクションを使って、そのメタデータからどのDBかを判定しています。昔ながらの方法です。
この辺りを実装するのも面倒ですから、いったんRDBは「H2」固定にするとします。もう少し実装が進んだ時に、きちんと実装することにしましょう。
どうあれこれで、SqlRenderer
を生成する流れも把握できました。
Select
の組み立て
さて、残すは SqlRenderer
に渡す Select
の組み立てです。SqlGenerator
の selectBuilder
の実装を確認しましょう。
private SelectBuilder.SelectWhere selectBuilder() { return selectBuilder(Collections.emptyList()); } private SelectBuilder.SelectWhere selectBuilder(Collection<SqlIdentifier> keyColumns) { Table table = getTable(); List<Expression> columnExpressions = new ArrayList<>(); List<Join> joinTables = new ArrayList<>(); for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext .findPersistentPropertyPaths(entity.getType(), p -> true)) { PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); // add a join if necessary Join join = getJoin(extPath); if (join != null) { joinTables.add(join); } Column column = getColumn(extPath); if (column != null) { columnExpressions.add(column); } } for (SqlIdentifier keyColumn : keyColumns) { columnExpressions.add(table.column(keyColumn).as(keyColumn)); } SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); for (Join join : joinTables) { baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); } return (SelectBuilder.SelectWhere) baseSelect; }
ざっと眺めてみると、何となくやりたいことは分かりますね。戻り値である baseSelect
から逆算し、また今回対応しない join
も無視して考えると、次のようなコードになりそうです。
return StatementBuilder.select(columnExpressions).from(getTable());
まずは Table
を作っている箇所から確認します。
private Table getTable() { return sqlContext.getTable(); }
さらにこの SqlContext
の実装も確認すると、コンストラクタで Table
クラスのインスタンスを作成していました。
SqlContext(RelationalPersistentEntity<?> entity) { this.entity = entity; this.table = Table.create(entity.getQualifiedTableName()); }
とりあえず RelationalPersistentEntity
さえあれば、問題なくインスタンスを作れるようです。これは既に出自の分かっているオブジェクトです。
続いて、Column
についても確認しましょう。columnExpressions
は、SELECT文の SELECT
直後に羅列するカラム名の一覧であることは容易に想像がつきます。
getColumn
メソッドはjoinやオブジェクトのネストなどを考慮しているため少し長い処理なのですが、シンプルなSELECT ALL処理だけで言うと、次の部分の処理になります。
return sqlContext.getColumn(path);
この SqlContext
の処理を追ってみます。
Column getColumn(PersistentPropertyPathExtension path) {
return getTable(path).column(path.getColumnName()).as(path.getColumnAlias());
}
Table
から PersistentPropertyPathExtension
を使って取れるみたいですね。
PersistentPropertyPathExtension
は上に書いた selectBuilder
の処理の中で生成しています。
for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext .findPersistentPropertyPaths(entity.getType(), p -> true)) { PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path);
for文が若干分かりづらいですが mappingContext
と entity
さえあれば問題なく PersistentPropertyPathExtension
が生成できそうで、特に別途必要なインスタンスはなさそうです。
どうあれ、ここまで見た流れで Select
と SqlRenderer
の組み合わせができそうです。
3. いったんSQL文を発行するサンプルを作ってみる
既存のコードを追ってばかりで分かりづらくなってきたので、ここまで読んだ内容を元に、いったん自作Spring DataでSQL文を生成するところまで作り込んでみます。
SqlRepositoryFactoryBeanでRelationalMappingContextの作成
まずは RelationalMappingContext
の生成ですが、spring-data-jdbc
では AbstractJdbcConfiguration
という @Congiguration
アノテーションをつけたクラスで行っていました。ただこのようなConfigurationを有効にするためにはAutoConfigurationなども必要になって面倒ですから、今回は固定で作ってしまいましょう。
どこでインスタンスを生成するか若干悩みますが spring-data-jdbc
では JdbcRepositoryFactoryBean
にAutowireしていましたので、それと同じように SqlRepositoryFactoryBean
クラスで生成するようにします。
public class SqlRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> { private final RelationalMappingContext mappingContext = new RelationalMappingContext(DefaultNamingStrategy.INSTANCE); protected SqlRepositoryFactoryBean(Class<? extends T> repositoryInterface) { super(repositoryInterface); } @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { return new SqlRepositoryFactory(mappingContext); } }
特に NamingStrategy
をカスタマイズする必要も今のところはないので、固定にしています。
SqlRepositoryFactoryでRepositoryのコンストラクタに渡す引数を指定
続いて SqlRepositoryFactory
で RelationalMappingContext
をコンストラクタで受け取るようにして、その RelationalMappingContext
と、そこから生成した RelationalPersistentEntity
をリポジトリ生成時のコンストラクタに渡すようにします。
public class SqlRepositoryFactory extends RepositoryFactorySupport { private final RelationalMappingContext mappingContext; public SqlRepositoryFactory(RelationalMappingContext mappingContext) { this.mappingContext = mappingContext; } @Override public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) { return null; } @Override protected Object getTargetRepository(RepositoryInformation metadata) { RelationalPersistentEntity<?> persistentEntity = mappingContext .getRequiredPersistentEntity(metadata.getDomainType()); return getTargetRepositoryViaReflection(metadata, persistentEntity, mappingContext); } @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) { return DefaultRepository.class; } }
この辺りも spring-data-jdbc
の劣化コピーといった感じの実装です。
DefaultRepositoryのコンストラクタで引数を受け取る
前回のエントリーで説明した通り、生成されるリポジトリのクラスは SqlRepositoryFactory#getRepositoryBaseClass
で返される DefaultRepository
になります。なので、その DefaultRepository
のコンストラクタで引数を受け取るようにします。
public class DefaultRepository<T, ID> implements CrudRepository<T, ID>, PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { private final RelationalPersistentEntity<T> entity; private final RelationalMappingContext mappingContext; private final SqlRenderer sqlRenderer; public DefaultRepository(RelationalPersistentEntity<T> entity, RelationalMappingContext mappingContext) { this.entity = entity; this.mappingContext = mappingContext; this.sqlRenderer = SqlRenderer.create(new RenderContextFactory(H2Dialect.INSTANCE).createRenderContext()); }
また SqlRenderer
もこのコンストラクタで生成することにしました。
DefaultRepositoryのfindAllを実装する
これで DefaultRepository
ではSQL文の組み立てに必要なものが揃った状態になるので findAll
を実装します。
@Override public List<T> findAll() { Table table = Table.create(entity.getQualifiedTableName()); List<Expression> columnExpressions = new ArrayList<>(); for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext .findPersistentPropertyPaths(entity.getType(), p -> true)) { PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); Column column = table.column(extPath.getColumnName()).as(extPath.getColumnAlias()); columnExpressions.add(column); } Select select = StatementBuilder.select(columnExpressions).from(table).build(); String sql = sqlRenderer.render(select); System.out.println(sql); return null; }
だいぶ端折った実装にしていますがエンティティクラスの情報を元に Table
を作成し、やはりエンティティクラスの情報を元にフィールド一覧から Column
の一覧を作り、そこから Select
を作成しています。
StatementBuilder.select(columnExpressions).from(table).build();
というのが、なかなか痺れるクエリビルディングなので、オレオレO/Rマッパーを作りたい人は、spring-data-relational
にあるこの辺りのクラスを読んで利用してみるのも良いと思います。
ということで、ここまでで findAll
の作成が済みました。
いざ、実行!
ではここまでで作った独自Spring Dataをサンプルアプリケーションから呼び出してみましょう。実装は前回と変わりませんが、念のためコードを貼っておきます。
@SpringBootApplication @EnableSqlRepositories public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
public record Emp (Long id, String name) {
}
@Repository public interface EmpRepository extends CrudRepository<Emp, Long> { }
@RestController public class EmpController { EmpRepository empRepository; public EmpController(EmpRepository empRepository) { this.empRepository = empRepository; } @GetMapping("/") Iterable<Emp> emp() { return empRepository.findAll(); } }
これで App
クラスを実行すると、問題なく起動します。
アクセスしてみましょう。
%curl localhost:8080 -i
HTTP/1.1 200 Content-Length: 0 Date: Fri, 23 Dec 2022 21:10:02 GMT
問題なく実行できています。そして、コンソールには・・・
SELECT "EMP"."ID" AS "ID", "EMP"."NAME" AS "NAME" FROM "EMP"
SQL文が表示されています、やりました!!!🎊🎊🎊🎉🎉🎉🥳🥳🥳🎉🎉🎉🎊🎊🎊
かったよ、、、ぼく!
4. 実際にSQLを発行する
SQL文まで発行できれば、もうこちらのもんですよね、後はSQLを実際に実行できるよう改修します。みんな大好きな JdbcTemplate
を使います。
SqlRepositoryFactoryBean
に JdbcTemplate
をAutowireする
まずは SqlRepositoryFactoryBean
に JdbcTemplate
を @Autowired
でセットできるようにします。もう少し先のクラスで指定しても良いのですが、spring-data-jdbc
がすべてこの FactoryBean
クラスに @Autowired
を集約していたのでそれに倣いました。
変更部分は次の通りです。
public class SqlRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> { private JdbcTemplate jdbcTemplate; @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { return new SqlRepositoryFactory(mappingContext, jdbcTemplate); } @Autowired public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }
@Autowired
で受け取って、渡すだけです。
SqlRepositoryFactory
のコンストラクタで受け取った JdbcTempalte
をRepsitoryのコンストラクタに渡す
続いて SqlRepositoryFactory
のコンストラクタで JdbcTemplate
を受け取り、それをRepository生成時のコンストラクタに渡せるようにします。
変更部分は次の通りです。
public class SqlRepositoryFactory extends RepositoryFactorySupport { private final JdbcTemplate jdbcTemplate; public SqlRepositoryFactory(RelationalMappingContext mappingContext, JdbcTemplate jdbcTemplate) { this.mappingContext = mappingContext; this.jdbcTemplate = jdbcTemplate; } @Override protected Object getTargetRepository(RepositoryInformation metadata) { RelationalPersistentEntity<?> persistentEntity = mappingContext .getRequiredPersistentEntity(metadata.getDomainType()); return getTargetRepositoryViaReflection(metadata, persistentEntity, mappingContext, jdbcTemplate); } }
コンストラクタで受け取って、渡すだけです。
DefaultRepository
の findAll
でクエリを実行する
そして同様に、DefaultRepository
のコンストラクタで JdbcTemplate
を受け取るようにします。
private final JdbcTemplate jdbcTemplate; public DefaultRepository(RelationalPersistentEntity<T> entity, RelationalMappingContext mappingContext, JdbcTemplate jdbcTemplate) { this.entity = entity; this.mappingContext = mappingContext; this.sqlRenderer = SqlRenderer.create(new RenderContextFactory(H2Dialect.INSTANCE).createRenderContext()); this.jdbcTemplate = jdbcTemplate; }
後は JdbcTemplate
を使ってクエリ発行するだけ、、、なのですが、JdbcTemplate
では ResultSet
をエンティティに変換するための RowMapper
が必要となります。その実装も必要なのですが、ここでは Bootiful SQL Template で作成した、ResultSet
を Record
にマッピングするための RecordMapper
をコピペして使います。
RecordMapper
のコードはこちらになります。実際にはこれを少しだけ改修しましたが、大きな話ではないので割愛します。
public class RecordMapper<T> implements RowMapper<T> { // (割愛) }
そしていよいよ findAll
の実装です。最後にクエリを実行する処理を入れます。
@Override public List<T> findAll() { // (略) String sql = sqlRenderer.render(select); return jdbcTemplate.query(sql, new RecordMapper<>(entity.getType())); }
これで実装が完了しました。あとは実行するだけです。
いざ、実行!!
テーブルがないと実行してもコケることが分かりきっているため、サンプルアプリケーション側でテーブルを作成します。
schema.sql
を次のような内容で作成します。ぜんぜん emp
っぽくないですが。
CREATE TABLE emp ( id BIGINT NOT NULL PRIMARY KEY, name VARCHAR(64) );
そして data.sql
を次のような内容で作成します。
INSERT INTO emp (id, name) VALUES (1, 'Nakamoto') ,(2, 'Kikuchi') ,(3, 'Mizuno') ,(4, 'Sayashi') ,(5, 'Fujihira') ,(6, 'Okazaki') ;
羅列された名前についてはご意見無用です。
これで準備は整いました。起動して実行してみましょう!
% 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"}]
きちんと取得結果が返ってきました!!!🎊🎊🎊🎉🎉🎉🥳🥳🥳🎉🎉🎉🎊🎊🎊
長すぎて誰もここまで読んでない気がしますが、とりあえず、僕はやりました、やりきりました。
まとめ
今回の話をまとめます。
spring-data-jdbc
ではAbstractJdbcConfiguration
で利用するBeanの定義をしている- この定義は
@Configuration
がついている。 - この定義を extends したクラスがAutoConfiguration設定されている
- この定義は
spring-data-jdbc
ではJdbcRepositoryFactoryBean
で利用するBeanを@Autowired
している- RepositoryFactoryの基底クラスである
RepositoryFactorySupport
のgetTargetRepository
メソッドには可変長引数でRepositoryのコンストラクタに渡す引数を指定することができる spring-data-relational
のRelationalMappingContext
とRelationalPersistentEntity
クラスがエンティティとテーブルをマッピングするための情報を作成するspring-data-relational
のSqlRenderer
StatementBuilder
Select
Table
Column
クラスなどを利用してSQLの文字列を作成することができる
正直、ほとんど実装してなくて、時間の大半は spring-data-jdbc
のコードリーディングに費やしたという感じですが、何にせよ無事に findAll
が動作するところまで動きました。
とりあえずここまでで動いたものをGitHubに置いておいたので、ちょっと読んでみたいという人はどうぞ。実質的には、ほんの数十行しか実装していません。 github.com
この後 DefaultRepository
の他のメソッドの実装を充実させていく分には、今回の延長でできそうなイメージが沸いてきました。
それよりもまだイメージの沸いていない任意のSQLの発行について、次回は挑戦したいと思います。