💨

DomaのCriteria APIやDaoでUPSERT

2024/03/21に公開

前提

以下バージョンから、DomaのCriteria API,DaoでUPSERTができるようになりました。

version リリース日
org.seasar.doma:doma-kotlin 2.57.0以降 2024-03-20
org.seasar.doma:doma-core 2.57.0以降 2024-03-20

MySQLは5.7が前提ですが、MySQL8でもUPSERTができるようにする方法も記載しています。

UPSERTのSQL

基本、以下Komapperの記事と同じですが、MySQLだけDomaで対応しているversionが5なので、MySQLだけ異なります。

[Kotlin] KomapperのUPSERT

MySQL(MySQL5)の場合のSQL
insert into DEPARTMENT (DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION)
values (1, 60, 'DEVELOPMENT', 'KYOTO', 1)
on duplicate key update DEPARTMENT_NO   = values(DEPARTMENT_NO),
                        DEPARTMENT_NAME = values(DEPARTMENT_NAME),
                        LOCATION        = values(LOCATION),
                        VERSION         = values(VERSION)

使い方

Komapperと似たようなインターフェイスとなります。
insert時に重複キーがある場合、updateするのかignoreなのかを指定するというものです。
Daoとentityqlでの動作は、 domaのentityの定義で、@Idを付与したものを重複キーとして扱います。

entityqlでは以下のように記載します。※Kotlinで書いています

entityql.insert(d, list).onDuplicateKeyUpdate().execute()
entityql.insert(d, department).onDuplicateKeyIgnore().execute()

@Daoでは以下のように記載します。※Kotlinで書いています

@Dao
interface DepartmentDao {
    @Insert(duplicateKeyType = DuplicateKeyType.UPDATE)
    fun insertOnDuplicateKeyUpdate(entity: Department): Int

    @Insert(duplicateKeyType = DuplicateKeyType.IGNORE)
    fun insertOnDuplicateKeyIgnore(entity: Department): Int

    @BatchInsert(duplicateKeyType = DuplicateKeyType.UPDATE)
    fun insertOnDuplicateKeyUpdate(entities: List<Department>): Array<Int>

    @BatchInsert(duplicateKeyType = DuplicateKeyType.IGNORE)
    fun insertOnDuplicateKeyIgnore(entities: List<Department>): Array<Int>
}

nativeSqlでは、重複のキーをkeysメソッドで、重複した場合の更新値をsetメソッドで任意に指定できます。
また、nativeSqlであっても、keysメソッドやsetメソッドでの指定を省略できます。省略した場合、@Idを付与したプロパティが重複キーとして扱われ、@Idを付与していないプロパティが更新値として扱われます。
set句で使えるexcludedメソッドは、insert句のvaluesで指定した値を指定できます。

nativeSql
    .insert(d)
    .values {
        it.value(d.departmentId, 1)
        it.value(d.departmentNo, 60)
        it.value(d.departmentName, "DEVELOPMENT")
        it.value(d.location, "KYOTO")
        it.value(d.version, 2)
    }
    .onDuplicateKeyUpdate()
    .keys(d.departmentId)
    .set {
        it.value(d.departmentNo, 60)
        it.value(d.departmentName, "DEVELOPMENT")
        it.value(d.location, it.excluded(e.location))
        it.value(d.version, 3)
    }
    .execute()

nativeSql
    .insert(d)
    .values {
        it.value(d.departmentId, 1)
        it.value(d.departmentNo, 60)
        it.value(d.departmentName, "DEVELOPMENT")
        it.value(d.location, "KYOTO")
        it.value(d.version, 2)
    }
    .onDuplicateKeyIgnore()
        .keys(d.departmentId)
        .execute()

MySQL8でのUPSERTをしたい場合

org.seasar.doma.jdbc.dialect.MysqlDialect#getUpsertAssemblerをoverrideして、以下実装を返すようにすれば、MySQL8でもUPSERTができるようになります。

Mysql8でのUpsertAssembler
import java.util.List;
import org.seasar.doma.internal.jdbc.sql.PreparedSqlBuilder;
import org.seasar.doma.jdbc.InParameter;
import org.seasar.doma.jdbc.criteria.tuple.Tuple2;
import org.seasar.doma.jdbc.entity.EntityPropertyType;
import org.seasar.doma.jdbc.entity.EntityType;
import org.seasar.doma.jdbc.query.DuplicateKeyType;
import org.seasar.doma.jdbc.query.UpsertAssembler;
import org.seasar.doma.jdbc.query.UpsertAssemblerContext;
import org.seasar.doma.jdbc.query.UpsertAssemblerSupport;
import org.seasar.doma.jdbc.query.UpsertSetValue;

public class Mysql8UpsertAssembler implements UpsertAssembler {
  private final PreparedSqlBuilder buf;
  private final EntityType<?> entityType;
  private final DuplicateKeyType duplicateKeyType;
  private final UpsertAssemblerSupport upsertAssemblerSupport;
  private final List<Tuple2<EntityPropertyType<?, ?>, InParameter<?>>> insertValues;
  private final List<Tuple2<EntityPropertyType<?, ?>, UpsertSetValue>> setValues;
  private final UpsertSetValue.Visitor upsertSetValueVisitor = new UpsertSetValueVisitor();

  public MysqlUpsertAssembler(UpsertAssemblerContext context) {
    this.buf = context.buf;
    this.entityType = context.entityType;
    this.duplicateKeyType = context.duplicateKeyType;
    this.insertValues = context.insertValues;
    this.setValues = context.setValues;
    this.upsertAssemblerSupport = new UpsertAssemblerSupport(context.naming, context.dialect);
  }

  @Override
  public void assemble() {
    buf.appendSql("insert");
    if (duplicateKeyType == DuplicateKeyType.IGNORE) {
      buf.appendSql(" ignore");
    }
    buf.appendSql(" into ");
    tableNameOnly(entityType);
    buf.appendSql(" (");
    for (Tuple2<EntityPropertyType<?, ?>, InParameter<?>> insertValue : insertValues) {
      column(insertValue.component1());
      buf.appendSql(", ");
    }
    buf.cutBackSql(2);
    buf.appendSql(") values (");
    for (Tuple2<EntityPropertyType<?, ?>, InParameter<?>> insertValue : insertValues) {
      buf.appendParameter(insertValue.component2());
      buf.appendSql(", ");
    }
    buf.cutBackSql(2);
    buf.appendSql(") as ");
    excludeAlias();
    if (duplicateKeyType == DuplicateKeyType.UPDATE) {
      buf.appendSql(" on duplicate key update ");
      for (Tuple2<EntityPropertyType<?, ?>, UpsertSetValue> setValue : setValues) {
        column(setValue.component1());
        buf.appendSql(" = ");
        setValue.component2().accept(upsertSetValueVisitor);
        buf.appendSql(", ");
      }
      buf.cutBackSql(2);
    }
  }

  private void tableNameOnly(EntityType<?> entityType) {
    String sql =
        this.upsertAssemblerSupport.targetTable(
            entityType, UpsertAssemblerSupport.TableNameType.NAME);
    buf.appendSql(sql);
  }

  private void excludeAlias() {
    String sql = this.upsertAssemblerSupport.excludeAlias();
    buf.appendSql(sql);
  }

  private void column(EntityPropertyType<?, ?> propertyType) {
    String sql = this.upsertAssemblerSupport.prop(propertyType);
    buf.appendSql(sql);
  }

  class UpsertSetValueVisitor implements UpsertSetValue.Visitor {
    @Override
    public void visit(UpsertSetValue.Param param) {
      buf.appendParameter(param.inParameter);
    }

    @Override
    public void visit(UpsertSetValue.Prop prop) {
      String sql =
              upsertAssemblerSupport.excludeProp(
                      prop.propertyType, UpsertAssemblerSupport.ColumnNameType.NAME_ALIAS);
      buf.appendSql(sql);
    }
  }
}

おわりに

各データベース毎の違いについては、Komapperを参考にさせてもらったので、苦労することなく実装できました…。感謝。

GitHubで編集を提案

Discussion