谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

独自Spring Dataを作ってみる その2 - findAllを作る

前回のエントリーで自作Spring Dataの扉を開けましたので、今回は実際に全件検索、findAllが動くところまで進めてみます。

TL;DR

このエントリーは長文すぎて誰も読まないと思うので、先にまとめておきます。

  • findAllだけ動く独自Spring DataとサンプルアプリケーションをGitHubに置いといたよ(リンク
  • spring-data-relationalSqlRenderer StatementBuilder Select Table Column クラスなどを利用してSQLの文字列を作成することができるのが、ちょっと楽しかったよ
  • この先は Spring Data JDBC のコードリーディングしたメモみたいなものだよ
  • 「ここから先の長文を読む者は一切の希望を捨てよ」

1. Repositoryクラスのインスタンス生成の流れを追う

findAllについては提供したいものがSpring Data JDBCと完全に同じなので、Spring Data JDBCのコードを読みながら再実装します。

SimpleJdbcRepositoryfindAll 実装を読む

spring-data-jdbcSimpleJdbcRepositoryfindAll の実装は次のようになっています。

@Override
public Iterable<T> findAll() {
    return entityOperations.findAll(entity.getType());
}

(in GitHub)

ここで使われている entityOperationsentitySimpleJdbcRepositoryインスタンス変数として定義されています。

private final JdbcAggregateOperations entityOperations;
private final PersistentEntity<T, ?> entity;
private final RelationalExampleMapper exampleMapper;

(in GitHub)

このうち JdbcAggregateOperations は、spring-data-jdbc が提供しているクラスで、クエリの実行を行っています。軽くソースを眺めてみましたが相当作り込んでいて目が回りました。メインとなる処理は JdbcTemplateSQL文字列を渡して実行するものですね。なのでここは目的を最短で達成するために、代わりに JdbcTemplate を使いたいと思います。

PersistentEntityRelationalExampleMapperspring-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());
}

(in GitHub)

entityOperationsentity についてはコンストラクタの引数で渡されたものをそのまま利用し、exampleMapper はコンストラクタで渡された JdbcConverter から作成しています。

このコンストラクタ、誰が呼び出しているのでしょうか。IntelliJSimpleJdbcRepository のコンストラクタを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);
}

(in GitHub)

見て分かる通り、可変長引数でコンストラクタに渡すオブジェクトを指定できるようです。

spring-data-jdbcJdbcRepositoryFactory クラスの 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);
}

(in GitHub)

やはり JdbcAggregateTemplateRelationalPersistentEntityconverter を引数に渡しています。これは SimpleJdbcRepository のコンストラクタと一致していますので、ここでコンストラクタに渡していると考えて間違いありません。

ではこれを真似て実装し、、、たいところなのですが、ここで RelationalPersistentEntityインスタンスを生成するために context というインスタンス変数が利用されています。これは JdbcRepositoryFactoryインスタンス変数です。

private final RelationalMappingContext context;

(in GitHub)

次はこれを追う必要があります。

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;
}

(in GitHub)

JdbcRepositoryFactoryインスタンスを作っているのは JdbcRepositoryFactoryBean ですから、そちらでインスタンス生成を行っている部分のコードを見直します。

@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
    JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(dataAccessStrategy, mappingContext,
            converter, dialect, publisher, operations);
// (略)
    return jdbcRepositoryFactory;
}

(in GitHub)

見ての通り、コンストラクタで変数を渡しています。このうち context は第二引数ですから mappingContext というオブジェクトがそれにあたります。これは JdbcRepositoryFactoryBeanインスタンス変数です。

private RelationalMappingContext mappingContext;

(in GitHub)

次はこの mappingContext を追っていくことになります。

JdbcRepositoryFactoryBeanmappingContext は誰が作って誰が渡しているのか?

この mappingContext を設定している箇所ですが、次の部分しかありません。

@Autowired
public void setMappingContext(RelationalMappingContext mappingContext) {
    Assert.notNull(mappingContext, "MappingContext must not be null");

    super.setMappingContext(mappingContext);
    this.mappingContext = mappingContext;
}

(in GitHub)

は? Autowired? 誰がインスタンスを作ってるの? 誰が呼び出してるの? っていうかフレームワークの中でもそういう事しちゃうの?

という感じで少し混乱しましたが、何にせよ @Autowired しているということは、どこかでインスタンス生成をしているに違いありません。このコンストラクタを Open Call Hierarchy してみると、、、ありました。

RelationalMappingContextのコンストラクタの呼び出し階層

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;
    }
(略)
}

(in GitHub)

なるほどね。@Configuration をつけたクラスを作り、その中に @Bean をつけたメソッドでインスタンスを返せばSpringコンテナの管理下に入るので、それを @Autowired させて使っているということですね。JdbcMappingContextRelationalMappingContext を継承したものなので、ここで生成した JdbcMappingContext のBeanが RelationalMappingContext としてAutowiredされるようです。

AbstractJdbcConfiguration には他にもいくつかのBean定義があり、それらを JdbcRepositoryFactoryBean でAutowiredしているところがありました。そういうお作法でやっているのだと理解すれば、簡単な話ですね。また後で必要なインスタンスがAutowiredされているのを見たら、AbstractJdbcConfiguration に戻ってくればインスタンスの生成を確認できそうです。

たどってたどって長くなりましたが mappingContext についてはこれで解決です。これでRepositoryの作成に必要となるインスタンスは準備できそうです。

2. SQL組み立ての流れを追う

Repositoryクラスのインスタンス生成についての流れは大まかに把握できたので、次は findAll の内部の実装がどうなっているかの確認です。

findAllの実行箇所を追う

再掲になりますが、SimpleDataJdbcfindAll の実装は次のようになっています。

@Override
public Iterable<T> findAll() {
    return entityOperations.findAll(entity.getType());
}

(in GitHub)

この 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);
}

(in GitHub)

ここで accessStrategy.findAll というメソッドを呼び出しています。これの実装は複数のクラスで提供されていますが、最もシンプルそうな DefaultDataAccessStrategy クラスの findAll メソッドを確認します。

@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
    return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType));
}

(in GitHub)

ようやくそれらしいコードがでてきました。

operations.query は、引数で指定されたSQLを実行するもの。JdbcTemplatequery メソッドですね。

sql(domainType).getFindAll() は、全件検索のSQLを組み立てるもの。

getEntityRowMapper(domainType) は、ResultSet をEntityにマッピングするための RowMapper を提供するもの。これも JdbcTemplate を使ったことがあれば馴染みのあるものです。

ということで最も分からないSQLの組み立て部分を追っていきます。

SQLの組み立てを追う

まずは sql メソッドを確認してみます。

private SqlGenerator sql(Class<?> domainType) {
    return sqlGeneratorSource.getSqlGenerator(domainType);
}

(in GitHub)

sql メソッドはEntityに対応した SqlGenerator クラスを返すもののようです。見るからにSQLをジェネレートしそうなクラス名です。

この呼び出し先である SqlGeneratorSource クラスの getSqlGenerator についても確認しておきます。

SqlGenerator getSqlGenerator(Class<?> domainType) {
    return CACHE.computeIfAbsent(domainType,
            t -> new SqlGenerator(context, converter, context.getRequiredPersistentEntity(t), dialect));
}

(in GitHub)

SqlGeneratorインスタンスを作ってキャッシュしておく処理のようですね。とりあえず今のところは無視しましょう。

sql(domainType).getFindAll() の処理に戻って SqlGenerator の実装を確認します。

String getFindAll() {
    return findAllSql.get();
}

(in GitHub)

生成済みである findAllSql を返しているようです。なるほどね、findAllのようなSQLは特にコンテキストで内容が変わるものではないから、使ったものを使い回しているのでしょうね。

ではこの findAllSql を確認しましょう。SqlGenrator クラスのインスタンス変数として定義されています。

private final Lazy<String> findAllSql = Lazy.of(this::createFindAllSql);

(in GitHub)

Lazy ということは遅延ローディングですね。初めて必要になって初めてSQL文を組み立てて、次回以降はそれを使い回す形だと理解できます。

ではここで呼んでいる createFindAllSql を確認しましょう。そろそろ核心に近づいてきた気がします。

private String createFindAllSql() {
    return render(selectBuilder().build());
}

(in GitHub)

private String render(Select select) {
    return this.sqlRenderer.render(select);
}

(in GitHub)

まだ核心ではありませんでした。sqlRendererselectBuilder().build() したものを渡すことで、SQL文を作成しているようです。

ここまではずっと spring-data-jdbc のクラスを追ってきましたが、ここで使われている SqlRenderer クラスや SelectBuilder クラスは spring-data-relational のクラスです。spring-data-relationalspring-data-commons と同じようにSpring Dataを作るための共通ライブラリのようなものですが、特にRDBMSに関連するクラス群を提供しているようです。

そういう立ち位置のため、自作のSpring Dataが spring-data-jdbc に依存することはできませんが、spring-data-relational なら依存して利用することができます。なのであまりコードを読み込まずに利用しても良いのですが、念のため実装を簡単に確認しておきましょう。

SqlRendererrender メソッドは次のようになっています。

@Override
public String render(Select select) {

    SelectStatementVisitor visitor = new SelectStatementVisitor(context);
    select.visit(visitor);

    return visitor.getRenderedPart().toString();
}

(in GitHub)

この内部実装はあまり真面目に追うつもりはありませんが、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();
}

(in GitHub)

めちゃくちゃゴリゴリにSQL文字列を組み立てていますね。どうあれ SqlRender というクラスが、引数で渡された Select を元にSQL文を文字列として組み立てていることが理解できました。この処理を追うのはこれぐらいにして、戻りましょう。

SqlRenderer は誰が作って誰が渡しているのか?

SqlRendererSqlGenerator のコンストラクタで作られています。

SqlGenerator(RelationalMappingContext mappingContext, JdbcConverter converter, RelationalPersistentEntity<?> entity,
        Dialect dialect) {
// (略)
    this.renderContext = new RenderContextFactory(dialect).createRenderContext();
    this.sqlRenderer = SqlRenderer.create(renderContext);
// (略)
}

(in GitHub)

これを見る限り dialectrenderContext さえあればインスタンス生成ができそうです。

SqlGenerator のコンストラクタは、先ほども記載した SqlGeneratorSource クラスの getSqlGenerator メソッドで呼び出しています。

SqlGenerator getSqlGenerator(Class<?> domainType) {
    return CACHE.computeIfAbsent(domainType,
            t -> new SqlGenerator(context, converter, context.getRequiredPersistentEntity(t), dialect));
}

(in GitHub)

ここでコンストラクタ引数として指定している contextdialectSqlGeneratorSource のコンストラクタで指定されています。

public SqlGeneratorSource(RelationalMappingContext context, JdbcConverter converter, Dialect dialect) {
// (略)
    this.context = context;
    this.converter = converter;
    this.dialect = dialect;
}

(in GitHub)

どんどん遡っていきましょう。

SqlGeneratorSourceインスタンスJdbcRepositoryFactoryBean で作っています。

SqlGeneratorSource sqlGeneratorSource = new SqlGeneratorSource(this.mappingContext, this.converter,
        this.dialect);

(in GitHub)

mappingContext は先ほど @Autowired されていたインスタンスですね。そうすると dialect も同様でしょうかね。そう思って JdbcRepositoryFactoryBean のコードを確認すると。

@Autowired
public void setDialect(Dialect dialect) {
    Assert.notNull(dialect, "Dialect must not be null");
    this.dialect = dialect;
}

(in GitHub)

ビンゴ! ありました。ちなみにドラマに出てくるエンジニアはすぐ「ビンゴ!」と言いますが、実際にエンジニアが「ビンゴ」と発言することはありませんね。

@Autowired されているということは、mappingContext と同様に AbstractJdbcConfiguration クラスで定義されていそうです。

@Bean
public Dialect jdbcDialect(NamedParameterJdbcOperations operations) {
    return DialectResolver.getDialect(operations.getJdbcOperations());
}

(in GitHub)

ビンゴ! ありました。またビンゴと言ってしまいました。

この DialectResolverspring-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;
}

(in GitHub)

いや完全にゴリゴリに実装してますね、マジかよ。RDBへのコネクションを使って、そのメタデータからどのDBかを判定しています。昔ながらの方法です。

この辺りを実装するのも面倒ですから、いったんRDBは「H2」固定にするとします。もう少し実装が進んだ時に、きちんと実装することにしましょう。

どうあれこれで、SqlRenderer を生成する流れも把握できました。

Select の組み立て

さて、残すは SqlRenderer に渡す Select の組み立てです。SqlGeneratorselectBuilder の実装を確認しましょう。

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;
}

(in GitHub)

ざっと眺めてみると、何となくやりたいことは分かりますね。戻り値である baseSelect から逆算し、また今回対応しない join も無視して考えると、次のようなコードになりそうです。

return StatementBuilder.select(columnExpressions).from(getTable());

まずは Table を作っている箇所から確認します。

private Table getTable() {
    return sqlContext.getTable();
}

(in GitHub)

さらにこの SqlContext の実装も確認すると、コンストラクタで Table クラスのインスタンスを作成していました。

SqlContext(RelationalPersistentEntity<?> entity) {
    this.entity = entity;
    this.table = Table.create(entity.getQualifiedTableName());
}

(in GitHub)

とりあえず RelationalPersistentEntity さえあれば、問題なくインスタンスを作れるようです。これは既に出自の分かっているオブジェクトです。

続いて、Column についても確認しましょう。columnExpressions は、SELECT文の SELECT 直後に羅列するカラム名の一覧であることは容易に想像がつきます。

getColumn メソッドはjoinやオブジェクトのネストなどを考慮しているため少し長い処理なのですが、シンプルなSELECT ALL処理だけで言うと、次の部分の処理になります。

return sqlContext.getColumn(path);

(in GitHub)

この SqlContext の処理を追ってみます。

Column getColumn(PersistentPropertyPathExtension path) {
    return getTable(path).column(path.getColumnName()).as(path.getColumnAlias());
}

(in GitHub)

Table から PersistentPropertyPathExtension を使って取れるみたいですね。

PersistentPropertyPathExtension は上に書いた selectBuilder の処理の中で生成しています。

   for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext
            .findPersistentPropertyPaths(entity.getType(), p -> true)) {

        PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path);

(in GitHub)

for文が若干分かりづらいですが mappingContextentity さえあれば問題なく PersistentPropertyPathExtension が生成できそうで、特に別途必要なインスタンスはなさそうです。

どうあれ、ここまで見た流れで SelectSqlRenderer の組み合わせができそうです。

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);
    }
}

(in GitHub)

特に NamingStrategy をカスタマイズする必要も今のところはないので、固定にしています。

SqlRepositoryFactoryでRepositoryのコンストラクタに渡す引数を指定

続いて SqlRepositoryFactoryRelationalMappingContext をコンストラクタで受け取るようにして、その 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;
    }
}

(in GitHub)

この辺りも 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());
    }

(in GitHub)

また 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;
}

(in GitHub)

だいぶ端折った実装にしていますがエンティティクラスの情報を元に 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 を使います。

SqlRepositoryFactoryBeanJdbcTemplate をAutowireする

まずは SqlRepositoryFactoryBeanJdbcTemplate@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);
    }
}

(in GitHub)

コンストラクタで受け取って、渡すだけです。

DefaultRepositoryfindAll でクエリを実行する

そして同様に、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;
}

(in GitHub)

後は JdbcTemplate を使ってクエリ発行するだけ、、、なのですが、JdbcTemplate では ResultSet をエンティティに変換するための RowMapper が必要となります。その実装も必要なのですが、ここでは Bootiful SQL Template で作成した、ResultSetRecordマッピングするための RecordMapper をコピペして使います。

RecordMapper のコードはこちらになります。実際にはこれを少しだけ改修しましたが、大きな話ではないので割愛します。

public class RecordMapper<T> implements RowMapper<T> {
    // (割愛)
}

(in GitHub)

そしていよいよ findAll の実装です。最後にクエリを実行する処理を入れます。

@Override
public List<T> findAll() {
// (略)
    String sql = sqlRenderer.render(select);

    return jdbcTemplate.query(sql, new RecordMapper<>(entity.getType()));
}

(in GitHub)

これで実装が完了しました。あとは実行するだけです。

いざ、実行!!

テーブルがないと実行してもコケることが分かりきっているため、サンプルアプリケーション側でテーブルを作成します。

schema.sql を次のような内容で作成します。ぜんぜん emp っぽくないですが。

CREATE TABLE emp
(
  id BIGINT NOT NULL PRIMARY KEY,
  name VARCHAR(64)
);

(in GitHub)

そして data.sql を次のような内容で作成します。

INSERT INTO emp (id, name)
VALUES
 (1, 'Nakamoto')
,(2, 'Kikuchi')
,(3, 'Mizuno')
,(4, 'Sayashi')
,(5, 'Fujihira')
,(6, 'Okazaki')
;

(in GitHub)

羅列された名前についてはご意見無用です。

これで準備は整いました。起動して実行してみましょう!

 % 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の基底クラスである RepositoryFactorySupportgetTargetRepository メソッドには可変長引数でRepositoryのコンストラクタに渡す引数を指定することができる
  • spring-data-relationalRelationalMappingContextRelationalPersistentEntity クラスがエンティティとテーブルをマッピングするための情報を作成する
  • spring-data-relationalSqlRenderer StatementBuilder Select Table Column クラスなどを利用してSQLの文字列を作成することができる

正直、ほとんど実装してなくて、時間の大半は spring-data-jdbc のコードリーディングに費やしたという感じですが、何にせよ無事に findAll が動作するところまで動きました。

とりあえずここまでで動いたものをGitHubに置いておいたので、ちょっと読んでみたいという人はどうぞ。実質的には、ほんの数十行しか実装していません。 github.com

この後 DefaultRepository の他のメソッドの実装を充実させていく分には、今回の延長でできそうなイメージが沸いてきました。

それよりもまだイメージの沸いていない任意のSQLの発行について、次回は挑戦したいと思います。