spring-framework icon indicating copy to clipboard operation
spring-framework copied to clipboard

Add `updateReturning`, `batchUpdateReturning` and `batchUpdateReturningStream` methods to `NamedParameterJdbcTemplate`

Open StephenFlavin opened this issue 2 years ago • 0 comments

Many databases now support UPDATE/INSERT/DELETE ... RETURNING * which enables a single database request to apply and return in a single database call, this allows the caller to see the values for generated fields such as ID's, default values and potentially data which is populated via triggers BEFORE INSERT OR UPDATE ... or in my case I have a batch insert that uses ON CONFLICT DO NOTHING RETURNING * which allows me to not fail the entire batch when some rows conflict and manage the conflicts manually.

Currently I see a lot of implementations which use an insert and then a select separately where the implementation needs to take care to use @Transactional.

The functionality of updateReturning can be achieved using the query methods however I would argue that this is counter intuitive. Both batchUpdateReturning and batchUpdateReturningStream are a little more complicated to workaround with the currently available query methods but they are possible (see: this Stack Overflow answer) but the solution has some drawbacks.

Example of the workaround I've been using

    public Set<ExternalId> markSeen(Collection<ExternalId> externalIds) {
        List<Object[]> values = externalIds.stream()
                // Object[] must be in order of the columns defined for the insert
                .map(externalId -> new Object[] {externalId.getStringId(), externalId.getDomain()})
                .toList();

        // insert batch returning everything that didn't conflict
        Set<ExternalId> unseenIds = namedParameterJdbcTemplate.query("""
            insert into seen_documents (external_id, domain)
            values :idAndDomain
            on conflict do nothing
            returning external_id, domain
            """,
                new MapSqlParameterSource("idAndDomain", values),
                resultExtractor);

        // return values that didn't get inserted
        return externalIds.stream()
                .filter(not(unseenIds::contains))
                .collect(Collectors.toUnmodifiableSet());
    }

I'm happy to add the functionality to the APIs if others think this is worth doing.

StephenFlavin avatar Nov 04 '22 23:11 StephenFlavin