46

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?

5
  • It seems the val() static method might be what I'm looking for: jooq.org/javadoc/3.3.x/org/jooq/impl/… Commented Apr 6, 2014 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? Commented Apr 6, 2014 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. Commented Apr 6, 2014 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. Commented Feb 12, 2015 at 3:34
  • 2
    FYI PosgreSQL 9.5 will have UPSERT. wiki.postgresql.org/wiki/UPSERT Commented May 15, 2015 at 10:46

4 Answers 4

27

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();
Sign up to request clarification or add additional context in comments.

2 Comments

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: Hey, thanks for mentioning. Feel free to provide this as a full answer. I'm sure it might be useful for others!
27

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

2 Comments

Doesn't work with PostrgreSQL (or actually H2 in PostgreSQL mode)
Works like a charm with PostgreSQL 17. @pstobiecki maybe you could try using testcontainers instead of H2, if you have that problem only with your tests.
6

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()
}

1 Comment

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.

1 Comment

+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 depesz.com/2012/06/10/why-is-upsert-so-complicated

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.