UPSERT в PostgreSQL с использованием jOOQ

Я пытаюсь выполнить UPSERT в PostgreSQL, используя библиотеку jOOQ.

Для этого я сейчас пытаюсь реализовать в 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'?

Обновление 1

Мне удалось заставить код заработать. Вот полный класс:

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 он не выглядит очень чистым. Есть идеи получше, как это сделать?


person uldall    schedule 06.04.2014    source источник
comment
Кажется, я ищу статический метод val (): jooq.org/javadoc/3.3.x/org/jooq/impl/   -  person uldall    schedule 06.04.2014
comment
Вы знаете, что вам придется использовать SERIALIZABLE транзакцию в цикле повтора или заблокировать таблицу, чтобы это работало надежно, верно?   -  person Craig Ringer    schedule 06.04.2014
comment
@CraigRinger Да, я в курсе. Вы можете порекомендовать лучший подход? Меня очень интересуют лучшие решения этой проблемы.   -  person uldall    schedule 06.04.2014
comment
sql select 3,'C','Z'; очень похож на sql values (3,'C','Z');, поэтому да, val() звучит как правильный метод.   -  person Jasen    schedule 12.02.2015
comment
К вашему сведению, PosgreSQL 9.5 будет иметь UPSERT. wiki.postgresql.org/wiki/UPSERT   -  person Roger    schedule 15.05.2015


Ответы (4)


jOOQ 3.7+ поддерживает предложение ON CONFLICT PostgreSQL 9.5:

Полный синтаксис, зависящий от поставщика PostgreSQL, еще не поддерживается, но вы можете использовать синтаксис MySQL или H2, которые можно эмулировать с помощью PostgreSQL ON CONFLICT:

MySQL 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();

H2 MERGE INTO ..

DSL.using(configuration)
   .mergeInto(TABLE, A, B, C)
   .values(1, "a", "b")
   .execute();
person Lukas Eder    schedule 29.01.2016
comment
Утилита, полученная из решения Лукаса для объектов TableRecord: public static int upsert(final DSLContext dslContext, final UpdatableRecord record) { return dslContext.insertInto(record.getTable()).set(record).onDuplicateKeyUpdate().set(record).execute(); } - person ud3sh; 21.09.2016
comment
@ ud3sh: Спасибо, что упомянули. Не стесняйтесь дать полный ответ. Я уверен, что это может быть полезно другим! - person Lukas Eder; 22.09.2016

Вот служебный метод upsert, полученный из решения Лукаса выше для объектов UpdatableRecord:

public static int upsert(final DSLContext dslContext, final UpdatableRecord record) {
    return dslContext.insertInto(record.getTable())
                     .set(record)
                     .onDuplicateKeyUpdate()
                     .set(record)
                     .execute();
}
person ud3sh    schedule 23.09.2016

Кажется немного сложным способом достижения цели. Почему бы не использовать простую хранимую функцию? как создать функцию upsert описано в руководство по postgresql, а затем просто вызовите его из своего кода Java.

person e4c5    schedule 13.08.2015
comment
+1. Единственный правильный способ реализовать upsert сейчас (до выпуска 9.5) - использовать сохраненную функцию с циклом, как написано в данной ссылке. Дополнительная информация находится здесь depesz.com/2012/06 / 10 / почему-это-апсерт-так сложно? - person alexius; 13.08.2015

На основе комментария @ ud3sh с JOOQ 3.11, Kotlin и PostgreSQL DSL

Это функция расширения для вызова 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()
}
person frno    schedule 05.06.2019