Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
import io.micronaut.data.runtime.operations.internal.query.BindableParametersStoredQuery;
import io.micronaut.data.runtime.operations.internal.sql.AbstractSqlRepositoryOperations;
import io.micronaut.data.runtime.operations.internal.sql.DefaultSqlPreparedQuery;
import io.micronaut.data.runtime.operations.internal.sql.SqlBatchSupport;
import io.micronaut.data.runtime.operations.internal.sql.SqlJsonColumnMapperProvider;
import io.micronaut.data.runtime.operations.internal.sql.SqlPreparedQuery;
import io.micronaut.data.runtime.operations.internal.sql.SqlStoredQuery;
Expand All @@ -114,6 +115,7 @@
import javax.sql.DataSource;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
Expand Down Expand Up @@ -850,7 +852,8 @@
final SqlStoredQuery<T, ?> storedQuery = getSqlStoredQuery(operation.getStoredQuery());
final RuntimePersistentEntity<T> persistentEntity = storedQuery.getPersistentEntity();
JdbcOperationContext ctx = createContext(operation, connection, storedQuery);
if (!isSupportsBatchInsert(persistentEntity, storedQuery)) {
boolean requiresGeneratedKeys = SqlBatchSupport.requiresBatchGeneratedKeys(persistentEntity, operation);
if (!isSupportsBatchInsert(ctx, persistentEntity, storedQuery, requiresGeneratedKeys)) {
return operation.split().stream()
.map(persistOp -> {
JdbcEntityOperations<T> op = new JdbcEntityOperations<>(ctx, storedQuery, persistentEntity, persistOp.getEntity(), true);
Expand All @@ -859,7 +862,7 @@
})
.toList();
} else {
JdbcEntitiesOperations<T> op = new JdbcEntitiesOperations<>(ctx, persistentEntity, operation, storedQuery, true);
JdbcEntitiesOperations<T> op = new JdbcEntitiesOperations<>(ctx, persistentEntity, operation, storedQuery, true, requiresGeneratedKeys);
op.persist();
return op.getEntities();
}
Expand Down Expand Up @@ -1155,7 +1158,50 @@

@Override
public boolean isSupportsBatchInsert(JdbcOperationContext jdbcOperationContext, RuntimePersistentEntity<?> persistentEntity) {
return isSupportsBatchInsert(persistentEntity, jdbcOperationContext.dialect);
boolean requiresGeneratedKeys = persistentEntity.hasIdentity() && persistentEntity.getIdentity().isGenerated();
return isSupportsBatchInsert(jdbcOperationContext, persistentEntity, requiresGeneratedKeys);
}

private boolean isSupportsBatchInsert(JdbcOperationContext ctx,
RuntimePersistentEntity<?> persistentEntity,
SqlStoredQuery<?, ?> storedQuery,
boolean requiresGeneratedKeys) {
if (storedQuery.getOperationType() == StoredQuery.OperationType.INSERT_RETURNING) {
return false;
}
return isSupportsBatchInsert(ctx, persistentEntity, requiresGeneratedKeys);
}

private boolean isSupportsBatchInsert(JdbcOperationContext ctx,
Comment thread
radovanradic marked this conversation as resolved.
RuntimePersistentEntity<?> persistentEntity,
boolean requiresGeneratedKeys) {
// JDBC metadata is only needed for the MySQL dialect, where Micronaut Data must
// distinguish MySQL from MariaDB and account for their generated-key batch behavior.
if (ctx.dialect != Dialect.MYSQL) {
return SqlBatchSupport.isSupportsBatchInsert(persistentEntity, ctx.dialect);
}
try {
DatabaseMetaData metaData = ctx.connection.getMetaData();
return SqlBatchSupport.isSupportsJdbcBatchInsert(
persistentEntity,
ctx.dialect,
metaData.getDatabaseProductName(),
metaData.getDriverName(),
metaData.supportsBatchUpdates(),
metaData.supportsGetGeneratedKeys(),
requiresGeneratedKeys
);
} catch (SQLException ignored) {

Check warning on line 1194 in data-jdbc/src/main/java/io/micronaut/data/jdbc/operations/DefaultJdbcRepositoryOperations.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "ignored" with an unnamed pattern.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-data&issues=AZ6M4CVVUDyJwRZkyKU0&open=AZ6M4CVVUDyJwRZkyKU0&pullRequest=3896
return SqlBatchSupport.isSupportsJdbcBatchInsert(
persistentEntity,
ctx.dialect,
null,
null,
null,
null,
requiresGeneratedKeys
);
}
Comment thread
radovanradic marked this conversation as resolved.
}

@SuppressWarnings({"rawtypes", "unchecked"})
Expand Down Expand Up @@ -1420,18 +1466,29 @@
private final class JdbcEntitiesOperations<T> extends AbstractSyncEntitiesOperations<JdbcOperationContext, T, SQLException> {

private final SqlStoredQuery<T, ?> storedQuery;
private final boolean requiresGeneratedKeys;
private int rowsUpdated;

private JdbcEntitiesOperations(JdbcOperationContext ctx, RuntimePersistentEntity<T> persistentEntity, Iterable<T> entities, SqlStoredQuery<T, ?> storedQuery) {
this(ctx, persistentEntity, entities, storedQuery, false);
}

private JdbcEntitiesOperations(JdbcOperationContext ctx, RuntimePersistentEntity<T> persistentEntity, Iterable<T> entities, SqlStoredQuery<T, ?> storedQuery, boolean insert) {
this(ctx, persistentEntity, entities, storedQuery, insert, insert && persistentEntity.hasIdentity() && persistentEntity.getIdentity().isGenerated());
}

private JdbcEntitiesOperations(JdbcOperationContext ctx,
RuntimePersistentEntity<T> persistentEntity,
Iterable<T> entities,
SqlStoredQuery<T, ?> storedQuery,
boolean insert,
boolean requiresGeneratedKeys) {
super(ctx,
DefaultJdbcRepositoryOperations.this.cascadeOperations,
DefaultJdbcRepositoryOperations.this.conversionService,
entityEventRegistry, persistentEntity, entities, insert);
this.storedQuery = storedQuery;
this.requiresGeneratedKeys = requiresGeneratedKeys;
}

@Override
Expand All @@ -1447,7 +1504,7 @@
private PreparedStatement prepare(Connection connection) throws SQLException {
if (insert) {
Dialect dialect = storedQuery.getDialect();
if (hasGeneratedId && (dialect == Dialect.ORACLE || dialect == Dialect.SQL_SERVER)) {
if (requiresGeneratedKeys && (dialect == Dialect.ORACLE || dialect == Dialect.SQL_SERVER)) {
if (isJsonEntityGeneratedId(storedQuery, persistentEntity)) {
// This is being closed in try with resources from where it is being called
@SuppressWarnings({"java:S2095"})
Expand All @@ -1458,7 +1515,7 @@
}
return connection.prepareStatement(storedQuery.getQuery(), new String[]{persistentEntity.getIdentity().getPersistedName()});
} else {
return connection.prepareStatement(storedQuery.getQuery(), hasGeneratedId ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
return connection.prepareStatement(storedQuery.getQuery(), requiresGeneratedKeys ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS);
}
} else {
return connection.prepareStatement(storedQuery.getQuery());
Expand Down Expand Up @@ -1487,7 +1544,7 @@
try (PreparedStatement ps = prepare(ctx.connection)) {
setParameters(ps, storedQuery);
rowsUpdated = Arrays.stream(ps.executeBatch()).sum();
if (hasGeneratedId) {
if (requiresGeneratedKeys) {
RuntimePersistentProperty<T> identity = persistentEntity.getIdentity();
List<Object> ids = new ArrayList<>();
try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.jdbc.h2

import io.micronaut.context.ApplicationContext
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.Insert
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class H2BatchInsertSpec extends Specification implements H2TestPropertyProvider {

@AutoCleanup
@Shared
ApplicationContext context = ApplicationContext.run(properties)

@Shared
H2BatchInsertBookRepository repository = context.getBean(H2BatchInsertBookRepository)

void setup() {
repository.deleteAll()
}

void "custom void insertAll stores generated-id inserts without mutating input ids"() {
given:
def books = [
new H2BatchInsertBook(title: "Solaris"),
new H2BatchInsertBook(title: "Eden")
]

when:
repository.customInsertAll(books)
def savedBooks = repository.findAll()

then:
books*.id == [null, null]
savedBooks.size() == 2
savedBooks*.id.every { it != null }
savedBooks*.title as Set == ["Solaris", "Eden"] as Set
}

void "custom count insertAll stores generated-id inserts without mutating input ids"() {
given:
def books = [
new H2BatchInsertBook(title: "Fiasco"),
new H2BatchInsertBook(title: "The Invincible")
]

when:
long inserted = repository.customInsertAllCount(books)
def savedBooks = repository.findAll()

then:
inserted == 2
books*.id == [null, null]
savedBooks.size() == 2
savedBooks*.id.every { it != null }
savedBooks*.title as Set == ["Fiasco", "The Invincible"] as Set
}
}

@MappedEntity("h2_batch_insert_book")
class H2BatchInsertBook {

@Id
@GeneratedValue
Long id

String title
}

@JdbcRepository(dialect = Dialect.H2)
interface H2BatchInsertBookRepository extends CrudRepository<H2BatchInsertBook, Long> {

@Insert
void customInsertAll(List<H2BatchInsertBook> entities)

@Insert
long customInsertAllCount(List<H2BatchInsertBook> entities)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.jdbc.mariadb

import io.micronaut.context.ApplicationContext
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.Insert
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class MariaBatchInsertSpec extends Specification implements MariaTestPropertyProvider {

@AutoCleanup
@Shared
ApplicationContext context = ApplicationContext.run(properties)

@Shared
MariaBatchBookRepository repository = context.getBean(MariaBatchBookRepository)

@Shared
MariaBatchRecordRepository recordRepository = context.getBean(MariaBatchRecordRepository)

void setup() {
repository.deleteAll()
recordRepository.deleteAll()
}

void "custom void insertAll stores generated-id inserts without mutating input ids"() {
given:
def books = [
new MariaBatchBook(title: "The Left Hand"),
new MariaBatchBook(title: "The Dispossessed")
]

when:
repository.customInsertAll(books)
def savedBooks = repository.findAll()

then:
savedBooks.size() == 2
books*.id == [null, null]
savedBooks*.id.every { it != null }
savedBooks*.title as Set == ["The Left Hand", "The Dispossessed"] as Set
}

void "custom count insertAll stores generated-id inserts without mutating input ids"() {
given:
def books = [
new MariaBatchBook(title: "The Lathe of Heaven"),
new MariaBatchBook(title: "City of Illusions")
]

when:
long inserted = repository.customInsertAllCount(books)
def savedBooks = repository.findAll()

then:
inserted == 2
savedBooks.size() == 2
books*.id == [null, null]
savedBooks*.id.every { it != null }
savedBooks*.title as Set == ["The Lathe of Heaven", "City of Illusions"] as Set
}

void "saveAll generated-key inserts populate ids through fallback path"() {
given:
def books = [
new MariaBatchBook(title: "A Wizard of Earthsea"),
new MariaBatchBook(title: "The Tombs of Atuan")
]

when:
def saved = repository.saveAll(books)

then:
saved*.id.every { it != null }
repository.count() == 2
}

void "saveAll generated-key record inserts populate ids through fallback path"() {
given:
def records = (0..<100).collect { new MariaBatchRecord(0L, "name-$it") }

when:
List<MariaBatchRecord> saved = recordRepository.saveAll(records)

then:
saved.size() == 100
saved.collect { it.id() }.every { it != null && it != 0L }
records.collect { it.id() }.every { it == 0L }
}

void "custom void insertAll stores generated-id record inserts without mutating input ids"() {
given:
def records = (0..<100).collect { new MariaBatchRecord(0L, "name-$it") }

when:
recordRepository.insertAll(records)
def savedRecords = recordRepository.findAll()

then:
records.collect { it.id() }.every { it == 0L }
savedRecords.size() == 100
savedRecords.every { it.id() != null && it.id() != 0L }
}
}

@MappedEntity("maria_batch_book")
class MariaBatchBook {

@Id
@GeneratedValue
Long id

String title
}

@JdbcRepository(dialect = Dialect.MYSQL)
interface MariaBatchBookRepository extends CrudRepository<MariaBatchBook, Long> {

@Insert
void customInsertAll(List<MariaBatchBook> entities)

@Insert
long customInsertAllCount(List<MariaBatchBook> entities)
}

@JdbcRepository(dialect = Dialect.MYSQL)
interface MariaBatchRecordRepository extends CrudRepository<MariaBatchRecord, Long> {

void insertAll(List<MariaBatchRecord> entities)
}
Loading
Loading