jpa
Querydsl
spring-boot

Spring Data JPA を適用したアプリケーションにて SQLQueryFactory にて DML を実行する方法について

概要

表題のとおり、Spring Data JPA を適用したアプリケーションにて、SQLQueryFactory (ネイティブの SQL を生成するための DSL クラス) にて DML を実行しようとした結果、SQLQueryFactory#update メソッドの引数が RelationalPath<?> インターフェースであったため、アノテーションプロセッサーにて生成されたクラスを引数に渡すことができませんでした。
これは、Spring Data JPA を適用してアノテーションプロセッサーを実行した場合、@Entity などのアノテーションがついているクラスから生成される QueryDSL のクラスは、下記のように EntityPathBase を継承しているためです。

QMusicFestival.java
// このクラスは、MusicFestival エンティティから QueryDSL にて自動生成されるクラスです。

public class QMusicFestival extends EntityPathBase<MusicFestival> {
}

SQLQueryFactory#select の引数は、EntityPath<?> インターフェースを許容していますが、SQLQueryFactory#update メソッドはなんと、EntityPath<?> を許容していないようでした。

AbstractSQLQueryFactory.java
public abstract class AbstractSQLQueryFactory<Q extends SQLCommonQuery<?>> implements SQLCommonQueryFactory<Q,
    SQLDeleteClause, SQLUpdateClause, SQLInsertClause, SQLMergeClause> {

    @Override
    public final SQLUpdateClause update(RelationalPath<?> path) {
        return new SQLUpdateClause(connection, configuration, path);
    }
}

回避策

下記のように、EntityPath<?> のクラスから、RelationalPathBase<?> のクラスを生成することで回避できます。

MusicFestivalRepository.java
@Repository
@RequiredArgsConstructor
@Slf4j
@Transactional
public class MusicFestivalRepository {

    private final SQLQueryFactory sqlQueryFactory;

    private RelationalPath<MusicFestival> musicFestivalRelationalPath = new RelationalPathBase<>(
            MusicFestival.class, QMusicFestival.musicFestival.getMetadata(), "", "musicFestival"
    );

    public long updateFestival() {
        SQLUpdateClause updateClause = sqlQueryFactory
                .update(musicFestivalRelationalPath) // relationalPath でないとコンパイルエラー
                .set(QMusicFestival.musicFestival.eventDate, LocalDate.now())
                .where(QMusicFestival.musicFestival.festivalId.eq(1)).addBatch();

        updateClause
                .set(QMusicFestival.musicFestival.place, "川中島")
                .where(QMusicFestival.musicFestival.festivalId.eq(Expressions.asNumber(1))).addBatch();

        return updateClause.execute();

    }
}

RelationalPathBase<?> の引数は、下記のように定義されているので、EntityPath を元に、

  • QueryDSL にてマップするクラス
  • EntityPath のメタデータ?
  • DBのスキーマ名 (今回はなし)
  • テーブル名

を渡します。

RelationalPathBase.java
public class RelationalPathBase<T> extends BeanPath<T> implements RelationalPath<T> {

    public RelationalPathBase(Class<? extends T> type, PathMetadata metadata, String schema,
            String table) {
        super(type, metadata);
        this.schema = schema;
        this.table = table;
        this.schemaAndTable = new SchemaAndTable(schema, table);
    }
}

情報がどこにも書かれていないので、トラブルシュートに時間がかかりました。
というか、SQLQueryFactory#select と同様に、EntityPath との互換性を用意して欲しいものでした。
JPA との混在の要件は少ないからか、やる気はあまりないのかもしれません。

補足

通常であれば、JPQL を生成するための DSL である JPAQueryFactory (JPQL を生成するための DSL クラス) を使用すれば、JPAQueryFactory#update の引数は、EntityPath<?> インターフェースであるため、上記のような問題は発生しません。しかしながら、下記の要因により、止むをえず、SQLQueryFactory#update メソッドを使用することになったため、問題が顕在化した次第です。

要因 1 : レガシーシステムの DB にアクセスした

今回は、要件上、止むを得ずレガシーシステムに対して、DB アクセスを実施しておりました。
この DB は、アプリケーション上の DB とは全く異なる DB であるため、レガシーシステムのエンティティに対して、@Entity のアノテーションを設定すると、後述の spring.jpa.hibernate.ddl-auto の設定により、新規アプリケーションにレガシーシステムのエンティティが存在するかのチェックを実施する動作になります。
この動作は全く不要な動作であるため、レガシーシステムのエンティティには、@MappedSuperclass のアノテーションを設定しました。
@MappedSuperclass@Entity が設定されたクラスが継承するためのクラスに設定するアノテーションであり、Spring Data JPA において、Entity としては見なされず、QueryDSL のソース自動生成の対象にのみなります。
これにより、Spring Data JPA においてエンティティとして認知されず余計なバリデーションがかからなくなる一方、それゆえ、レガシーシステムのエンティティ (@MappedSuperclass のアノテーションが設定されたクラス) は、JPQL、JpaRepository など、JPA 固有の機能が使用できない、というトレードオフになりました。

要因 2 : spring.jpa.hibernate.ddl-auto を validate に設定した

Spring Boot の設定ファイルである、application.properties には、spring.jpa.hibernate.ddl-auto キーが存在します。
このキーに validate を設定すると、spring.datasource.url に指定された DB に @Entity が設定されたテーブルが存在するかのチェックを行います。
上述のとおり、レガシーシステムのエンティティに @Entity をつけると、このチェックが新規アプリケーションの DB に走るのでアプリケーション起動時にエラーとなります。
none を設定すると何も行いませんが、アプリケーションが想定どおりのテーブルが存在していることをアプリケーション起動時に確認するために、validate の設定を実施いたしました。