JOOQライブラリを使用してPostgreSQLでUPSERTを実行しようとしています。
これを行うために、私は現在、jOOQで次のSQLステートメントを実装しようとしています: https://stackoverflow.com/a/6527838
これまでのところ、私のコードは次のようになります。
public class UpsertExecutor {
private static final Logger logger = LoggerFactory.getLogger(UpsertExecutor.class);
private final JOOQContextProvider jooqProvider;
@Inject
public UpsertExecutor(JOOQContextProvider jooqProvider) {
Preconditions.checkNotNull(jooqProvider);
this.jooqProvider = jooqProvider;
}
@Transactional
public <T extends Record> void executeUpsert(Table<T> table, Condition condition, Map<? extends Field<?>, ?> recordValues) {
/*
* All of this is for trying to do an UPSERT on PostgreSQL. See:
* https://stackoverflow.com/a/6527838
*/
SelectConditionStep<Record1<Integer>> notExistsSelect = jooqProvider.getDSLContext().selectOne().from(table).where(condition);
SelectConditionStep<Record> insertIntoSelect = jooqProvider.getDSLContext().select(recordValues).whereNotExists(notExistsSelect);
try {
int[] result = jooqProvider.getDSLContext().batch(
jooqProvider.getDSLContext().update(table).set(recordValues).where(condition),
jooqProvider.getDSLContext().insertInto(table).select(insertIntoSelect)
).execute();
long rowsAffectedTotal = 0;
for (int rowsAffected : result) {
rowsAffectedTotal += rowsAffected;
}
if (rowsAffectedTotal != 1) {
throw new RuntimeException("Upsert must only affect 1 row. Affected: " + rowsAffectedTotal + ". Table: " + table + ". Condition: " + condition);
}
} catch (DataAccessException e) {
if (e.getCause() instanceof BatchUpdateException) {
BatchUpdateException cause = (BatchUpdateException)e.getCause();
logger.error("Batch update error in upsert.", cause.getNextException());
}
throw e;
}
}
}
ただし、select()は値のマップをサポートしていないため、このコードはコンパイルされません。
SelectConditionStep<Record> insertIntoSelect = jooqProvider.getDSLContext().select(recordValues).whereNotExists(notExistsSelect);
Select_)に次のような事前定義された値のセットを提供するにはどうすればよいですか:SELECT 3, 'C', 'Z'
?
私はなんとかコードを機能させることができました。ここに完全なクラスがあります:
public class UpsertExecutor {
private static final Logger logger = LoggerFactory.getLogger(UpsertExecutor.class);
private final JOOQContextProvider jooqProvider;
@Inject
public UpsertExecutor(JOOQContextProvider jooqProvider) {
Preconditions.checkNotNull(jooqProvider);
this.jooqProvider = jooqProvider;
}
@Transactional
public <T extends Record> void executeUpsert(Table<T> table, Condition condition, List<FieldValue<Field<?>, ?>> recordValues) {
/*
* All of this is for trying to do an UPSERT on PostgreSQL. See:
* https://stackoverflow.com/a/6527838
*/
Map<Field<?>, Object> recordValuesMap = new HashMap<Field<?>, Object>();
for (FieldValue<Field<?>, ?> entry : recordValues) {
recordValuesMap.put(entry.getFieldName(), entry.getFieldValue());
}
List<Param<?>> params = new LinkedList<Param<?>>();
for (FieldValue<Field<?>, ?> entry : recordValues) {
params.add(val(entry.getFieldValue()));
}
List<Field<?>> fields = new LinkedList<Field<?>>();
for (FieldValue<Field<?>, ?> entry : recordValues) {
fields.add(entry.getFieldName());
}
SelectConditionStep<Record1<Integer>> notExistsSelect = jooqProvider.getDSLContext().selectOne().from(table).where(condition);
SelectConditionStep<Record> insertIntoSelect = jooqProvider.getDSLContext().select(params).whereNotExists(notExistsSelect);
try {
int[] result = jooqProvider.getDSLContext().batch(
jooqProvider.getDSLContext().update(table).set(recordValuesMap).where(condition),
jooqProvider.getDSLContext().insertInto(table, fields).select(insertIntoSelect)
).execute();
long rowsAffectedTotal = 0;
for (int rowsAffected : result) {
rowsAffectedTotal += rowsAffected;
}
if (rowsAffectedTotal != 1) {
throw new RuntimeException("Upsert must only affect 1 row. Affected: " + rowsAffectedTotal + ". Table: " + table + ". Condition: " + condition);
}
} catch (DataAccessException e) {
if (e.getCause() instanceof BatchUpdateException) {
BatchUpdateException cause = (BatchUpdateException)e.getCause();
logger.error("Batch update error in upsert.", cause.getNextException());
}
throw e;
}
}
}
ただし、List<FieldValue<Field<?>, ?>> recordValues
パラメーターではあまりクリーンに感じられません。これを行う方法についてのより良いアイデアはありますか?
jOOQ 3.7+は、PostgreSQL9.5のON CONFLICT
句をサポートしています。
完全なPostgreSQLベンダー固有の構文はまだサポートされていませんが、MySQLまたはH2構文を使用できます。どちらも、PostgreSQLのON CONFLICT
を使用してエミュレートできます。
INSERT .. ON DUPLICATE KEY UPDATE
:DSL.using(configuration)
.insertInto(TABLE)
.columns(ID, A, B)
.values(1, "a", "b")
.onDuplicateKeyUpdate()
.set(A, "a")
.set(B, "b")
.execute();
MERGE INTO ..
DSL.using(configuration)
.mergeInto(TABLE, A, B, C)
.values(1, "a", "b")
.execute();
次に、上記のLucasのUpdatableRecordオブジェクトのソリューションから派生したupsertユーティリティメソッドを示します。
public static int upsert(final DSLContext dslContext, final UpdatableRecord record) {
return dslContext.insertInto(record.getTable())
.set(record)
.onDuplicateKeyUpdate()
.set(record)
.execute();
}
目的を達成するための少し複雑な方法のようです。単純な保存機能を使用しないのはなぜですか?アップサート関数の作成方法は postgresqlマニュアル に記載されており、Javaコードから呼び出すだけです。
JOOQ 3.11、Kotlin、およびPostgreSQL DSLでの@ ud3shコメントに触発されました
これは、upsert
オブジェクトでUpdatableRecord
を直接呼び出す拡張関数です
import org.jooq.UpdatableRecord
internal fun UpdatableRecord<*>.upsert(): Int {
if(this.configuration() == null) {
throw NullPointerException("Attach configuration to record before calling upsert")
}
return this.configuration().dsl().insertInto(this.getTable()).set(this).onConflict().doUpdate().set(this).execute()
}