43

I am trying to perform an UPSERT in PostgreSQL using the jOOQ library.

For doing this I am currently trying to implement the following SQL statement in jOOQ: https://stackoverflow.com/a/6527838

My code looks like this so far:

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

This code does however not compile, since select() doesn't support a map of values:

SelectConditionStep<Record> insertIntoSelect = jooqProvider.getDSLContext().select(recordValues).whereNotExists(notExistsSelect);

The Question

How do I provide select() with a set of predefined values like this: SELECT 3, 'C', 'Z'?

Update 1

I managed to get the code working. Here is the complete class:

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

It does however not feel very clean with the List<FieldValue<Field<?>, ?>> recordValues parameter. Any better ideas on how to do this?

Community
  • 1
  • 1
uldall
  • 2,458
  • 1
  • 17
  • 31
  • It seems the val() static method might be what I'm looking for: http://www.jooq.org/javadoc/3.3.x/org/jooq/impl/DSL.html#val%28java.lang.Object,%20java.lang.Class%29 – uldall Apr 06 '14 at 10:05
  • 1
    You know you'll have to use `SERIALIZABLE` transactions in a retry loop, or lock the table, for that to work reliably, right? – Craig Ringer Apr 06 '14 at 10:40
  • @CraigRinger Yes, I am aware of that. Can you recommend a better approach? I am very interested in better solutions to this problem. – uldall Apr 06 '14 at 10:51
  • sql `select 3,'C','Z';` is much the same as sql `values (3,'C','Z');` , so yes `val()` sounds like the right method. – Jasen Feb 12 '15 at 03:34
  • 2
    FYI PosgreSQL 9.5 will have UPSERT. https://wiki.postgresql.org/wiki/UPSERT – Roger May 15 '15 at 10:46

4 Answers4

26

jOOQ 3.7+ supports PostgreSQL 9.5's ON CONFLICT clause:

The full PostgreSQL vendor-specific syntax is not yet supported, but you can use the MySQL or H2 syntax, which can both be emulated using PostgreSQL's 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();
Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • 2
    Utility derived from Lucas' solution for TableRecord objects: ```public static int upsert(final DSLContext dslContext, final UpdatableRecord record) { return dslContext.insertInto(record.getTable()).set(record).onDuplicateKeyUpdate().set(record).execute(); }``` – ud3sh Sep 21 '16 at 14:41
  • 1
    @ud3sh: Hey, thanks for mentioning. Feel free to provide this as a full answer. I'm sure it might be useful for others! – Lukas Eder Sep 22 '16 at 08:12
24

Here is an upsert utility method derived from Lucas' solution above for UpdatableRecord objects:

public static int upsert(final DSLContext dslContext, final UpdatableRecord record) {
    return dslContext.insertInto(record.getTable())
                     .set(record)
                     .onDuplicateKeyUpdate()
                     .set(record)
                     .execute();
}
Johannes Barop
  • 7,323
  • 2
  • 25
  • 33
ud3sh
  • 1,249
  • 10
  • 13
5

Inspired by @ud3sh comment with JOOQ 3.11, Kotlin, and the PostgreSQL DSL

This is an extension function to call upsert directly on the UpdatableRecord object

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()
}
frno
  • 2,641
  • 2
  • 18
  • 19
4

Seems a bit of a complicated way of achieving the objective. Why not use a simple stored fucntion? how to create an upsert function is described in the postgresql manual then just call it from your java code.

e4c5
  • 52,766
  • 11
  • 101
  • 134
  • 1
    +1. The only correct way to implement upsert now (until 9.5 released) is to use stored function with cycle as written in given link. More information is here http://www.depesz.com/2012/06/10/why-is-upsert-so-complicated/ – alexius Aug 13 '15 at 04:09