From 2a152596cf95395946c2b3bb34ab147d0ffd110c Mon Sep 17 00:00:00 2001 From: Radovan Radic Date: Mon, 28 Apr 2025 12:16:56 +0200 Subject: [PATCH 01/26] Fix schema generator and query execution when entities have dynamic/configurable mapping value (for Oracle DB) (#3393) * Attempt to implement property replacement for Oracle when uppercased * Fix quoting logic * Fix cascade operation * Fixed resolving of query placeholder values * Trigger build for branch temporary * Remove debug * Revert "Trigger build for branch temporary" This reverts commit c9fdf30d9ff59ea76b2d1f1d2563a9ebb6fd58a9. --- .../builder/AbstractSqlLikeQueryBuilder.java | 12 ++++ .../sql/AbstractSqlLikeQueryBuilder2.java | 12 ++++ .../query/builder/sql/SqlQueryBuilder.java | 40 ++++++------- .../query/builder/sql/SqlQueryBuilder2.java | 40 ++++++------- .../builder/sql/SqlQueryBuilderUtils.java | 48 +++++++++++++++ .../internal/AbstractCascadeOperations.java | 6 +- .../sql/AbstractSqlRepositoryOperations.java | 59 +++++++++++++++++-- .../jdbc-example-records-java/build.gradle | 4 +- .../src/main/java/example/BookRepository.java | 2 +- .../src/main/java/example/CartRepository.java | 2 +- .../src/main/java/example/City.java | 4 +- .../java/example/CourseRatingRepository.java | 2 +- .../main/java/example/PlantRepository.java | 2 +- .../main/java/example/StudentRepository.java | 2 +- .../src/main/java/example/User.java | 2 +- .../src/main/java/example/UserRepository.java | 4 +- .../src/main/resources/application.yml | 11 ++-- .../test/java/example/BookRepositorySpec.java | 4 +- 18 files changed, 189 insertions(+), 67 deletions(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java index 69750245e2c..b735cffb671 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/AbstractSqlLikeQueryBuilder.java @@ -721,6 +721,18 @@ protected String getTableAsKeyword() { * @return The quoted name */ protected String quote(String persistedName) { + return quote(persistedName, false); + } + + /** + * Quote a persisted name (schema, table or column name) for the dialect. + * + * @param persistedName The persisted name. + * @param supportsDynamicValues Whether persisted name supports dynamic values. Schema and table can have + * dynamic value (like ${config.entry}) and columns can't. + * @return The quoted name + */ + protected String quote(String persistedName, boolean supportsDynamicValues) { return "\"" + persistedName + "\""; } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java index 36802013751..8c16f9e8685 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder2.java @@ -401,6 +401,18 @@ protected String getTableAsKeyword() { * @return The quoted name */ protected String quote(String persistedName) { + return quote(persistedName, false); + } + + /** + * Quote a persisted name (schema, table or column name) for the dialect. + * + * @param persistedName The persisted name. + * @param supportsDynamicValues Whether persisted name supports dynamic values. Schema and table can have + * dynamic value (like ${config.entry}) and columns can't. + * @return The quoted name + */ + protected String quote(String persistedName, boolean supportsDynamicValues) { return "\"" + persistedName + "\""; } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java index 83734bb6eb3..a0918e005ab 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder.java @@ -270,7 +270,7 @@ String[] buildDropTableStatements(@NonNull PersistentEntity entity) { .orElseGet(() -> getMappedName(namingStrategy, association) ); - dropStatements.add("DROP TABLE " + (escape ? quote(joinTableName) : joinTableName) + ";"); + dropStatements.add("DROP TABLE " + (escape ? quote(joinTableName, true) : joinTableName) + ";"); } dropStatements.add(sql); @@ -298,12 +298,12 @@ String buildJoinTableInsert(@NonNull PersistentEntity entity, @NonNull Associati .orElseGet(() -> getMappedName(namingStrategy, association) ); - joinTableName = quote(joinTableName); + joinTableName = quote(joinTableName, true); String joinTableSchema = annotationMetadata .stringValue(ANN_JOIN_TABLE, SqlMembers.SCHEMA) .orElse(getSchemaName(entity)); if (StringUtils.isNotEmpty(joinTableSchema)) { - joinTableSchema = quote(joinTableSchema); + joinTableSchema = quote(joinTableSchema, true); joinTableName = joinTableSchema + DOT + joinTableName; } List leftJoinColumns = resolveJoinTableJoinColumns(annotationMetadata, true, entity, namingStrategy); @@ -353,7 +353,7 @@ String[] buildCreateTableStatements(@NonNull PersistentEntity entity) { String schema = getSchemaName(entity); if (StringUtils.isNotEmpty(schema)) { if (escape) { - schema = quote(schema); + schema = quote(schema, true); } createStatements.add("CREATE SCHEMA " + schema + ";"); } @@ -376,12 +376,12 @@ String[] buildCreateTableStatements(@NonNull PersistentEntity entity) { getMappedName(namingStrategy, association) ); if (escape) { - joinTableName = quote(joinTableName); + joinTableName = quote(joinTableName, true); } String joinTableSchema = annotationMetadata.stringValue(ANN_JOIN_TABLE, SqlMembers.SCHEMA).orElse(null); if (StringUtils.isNotEmpty(joinTableSchema)) { if (escape) { - joinTableSchema = quote(joinTableSchema); + joinTableSchema = quote(joinTableSchema, true); } } else { joinTableSchema = schema; @@ -538,7 +538,7 @@ String[] buildCreateTableStatements(@NonNull PersistentEntity entity) { createStatements.add(generatedDefinition); } else if (isSequence) { final boolean isSqlServer = dialect == Dialect.SQL_SERVER; - final String sequenceName = quote(unescapedTableName + SEQ_SUFFIX); + final String sequenceName = quote(unescapedTableName + SEQ_SUFFIX, true); String createSequenceStmt = "CREATE SEQUENCE " + sequenceName; if (isSqlServer) { createSequenceStmt += " AS BIGINT"; @@ -1215,10 +1215,10 @@ private String[] asStringPath(List associations, PersistentProperty private String getSequenceStatement(String unescapedTableName, PersistentProperty property) { final String sequenceName = resolveSequenceName(property, unescapedTableName); return switch (dialect) { - case ORACLE -> quote(sequenceName) + ".nextval"; + case ORACLE -> quote(sequenceName, true) + ".nextval"; case POSTGRES -> "nextval('" + sequenceName + "')"; case H2 -> "nextval('" + sequenceName + "')"; - case SQL_SERVER -> "NEXT VALUE FOR " + quote(sequenceName); + case SQL_SERVER -> "NEXT VALUE FOR " + quote(sequenceName, true); default -> throw new IllegalStateException("Cannot generate a sequence for dialect: " + dialect); }; } @@ -1307,12 +1307,12 @@ public String getTableName(PersistentEntity entity) { String schema = getSchemaName(entity); if (StringUtils.isNotEmpty(schema)) { if (escape) { - return quote(schema) + '.' + quote(tableName); + return quote(schema, true) + '.' + quote(tableName, true); } else { return schema + '.' + tableName; } } else { - return escape ? quote(tableName) : tableName; + return escape ? quote(tableName, true) : tableName; } } @@ -1504,7 +1504,7 @@ protected void buildJoin(String joinType, .stringValue(ANN_JOIN_TABLE, SqlMembers.SCHEMA) .orElse(getSchemaName(associationOwner)); if (StringUtils.isNotEmpty(joinTableSchema) && escape) { - joinTableSchema = quote(joinTableSchema); + joinTableSchema = quote(joinTableSchema, true); } String joinTableName = annotationMetadata .stringValue(ANN_JOIN_TABLE, "name") @@ -1512,7 +1512,7 @@ protected void buildJoin(String joinType, String joinTableAlias = annotationMetadata .stringValue(ANN_JOIN_TABLE, "alias") .orElseGet(() -> currentJoinAlias + joinTableName + "_"); - String finalTableName = escape ? quote(joinTableName) : joinTableName; + String finalTableName = escape ? quote(joinTableName, true) : joinTableName; if (StringUtils.isNotEmpty(joinTableSchema)) { finalTableName = joinTableSchema + DOT + finalTableName; } @@ -1708,20 +1708,16 @@ private List merge(List left, List right) { return associations; } - /** - * Quote a column name for the dialect. - * - * @param persistedName The persisted name. - * @return The quoted name - */ @Override - protected String quote(String persistedName) { + protected String quote(String persistedName, boolean supportsDynamicValues) { return switch (dialect) { case MYSQL, H2 -> '`' + persistedName + '`'; case SQL_SERVER -> '[' + persistedName + ']'; - case ORACLE -> + case ORACLE -> { // Oracle requires quoted identifiers to be in upper case - '"' + persistedName.toUpperCase(Locale.ENGLISH) + '"'; + String result = supportsDynamicValues ? SqlQueryBuilderUtils.mapPersistedName(persistedName, s -> s.toUpperCase(Locale.ENGLISH)) : persistedName.toUpperCase(Locale.ENGLISH); + yield '"' + result + '"'; + } default -> '"' + persistedName + '"'; }; } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java index 9bdb896d128..ff43021f127 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java @@ -248,7 +248,7 @@ public String[] buildDropTableStatements(@NonNull PersistentEntity entity) { .orElseGet(() -> getMappedName(namingStrategy, association) ); - dropStatements.add("DROP TABLE " + (escape ? quote(joinTableName) : joinTableName) + ";"); + dropStatements.add("DROP TABLE " + (escape ? quote(joinTableName, true) : joinTableName) + ";"); } dropStatements.add(sql); @@ -276,12 +276,12 @@ public String buildJoinTableInsert(@NonNull PersistentEntity entity, @NonNull As .orElseGet(() -> getMappedName(namingStrategy, association) ); - joinTableName = quote(joinTableName); + joinTableName = quote(joinTableName, true); String joinTableSchema = annotationMetadata .stringValue(ANN_JOIN_TABLE, SqlMembers.SCHEMA) .orElse(getSchemaName(entity)); if (StringUtils.isNotEmpty(joinTableSchema)) { - joinTableSchema = quote(joinTableSchema); + joinTableSchema = quote(joinTableSchema, true); joinTableName = joinTableSchema + DOT + joinTableName; } List leftJoinColumns = resolveJoinTableJoinColumns(annotationMetadata, true, entity, namingStrategy); @@ -330,7 +330,7 @@ public String[] buildCreateTableStatements(@NonNull PersistentEntity entity) { String schema = getSchemaName(entity); if (StringUtils.isNotEmpty(schema)) { if (escape) { - schema = quote(schema); + schema = quote(schema, true); } createStatements.add("CREATE SCHEMA " + schema + ";"); } @@ -353,12 +353,12 @@ public String[] buildCreateTableStatements(@NonNull PersistentEntity entity) { getMappedName(namingStrategy, association) ); if (escape) { - joinTableName = quote(joinTableName); + joinTableName = quote(joinTableName, true); } String joinTableSchema = annotationMetadata.stringValue(ANN_JOIN_TABLE, SqlMembers.SCHEMA).orElse(null); if (StringUtils.isNotEmpty(joinTableSchema)) { if (escape) { - joinTableSchema = quote(joinTableSchema); + joinTableSchema = quote(joinTableSchema, true); } } else { joinTableSchema = schema; @@ -517,7 +517,7 @@ public String[] buildCreateTableStatements(@NonNull PersistentEntity entity) { createStatements.add(generatedDefinition); } else if (isSequence) { final boolean isSqlServer = dialect == Dialect.SQL_SERVER; - final String sequenceName = quote(unescapedTableName + SEQ_SUFFIX); + final String sequenceName = quote(unescapedTableName + SEQ_SUFFIX, true); String createSequenceStmt = "CREATE SEQUENCE " + sequenceName; if (isSqlServer) { createSequenceStmt += " AS BIGINT"; @@ -1051,9 +1051,9 @@ private String[] asStringPath(List associations, PersistentProperty private String getSequenceStatement(String unescapedTableName, PersistentProperty property) { final String sequenceName = resolveSequenceName(property, unescapedTableName); return switch (dialect) { - case ORACLE -> quote(sequenceName) + ".nextval"; + case ORACLE -> quote(sequenceName, true) + ".nextval"; case POSTGRES -> "nextval('" + sequenceName + "')"; - case SQL_SERVER -> "NEXT VALUE FOR " + quote(sequenceName); + case SQL_SERVER -> "NEXT VALUE FOR " + quote(sequenceName, true); default -> throw new IllegalStateException("Cannot generate a sequence for dialect: " + dialect); }; @@ -1089,12 +1089,12 @@ public String getTableName(PersistentEntity entity) { String schema = getSchemaName(entity); if (StringUtils.isNotEmpty(schema)) { if (escape) { - return quote(schema) + '.' + quote(tableName); + return quote(schema, true) + '.' + quote(tableName, true); } else { return schema + '.' + tableName; } } else { - return escape ? quote(tableName) : tableName; + return escape ? quote(tableName, true) : tableName; } } @@ -1195,7 +1195,7 @@ protected void buildJoin(String joinType, .stringValue(ANN_JOIN_TABLE, SqlMembers.SCHEMA) .orElse(getSchemaName(associationOwner)); if (StringUtils.isNotEmpty(joinTableSchema) && escape) { - joinTableSchema = quote(joinTableSchema); + joinTableSchema = quote(joinTableSchema, true); } String joinTableName = annotationMetadata .stringValue(ANN_JOIN_TABLE, "name") @@ -1203,7 +1203,7 @@ protected void buildJoin(String joinType, String joinTableAlias = annotationMetadata .stringValue(ANN_JOIN_TABLE, "alias") .orElseGet(() -> currentJoinAlias + joinTableName + "_"); - String finalTableName = escape ? quote(joinTableName) : joinTableName; + String finalTableName = escape ? quote(joinTableName, true) : joinTableName; if (StringUtils.isNotEmpty(joinTableSchema)) { finalTableName = joinTableSchema + DOT + finalTableName; } @@ -1388,20 +1388,16 @@ private List merge(List left, List right) { return associations; } - /** - * Quote a column name for the dialect. - * - * @param persistedName The persisted name. - * @return The quoted name - */ @Override - protected String quote(String persistedName) { + protected String quote(String persistedName, boolean supportsDynamicValues) { return switch (dialect) { case MYSQL, H2 -> '`' + persistedName + '`'; case SQL_SERVER -> '[' + persistedName + ']'; - case ORACLE -> + case ORACLE -> { // Oracle requires quoted identifiers to be in upper case - '"' + persistedName.toUpperCase(Locale.ENGLISH) + '"'; + String result = supportsDynamicValues ? SqlQueryBuilderUtils.mapPersistedName(persistedName, s -> s.toUpperCase(Locale.ENGLISH)) : persistedName.toUpperCase(Locale.ENGLISH); + yield '"' + result + '"'; + } default -> '"' + persistedName + '"'; }; } diff --git a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilderUtils.java b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilderUtils.java index 5262febacec..b754e274ea8 100644 --- a/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilderUtils.java +++ b/data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilderUtils.java @@ -15,10 +15,12 @@ */ package io.micronaut.data.model.query.builder.sql; +import io.micronaut.context.exceptions.ConfigurationException; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.MappedProperty; import io.micronaut.data.exceptions.MappingException; import io.micronaut.data.model.Association; @@ -31,6 +33,7 @@ import java.sql.Clob; import java.util.Optional; import java.util.OptionalInt; +import java.util.function.Function; /** * The utility methods for query builders. @@ -38,8 +41,53 @@ @Internal final class SqlQueryBuilderUtils { + private static final String PREFIX = "${"; + private static final String SUFFIX = "}"; + private SqlQueryBuilderUtils() { } + /** + * Maps the persisted name by applying the provided mapping function to each segment + * of the persisted name that does not contain placeholders. Placeholders are defined + * as strings enclosed within '${' and '}' characters. + * + * @param persistedName the persisted name to be mapped + * @param mapFunction the function to apply to each non-placeholder segment + * @return the mapped persisted name + * @throws ConfigurationException if incomplete placeholder definitions are detected + */ + static String mapPersistedName(String persistedName, Function mapFunction) { + if (StringUtils.isEmpty(persistedName)) { + return persistedName; + } + StringBuilder sb = new StringBuilder(); + + String value = persistedName; + int i = value.indexOf(PREFIX); + while (i > -1) { + //the text before the prefix + if (i > 0) { + String rawSegment = value.substring(0, i); + sb.append(mapFunction.apply(rawSegment)); + } + // everything after the prefix + value = value.substring(i + PREFIX.length()); + int suffixIdx = value.indexOf(SUFFIX); + if (suffixIdx > -1) { + String expr = value.substring(0, suffixIdx).trim(); + sb.append(PREFIX).append(expr).append(SUFFIX); + value = value.substring(suffixIdx + SUFFIX.length()); + } else { + throw new ConfigurationException("Incomplete placeholder definitions detected: " + persistedName); + } + i = value.indexOf(PREFIX); + } + if (!value.isEmpty()) { + sb.append(mapFunction.apply(value)); + } + return sb.toString(); + } + /** * Adds column type for the column for creating table. * diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java index 89b8180a5ae..c9d0628ed4e 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/AbstractCascadeOperations.java @@ -102,6 +102,10 @@ protected void cascade(AnnotationMetadata annotationMetadata, Class repos case MANY_TO_MANY: final PersistentAssociationPath inverse = association.getInversePathSide().orElse(null); Iterable children = (Iterable) association.getProperty().get(entity); + if (children != null) { + // If collection is immutable then below code won't work (iterator.set(...)) + children = new ArrayList<>(CollectionUtils.iterableToList(children)); + } if (children == null || !children.iterator().hasNext()) { continue; } @@ -185,7 +189,7 @@ protected T afterCascadedMany(T entity, List associations, Iter } } } - if (prevChildren != newChildren) { + if (association.getProperty().get(entity) != newChildren) { entity = convertAndSetWithValue(association.getProperty(), entity, newChildren); } return entity; diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java index c7439ed63ef..ccc48117173 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java @@ -18,12 +18,14 @@ import io.micronaut.aop.MethodInvocationContext; import io.micronaut.context.ApplicationContextProvider; import io.micronaut.context.BeanContext; +import io.micronaut.context.env.PropertyPlaceholderResolver; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.beans.BeanProperty; import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.AutoPopulated; import io.micronaut.data.annotation.MappedProperty; import io.micronaut.data.annotation.Repository; @@ -116,6 +118,7 @@ public abstract class AbstractSqlRepositoryOperations sqlJsonColumnMapperProvider; protected final Map queryBuilders = new HashMap<>(10); + protected final PropertyPlaceholderResolver propertyPlaceholderResolver; protected final Map repositoriesWithHardcodedDataSource = new HashMap<>(10); private final Map entityInserts = new ConcurrentHashMap<>(10); private final Map entityUpdates = new ConcurrentHashMap<>(10); @@ -167,6 +170,7 @@ protected AbstractSqlRepositoryOperations( repositoriesWithHardcodedDataSource.put(beanType, targetDs); } } + this.propertyPlaceholderResolver = getApplicationContext().getEnvironment().getPlaceholderResolver(); } /** @@ -310,8 +314,8 @@ protected SqlStoredQuery resolveEntityInsert(AnnotationMetadata annota return entityInserts.computeIfAbsent(new QueryKey(repositoryType, rootEntity), (queryKey) -> { final SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType); final QueryResult queryResult = queryBuilder.buildInsert(annotationMetadata, new SqlQueryBuilder2.InsertQueryDefinitionImpl(persistentEntity)); - - return new DefaultSqlStoredQuery<>(QueryResultStoredQuery.single(OperationType.INSERT, "Custom insert", AnnotationMetadata.EMPTY_METADATA, queryResult, rootEntity), persistentEntity, queryBuilder); + final QueryResult newQueryResult = replaceQueryPlaceholders(queryResult); + return new DefaultSqlStoredQuery<>(QueryResultStoredQuery.single(OperationType.INSERT, "Custom insert", AnnotationMetadata.EMPTY_METADATA, newQueryResult, rootEntity), persistentEntity, queryBuilder); }); } @@ -370,8 +374,9 @@ protected SqlStoredQuery resolveEntityUpdate(AnnotationMetadata annota .forEach(prop -> criteriaUpdate.set(prop.getName(), criteriaBuilder.parameter(prop.getType()))); final QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaUpdate).buildQuery(annotationMetadata, queryBuilder); + final QueryResult newQueryResult = replaceQueryPlaceholders(queryResult); return new DefaultSqlStoredQuery<>( - QueryResultStoredQuery.single(OperationType.UPDATE, "Custom update", AnnotationMetadata.EMPTY_METADATA, queryResult, rootEntity), + QueryResultStoredQuery.single(OperationType.UPDATE, "Custom update", AnnotationMetadata.EMPTY_METADATA, newQueryResult, rootEntity), persistentEntity, queryBuilder); }); @@ -388,7 +393,7 @@ protected SqlStoredQuery resolveEntityUpdate(AnnotationMetadata annota * @return The operation */ protected SqlStoredQuery resolveSqlInsertAssociation(Class repositoryType, RuntimeAssociation association, RuntimePersistentEntity persistentEntity, T entity) { - String sqlInsert = resolveAssociationInsert(repositoryType, persistentEntity, association); + String sqlInsert = resolveEnvPlaceholderValues(resolveAssociationInsert(repositoryType, persistentEntity, association)); final SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType); List parameters = new ArrayList<>(); for (Map.Entry property : idPropertiesWithValues(persistentEntity.getIdentity(), entity).toList()) { @@ -651,6 +656,42 @@ protected final JsonDataType getJsonDataType(QueryResultInfo queryResultInfo) { return JsonDataType.DEFAULT; } + /** + * Replaces placeholders in the given query result with actual values. + * + * @param queryResult The query result to modify + * @return A new query result with replaced placeholders + */ + protected final QueryResult replaceQueryPlaceholders(QueryResult queryResult) { + return new QueryResult() { + @NonNull + @Override + public String getQuery() { + return resolveEnvPlaceholderValues(queryResult.getQuery()); + } + + @Override + public List getQueryParts() { + return queryResult.getQueryParts(); + } + + /** + * Returns the parameters binding for this query. + * + * @return the parameters binding + */ + @Override + public List getParameterBindings() { + return queryResult.getParameterBindings(); + } + + @Override + public Map getAdditionalRequiredParameters() { + return queryResult.getAdditionalRequiredParameters(); + } + }; + } + /** * Creates {@link JsonQueryResultMapper} for JSON deserialization. * @@ -774,6 +815,16 @@ public Object read(RS object, String name) { }; } + private String resolveEnvPlaceholderValues(String value) { + if (StringUtils.isEmpty(value)) { + return value; + } + if (value.contains(propertyPlaceholderResolver.getPrefix())) { + value = propertyPlaceholderResolver.resolveRequiredPlaceholders(value); + } + return value; + } + /** * Used to cache queries for entities. */ diff --git a/doc-examples/jdbc-example-records-java/build.gradle b/doc-examples/jdbc-example-records-java/build.gradle index 441652be26d..31aff8b5a24 100644 --- a/doc-examples/jdbc-example-records-java/build.gradle +++ b/doc-examples/jdbc-example-records-java/build.gradle @@ -23,5 +23,7 @@ dependencies { runtimeOnly mnSql.micronaut.jdbc.tomcat runtimeOnly mnLogging.logback.classic - runtimeOnly mnSql.h2 + runtimeOnly mnSql.ojdbc11 + + testResourcesService mnSql.ojdbc11 } diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/BookRepository.java b/doc-examples/jdbc-example-records-java/src/main/java/example/BookRepository.java index 9f1a42e5def..f680d80c3a4 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/BookRepository.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/BookRepository.java @@ -11,7 +11,7 @@ import java.util.List; -@JdbcRepository(dialect = Dialect.H2) +@JdbcRepository(dialect = Dialect.ORACLE) public interface BookRepository extends CrudRepository { // tag::simple[] Book findByTitle(String title); diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/CartRepository.java b/doc-examples/jdbc-example-records-java/src/main/java/example/CartRepository.java index cc4b0dd65d7..84a756bbe66 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/CartRepository.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/CartRepository.java @@ -8,7 +8,7 @@ import java.util.Optional; -@JdbcRepository(dialect = Dialect.H2) +@JdbcRepository(dialect = Dialect.ORACLE) public interface CartRepository extends CrudRepository { @Join("items") diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/City.java b/doc-examples/jdbc-example-records-java/src/main/java/example/City.java index b6a4df7d644..169c47ce230 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/City.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/City.java @@ -5,7 +5,7 @@ import io.micronaut.data.annotation.Id; import io.micronaut.data.annotation.MappedEntity; -@MappedEntity +@MappedEntity("${entity.prefix}city") public record City( @Id @GeneratedValue @Nullable Long id, @@ -14,4 +14,4 @@ public record City( public City(String name) { this(null, name); } -} \ No newline at end of file +} diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/CourseRatingRepository.java b/doc-examples/jdbc-example-records-java/src/main/java/example/CourseRatingRepository.java index 5ed43f1b77d..3a954c64fd6 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/CourseRatingRepository.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/CourseRatingRepository.java @@ -4,6 +4,6 @@ import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; -@JdbcRepository(dialect = Dialect.H2) +@JdbcRepository(dialect = Dialect.ORACLE) public interface CourseRatingRepository extends CrudRepository { } diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/PlantRepository.java b/doc-examples/jdbc-example-records-java/src/main/java/example/PlantRepository.java index ac813c84955..962787648ec 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/PlantRepository.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/PlantRepository.java @@ -4,6 +4,6 @@ import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; -@JdbcRepository(dialect = Dialect.H2) +@JdbcRepository(dialect = Dialect.ORACLE) public interface PlantRepository extends CrudRepository { } diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/StudentRepository.java b/doc-examples/jdbc-example-records-java/src/main/java/example/StudentRepository.java index 2e29e92b7a7..33a7fc5d55f 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/StudentRepository.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/StudentRepository.java @@ -8,7 +8,7 @@ import java.util.Optional; -@JdbcRepository(dialect = Dialect.H2) +@JdbcRepository(dialect = Dialect.ORACLE) public interface StudentRepository extends CrudRepository { @Join("courses") diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/User.java b/doc-examples/jdbc-example-records-java/src/main/java/example/User.java index fe21cff2bd8..ffbdc3ac10a 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/User.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/User.java @@ -6,7 +6,7 @@ import io.micronaut.data.annotation.Relation; import io.micronaut.data.annotation.Version; -@MappedEntity +@MappedEntity("${entity.prefix}user") public record User( @Id @GeneratedValue Long id, diff --git a/doc-examples/jdbc-example-records-java/src/main/java/example/UserRepository.java b/doc-examples/jdbc-example-records-java/src/main/java/example/UserRepository.java index 8fcd57f8b4a..f3a9c2e1530 100644 --- a/doc-examples/jdbc-example-records-java/src/main/java/example/UserRepository.java +++ b/doc-examples/jdbc-example-records-java/src/main/java/example/UserRepository.java @@ -8,7 +8,7 @@ import java.util.Optional; -@JdbcRepository(dialect = Dialect.H2) +@JdbcRepository(dialect = Dialect.ORACLE) public interface UserRepository extends CrudRepository { @Join("address") @@ -17,4 +17,6 @@ public interface UserRepository extends CrudRepository { Optional findById(@NonNull Long id); User save(String name, Address address); + + Long countById(Long id); } diff --git a/doc-examples/jdbc-example-records-java/src/main/resources/application.yml b/doc-examples/jdbc-example-records-java/src/main/resources/application.yml index a38a6f00501..e9c76fadece 100644 --- a/doc-examples/jdbc-example-records-java/src/main/resources/application.yml +++ b/doc-examples/jdbc-example-records-java/src/main/resources/application.yml @@ -1,8 +1,7 @@ datasources: default: - url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - driverClassName: org.h2.Driver - username: sa - password: '' - schema-generate: CREATE_DROP - dialect: H2 + driverClassName: oracle.jdbc.OracleDriver + dialect: ORACLE + schema-generate: CREATE +entity: + prefix: TBL_COMMON_ diff --git a/doc-examples/jdbc-example-records-java/src/test/java/example/BookRepositorySpec.java b/doc-examples/jdbc-example-records-java/src/test/java/example/BookRepositorySpec.java index f7c91a24ba0..4c8c0c21c8f 100644 --- a/doc-examples/jdbc-example-records-java/src/test/java/example/BookRepositorySpec.java +++ b/doc-examples/jdbc-example-records-java/src/test/java/example/BookRepositorySpec.java @@ -37,7 +37,7 @@ void testAnnotationMetadata() { .orElse(null); assertEquals( // <4> - "SELECT book_.`id`,book_.`date_created`,book_.`title`,book_.`pages` FROM `book` book_ WHERE (book_.`title` = ?)", + "SELECT book_.\"ID\",book_.\"DATE_CREATED\",book_.\"TITLE\",book_.\"PAGES\" FROM \"BOOK\" book_ WHERE (book_.\"TITLE\" = ?)", query ); @@ -129,4 +129,4 @@ void testDto() { assertEquals("The Shining", book.getTitle()); } -} \ No newline at end of file +} From 602af16c82a53f63e2164a3b7f442dcccf8a54a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:39:38 +0200 Subject: [PATCH 02/26] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.8.12 (#3400) * fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.8.12 * Use mysql:8.4.5 image * Use mysql:8.4.5 image * Use mysql:8.4.5 image --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: radovanradic --- .../hibernate/reactive/MySqlHibernateReactiveProperties.java | 3 ++- data-jdbc/src/test/resources/application.yml | 1 + data-r2dbc/src/test/resources/application.yaml | 1 + .../src/main/resources/application.yml | 4 ++++ .../src/test/resources/application-test.yml | 5 +++++ .../r2dbc-example-kotlin/src/main/resources/application.yml | 5 +++++ .../src/main/resources/application.yml | 5 +++++ gradle/libs.versions.toml | 2 +- 8 files changed, 24 insertions(+), 2 deletions(-) diff --git a/data-hibernate-reactive/src/test/java/io/micronaut/data/hibernate/reactive/MySqlHibernateReactiveProperties.java b/data-hibernate-reactive/src/test/java/io/micronaut/data/hibernate/reactive/MySqlHibernateReactiveProperties.java index 23590a5ad4b..aa02779a759 100644 --- a/data-hibernate-reactive/src/test/java/io/micronaut/data/hibernate/reactive/MySqlHibernateReactiveProperties.java +++ b/data-hibernate-reactive/src/test/java/io/micronaut/data/hibernate/reactive/MySqlHibernateReactiveProperties.java @@ -11,7 +11,8 @@ default Map getProperties() { return Map.of( "jpa.default.properties.hibernate.hbm2ddl.auto", "create-drop", "jpa.default.reactive", "true", - "jpa.default.properties.hibernate.connection.db-type", "mysql" + "jpa.default.properties.hibernate.connection.db-type", "mysql", + "test-resources.containers.mysql.image-name", "mysql:8.4.5" ); } } diff --git a/data-jdbc/src/test/resources/application.yml b/data-jdbc/src/test/resources/application.yml index edca537035f..fae18770f38 100644 --- a/data-jdbc/src/test/resources/application.yml +++ b/data-jdbc/src/test/resources/application.yml @@ -8,6 +8,7 @@ test-resources: startup-timeout: 300s mysql: startup-timeout: 300s + image-name: mysql:8.4.5 oracle: startup-timeout: 600s db-name: test diff --git a/data-r2dbc/src/test/resources/application.yaml b/data-r2dbc/src/test/resources/application.yaml index 838b600c7fd..5d0748721e6 100644 --- a/data-r2dbc/src/test/resources/application.yaml +++ b/data-r2dbc/src/test/resources/application.yaml @@ -8,6 +8,7 @@ test-resources: startup-timeout: 300s mysql: startup-timeout: 300s + image-name: mysql:8.4.5 oracle: startup-timeout: 300s postgres: diff --git a/doc-examples/hibernate-reactive-example-java/src/main/resources/application.yml b/doc-examples/hibernate-reactive-example-java/src/main/resources/application.yml index d3ef94d4c9d..d946db0a081 100644 --- a/doc-examples/hibernate-reactive-example-java/src/main/resources/application.yml +++ b/doc-examples/hibernate-reactive-example-java/src/main/resources/application.yml @@ -6,3 +6,7 @@ jpa: hbm2ddl: auto: create-drop compileTimeHibernateProxies: true +test-resources: + containers: + mysql: + image-name: mysql:8.4.5 diff --git a/doc-examples/hibernate-sync-and-reactive-example-java/src/test/resources/application-test.yml b/doc-examples/hibernate-sync-and-reactive-example-java/src/test/resources/application-test.yml index ef3e97e3185..42b5b239a01 100644 --- a/doc-examples/hibernate-sync-and-reactive-example-java/src/test/resources/application-test.yml +++ b/doc-examples/hibernate-sync-and-reactive-example-java/src/test/resources/application-test.yml @@ -22,3 +22,8 @@ jpa: url: ${datasources.sync.url} username: ${datasources.sync.username} password: ${datasources.sync.password} + +test-resources: + containers: + mysql: + image-name: mysql:8.4.5 diff --git a/doc-examples/r2dbc-example-kotlin/src/main/resources/application.yml b/doc-examples/r2dbc-example-kotlin/src/main/resources/application.yml index b84d4d00536..966b7039a0c 100644 --- a/doc-examples/r2dbc-example-kotlin/src/main/resources/application.yml +++ b/doc-examples/r2dbc-example-kotlin/src/main/resources/application.yml @@ -11,3 +11,8 @@ datasources: default: db-type: mariadb dialect: mysql + +test-resources: + containers: + mysql: + image-name: mysql:8.4.5 diff --git a/doc-examples/r2dbc-multitenancy-datasource-example-java/src/main/resources/application.yml b/doc-examples/r2dbc-multitenancy-datasource-example-java/src/main/resources/application.yml index 8e9a06f91e6..49a28c23fa2 100644 --- a/doc-examples/r2dbc-multitenancy-datasource-example-java/src/main/resources/application.yml +++ b/doc-examples/r2dbc-multitenancy-datasource-example-java/src/main/resources/application.yml @@ -17,3 +17,8 @@ r2dbc: db-type: mariadb schema-generate: CREATE_DROP dialect: MYSQL + +test-resources: + containers: + mysql: + image-name: mysql:8.4.5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da5bbf00693..0e1dd6952f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.8.9" +micronaut = "4.8.12" micronaut-platform = "4.7.6" micronaut-docs = "2.0.0" micronaut-gradle-plugin = "4.4.5" From 437c889ebdc46e21239d6b16fd89ddfe37cd2965 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 28 Apr 2025 16:53:02 +0200 Subject: [PATCH 03/26] Fix for EmbeddedId regression in 4.0 --- .github/workflows/graalvm-latest.yml | 1 + .github/workflows/gradle.yml | 1 + .../EmbeddedAssociationJoinSpec.groovy | 2 +- .../CustomEmbeddedNameMapping.groovy | 8 ++++---- .../data/model/naming/NamingStrategy.java | 12 ++++++----- .../query/builder/SqlQueryBuilderSpec.groovy | 20 +++++++++---------- .../data/processor/sql/BuildQuerySpec.groovy | 6 +++--- .../sql/ColumnTransformerSpec.groovy | 12 +++++------ .../sql/CompositePrimaryKeySpec.groovy | 12 +++++------ .../processor/visitors/EmbeddedSpec.groovy | 4 ++-- .../data/tck/entities/Restaurant.java | 1 + 11 files changed, 42 insertions(+), 37 deletions(-) diff --git a/.github/workflows/graalvm-latest.yml b/.github/workflows/graalvm-latest.yml index e3053b0dc87..3073d2c0bd4 100644 --- a/.github/workflows/graalvm-latest.yml +++ b/.github/workflows/graalvm-latest.yml @@ -9,6 +9,7 @@ on: branches: - master - '[1-9]+.[0-9]+.x' + - embeddedid-colname pull_request: branches: - master diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 2327d05ea86..39a83b7c110 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -9,6 +9,7 @@ on: branches: - master - '[1-9]+.[0-9]+.x' + - embeddedid-colname pull_request: branches: - master diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy index 58c2b684b29..cc48739697d 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy @@ -57,7 +57,7 @@ class EmbeddedAssociationJoinSpec extends Specification implements H2TestPropert `id` bigint primary key not null, `value` text, `example` text, - `part_text` text); + `text` text); """).execute() } diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedNameMapping/CustomEmbeddedNameMapping.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedNameMapping/CustomEmbeddedNameMapping.groovy index d6815e50259..1fa63df2d82 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedNameMapping/CustomEmbeddedNameMapping.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedNameMapping/CustomEmbeddedNameMapping.groovy @@ -70,7 +70,7 @@ class CustomEmbeddedNameMapping extends Specification implements H2TestPropertyP def statements = encoder.buildCreateTableStatements(getRuntimePersistentEntity(MyBook)) then: - statements.join("\n") == 'CREATE TABLE "MyBook" ("id" VARCHAR(255) NOT NULL,"authorFirstName" VARCHAR(255) NOT NULL,"authorLastName" VARCHAR(255) NOT NULL,"authorDetailsIncludedNumberAge" INT NOT NULL, PRIMARY KEY("id"));' + statements.join("\n") == 'CREATE TABLE "MyBook" ("id" VARCHAR(255) NOT NULL,"firstName" VARCHAR(255) NOT NULL,"lastName" VARCHAR(255) NOT NULL,"numberAge" INT NOT NULL, PRIMARY KEY("id"));' } void "test build insert"() { @@ -79,7 +79,7 @@ class CustomEmbeddedNameMapping extends Specification implements H2TestPropertyP def res = encoder.buildInsert(AnnotationMetadata.EMPTY_METADATA, getRuntimePersistentEntity(MyBook)) then: - res.query == 'INSERT INTO "MyBook" ("authorFirstName","authorLastName","authorDetailsIncludedNumberAge","id") VALUES (?,?,?,?)' + res.query == 'INSERT INTO "MyBook" ("firstName","lastName","numberAge","id") VALUES (?,?,?,?)' } void "test update"() { @@ -92,7 +92,7 @@ class CustomEmbeddedNameMapping extends Specification implements H2TestPropertyP ) then: - res.query == 'UPDATE "MyBook" SET "id"=?,"authorFirstName"=?,"authorLastName"=?,"authorDetailsIncludedNumberAge"=? WHERE ("id" = ?)' + res.query == 'UPDATE "MyBook" SET "id"=?,"firstName"=?,"lastName"=?,"numberAge"=? WHERE ("id" = ?)' res.parameters == [ '1':'id', '2':'author.firstName', @@ -107,7 +107,7 @@ class CustomEmbeddedNameMapping extends Specification implements H2TestPropertyP QueryBuilder encoder = new SqlQueryBuilder() def q = encoder.buildQuery(AnnotationMetadata.EMPTY_METADATA, QueryModel.from(getRuntimePersistentEntity(MyBook)).idEq(new QueryParameter("xyz"))) then: - q.query == 'SELECT my_book_."id",my_book_."authorFirstName",my_book_."authorLastName",my_book_."authorDetailsIncludedNumberAge" FROM "MyBook" my_book_ WHERE (my_book_."id" = ?)' + q.query == 'SELECT my_book_."id",my_book_."firstName",my_book_."lastName",my_book_."numberAge" FROM "MyBook" my_book_ WHERE (my_book_."id" = ?)' } @Shared diff --git a/data-model/src/main/java/io/micronaut/data/model/naming/NamingStrategy.java b/data-model/src/main/java/io/micronaut/data/model/naming/NamingStrategy.java index 13828037dfd..638a476ed1d 100644 --- a/data-model/src/main/java/io/micronaut/data/model/naming/NamingStrategy.java +++ b/data-model/src/main/java/io/micronaut/data/model/naming/NamingStrategy.java @@ -148,11 +148,13 @@ default String mappedAssociatedName(@NonNull String associatedName) { foreignAssociation = association; } final String originalAssocName = association.getName(); - String assocName = association.getKind() == Relation.Kind.EMBEDDED ? association.getAnnotationMetadata().stringValue(MappedProperty.class).orElse(originalAssocName) : originalAssocName; - if (!sb.isEmpty()) { - sb.append(mappedAssociatedName(assocName)); - } else { - sb.append(assocName); + String assocName = association.getKind() == Relation.Kind.EMBEDDED ? association.getAnnotationMetadata().stringValue(MappedProperty.class).orElse(StringUtils.EMPTY_STRING) : originalAssocName; + if (StringUtils.isNotEmpty(assocName)) { + if (!sb.isEmpty()) { + sb.append(mappedAssociatedName(assocName)); + } else { + sb.append(assocName); + } } } if (foreignAssociation != null) { diff --git a/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy index c02bb69b236..5aa36fef95a 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy @@ -565,12 +565,12 @@ interface MyRepository { query << [ 'SELECT shipment_."sp_country",shipment_."sp_city",shipment_."field" FROM "Shipment1" shipment_ WHERE (shipment_."sp_country" = ? AND shipment_."sp_city" = ?)', 'SELECT shipment_."sp_country",shipment_."sp_city",shipment_."field" FROM "Shipment1" shipment_ WHERE (shipment_."sp_country" = ?)', - 'SELECT user_role_."id_user_id",user_role_."id_role_id" FROM "user_role_composite" user_role_ INNER JOIN "role_composite" user_role_id_role_ ON user_role_."id_role_id"=user_role_id_role_."id"', - 'SELECT user_role_."id_user_id",user_role_."id_role_id" FROM "user_role_composite" user_role_ INNER JOIN "user_composite" user_role_id_user_ ON user_role_."id_user_id"=user_role_id_user_."id" WHERE (user_role_."id_user_id" = ?)', - 'SELECT uidx."uuid",uidx."name",uidx."child_id",uidx."xyz",uidx."embedded_child_embedded_child2_id",uidx."nullable_value" FROM "uuid_entity" uidx WHERE (uidx."uuid" = ?)', - 'SELECT user_role_."id_user_id",user_role_."id_role_id" FROM "user_role_composite" user_role_ WHERE (user_role_."id_user_id" = ? AND user_role_."id_role_id" = ?)', + 'SELECT user_role_."user_id",user_role_."role_id" FROM "user_role_composite" user_role_ INNER JOIN "role_composite" user_role_id_role_ ON user_role_."role_id"=user_role_id_role_."id"', + 'SELECT user_role_."user_id",user_role_."role_id" FROM "user_role_composite" user_role_ INNER JOIN "user_composite" user_role_id_user_ ON user_role_."user_id"=user_role_id_user_."id" WHERE (user_role_."user_id" = ?)', + 'SELECT uidx."uuid",uidx."name",uidx."child_id",uidx."xyz",uidx."embedded_child2_id",uidx."nullable_value" FROM "uuid_entity" uidx WHERE (uidx."uuid" = ?)', + 'SELECT user_role_."user_id",user_role_."role_id" FROM "user_role_composite" user_role_ WHERE (user_role_."user_id" = ? AND user_role_."role_id" = ?)', 'SELECT challenge_."id",challenge_."token",challenge_."authentication_id",challenge_authentication_device_."NAME" AS authentication_device_NAME,challenge_authentication_device_."USER_ID" AS authentication_device_USER_ID,challenge_authentication_device_user_."NAME" AS authentication_device_user_NAME,challenge_authentication_."DESCRIPTION" AS authentication_DESCRIPTION,challenge_authentication_."DEVICE_ID" AS authentication_DEVICE_ID FROM "challenge" challenge_ INNER JOIN "AUTHENTICATION" challenge_authentication_ ON challenge_."authentication_id"=challenge_authentication_."ID" INNER JOIN "DEVICE" challenge_authentication_device_ ON challenge_authentication_."DEVICE_ID"=challenge_authentication_device_."ID" INNER JOIN "USER" challenge_authentication_device_user_ ON challenge_authentication_device_."USER_ID"=challenge_authentication_device_user_."ID" WHERE (challenge_."id" = ?)', - 'SELECT user_role_id_role_."id",user_role_id_role_."name" FROM "user_role_composite" user_role_ INNER JOIN "role_composite" user_role_id_role_ ON user_role_."id_role_id"=user_role_id_role_."id" WHERE (user_role_."id_user_id" = ?)', + 'SELECT user_role_id_role_."id",user_role_id_role_."name" FROM "user_role_composite" user_role_ INNER JOIN "role_composite" user_role_id_role_ ON user_role_."role_id"=user_role_id_role_."id" WHERE (user_role_."user_id" = ?)', 'SELECT meal_."mid",meal_."current_blood_glucose",meal_."created_on",meal_."updated_on",meal_."actual",meal_foods_."fid" AS foods_fid,meal_foods_."key" AS foods_key,meal_foods_."carbohydrates" AS foods_carbohydrates,meal_foods_."portion_grams" AS foods_portion_grams,meal_foods_."created_on" AS foods_created_on,meal_foods_."updated_on" AS foods_updated_on,meal_foods_."fk_meal_id" AS foods_fk_meal_id,meal_foods_."fk_alt_meal" AS foods_fk_alt_meal,meal_foods_."loooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name" AS ln,meal_foods_."fresh" AS foods_fresh FROM "meal" meal_ INNER JOIN "food" meal_foods_ ON meal_."mid"=meal_foods_."fk_meal_id" AND meal_foods_.fresh = \'Y\' WHERE (meal_."mid" = ? AND (meal_.actual = \'Y\'))' ] } @@ -592,8 +592,8 @@ interface MyRepository { ] query << [ 'INSERT INTO "Shipment1" ("field","sp_country","sp_city") VALUES (?,?,?)', - 'INSERT INTO "uuid_entity" ("name","child_id","xyz","embedded_child_embedded_child2_id","nullable_value","uuid") VALUES (?,?,?,?,?,?)', - 'INSERT INTO "user_role_composite" ("id_user_id","id_role_id") VALUES (?,?)' + 'INSERT INTO "uuid_entity" ("name","child_id","xyz","embedded_child2_id","nullable_value","uuid") VALUES (?,?,?,?,?,?)', + 'INSERT INTO "user_role_composite" ("user_id","role_id") VALUES (?,?)' ] } @@ -614,8 +614,8 @@ interface MyRepository { ] query << [ 'CREATE TABLE "Shipment1" ("sp_country" VARCHAR(255) NOT NULL,"sp_city" VARCHAR(255) NOT NULL,"field" VARCHAR(255) NOT NULL, PRIMARY KEY("sp_country","sp_city"));', - 'CREATE TABLE "uuid_entity" ("uuid" UUID,"name" VARCHAR(255) NOT NULL,"child_id" UUID,"xyz" UUID,"embedded_child_embedded_child2_id" UUID,"nullable_value" UUID, PRIMARY KEY("uuid"));', - 'CREATE TABLE "user_role_composite" ("id_user_id" BIGINT NOT NULL,"id_role_id" BIGINT NOT NULL, PRIMARY KEY("id_user_id","id_role_id"));' + 'CREATE TABLE "uuid_entity" ("uuid" UUID,"name" VARCHAR(255) NOT NULL,"child_id" UUID,"xyz" UUID,"embedded_child2_id" UUID,"nullable_value" UUID, PRIMARY KEY("uuid"));', + 'CREATE TABLE "user_role_composite" ("user_id" BIGINT NOT NULL,"role_id" BIGINT NOT NULL, PRIMARY KEY("user_id","role_id"));' ] } @@ -686,7 +686,7 @@ interface MyRepository { def q = encoder.buildQuery(AnnotationMetadata.EMPTY_METADATA, QueryModel.from(getRuntimePersistentEntity(Project)).idEq(new QueryParameter("projectId"))) then: - q.query == 'SELECT project_."project_id_department_id",project_."project_id_project_id",LOWER(project_.name) AS name,project_.name AS db_name,UPPER(project_.org) AS org FROM "project" project_ WHERE (project_."project_id_department_id" = ? AND project_."project_id_project_id" = ?)' + q.query == 'SELECT project_."department_id",project_."project_id",LOWER(project_.name) AS name,project_.name AS db_name,UPPER(project_.org) AS org FROM "project" project_ WHERE (project_."department_id" = ? AND project_."project_id" = ?)' q.parameters == [ '1': 'projectId.departmentId', '2': 'projectId.projectId' diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index c2e0d1626e8..55e51fdcfba 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -559,7 +559,7 @@ interface UserRoleRepository extends GenericRepository { def query = getQuery(method) expect: - query == 'SELECT user_role_id_role_.`id`,user_role_id_role_.`name` FROM `user_role_composite` user_role_ INNER JOIN `role_composite` user_role_id_role_ ON user_role_.`id_role_id`=user_role_id_role_.`id` WHERE (user_role_.`id_user_id` = ?)' + query == 'SELECT user_role_id_role_.`id`,user_role_id_role_.`name` FROM `user_role_composite` user_role_ INNER JOIN `role_composite` user_role_id_role_ ON user_role_.`role_id`=user_role_id_role_.`id` WHERE (user_role_.`user_id` = ?)' getParameterBindingIndexes(method) == ["0"] as String[] getParameterBindingPaths(method) == ["id"] as String[] getParameterPropertyPaths(method) == ["id.user.id"] as String[] @@ -1431,7 +1431,7 @@ interface UserRoleRepository extends GenericRepository { expect: countQuery == 'SELECT COUNT(*) FROM `user_role_composite` user_role_' - countDistinctQuery == 'SELECT COUNT(DISTINCT( CONCAT(user_role_.`id_user_id`,user_role_.`id_role_id`))) FROM `user_role_composite` user_role_' + countDistinctQuery == 'SELECT COUNT(DISTINCT( CONCAT(user_role_.`user_id`,user_role_.`role_id`))) FROM `user_role_composite` user_role_' } void "test escape query"() { @@ -1750,7 +1750,7 @@ class Zone { when: def update = repository.findPossibleMethods("update").findFirst().get() then: - getQuery(update) == "UPDATE `comp_settlement` SET `description`=?,`settlement_type_id`=?,`zone_id`=?,`is_enabled`=? WHERE (`code` = ? AND `code_id` = ? AND `id_county_id_id` = ? AND `id_county_id_state_id` = ?)" + getQuery(update) == "UPDATE `comp_settlement` SET `description`=?,`settlement_type_id`=?,`zone_id`=?,`is_enabled`=? WHERE (`code` = ? AND `code_id` = ? AND `county_id` = ? AND `county_state_id` = ?)" } void "test combined id"() { diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/ColumnTransformerSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/ColumnTransformerSpec.groovy index 32fedf93c3b..f558dd41fcc 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/ColumnTransformerSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/ColumnTransformerSpec.groovy @@ -65,7 +65,7 @@ class Project { def sql = builder.buildInsert(AnnotationMetadata.EMPTY_METADATA, entity).query expect: - sql == 'INSERT INTO "project" ("name","db_name","org","project_id_department_id","project_id_project_id") VALUES (UPPER(?),?,?,?,?)' + sql == 'INSERT INTO "project" ("name","db_name","org","department_id","project_id") VALUES (UPPER(?),?,?,?,?)' } @@ -86,7 +86,7 @@ class Project { def sql = builder.buildQuery(AnnotationMetadata.EMPTY_METADATA, QueryModel.from(entity)).query expect: - sql == 'SELECT project_."project_id_department_id",project_."project_id_project_id",LOWER(project_.name) AS name,project_.name AS db_name,UPPER(project_.org) AS org FROM "project" project_' + sql == 'SELECT project_."department_id",project_."project_id",LOWER(project_.name) AS name,project_.name AS db_name,UPPER(project_.org) AS org FROM "project" project_' } void "test build query with column reader in where"() { given: @@ -95,7 +95,7 @@ class Project { def sql = builder.buildQuery(AnnotationMetadata.EMPTY_METADATA, QueryModel.from(entity).eq("name", new QueryParameter("xyz"))).query expect: - sql == 'SELECT project_."project_id_department_id",project_."project_id_project_id",LOWER(project_.name) AS name,project_.name AS db_name,UPPER(project_.org) AS org FROM "project" project_ WHERE (project_."name" = UPPER(?))' + sql == 'SELECT project_."department_id",project_."project_id",LOWER(project_.name) AS name,project_.name AS db_name,UPPER(project_.org) AS org FROM "project" project_ WHERE (project_."name" = UPPER(?))' } void "test update query with column readers and writers"() { @@ -121,7 +121,7 @@ class Project { def sql = builder.buildInsert(AnnotationMetadata.EMPTY_METADATA, entity).query expect: - sql == 'INSERT INTO "transform" ("xyz","project_id_department_id","project_id_project_id") VALUES (LOWER(?),?,?)' + sql == 'INSERT INTO "transform" ("xyz","department_id","project_id") VALUES (LOWER(?),?,?)' } @@ -142,7 +142,7 @@ class Project { def sql = builder.buildQuery(AnnotationMetadata.EMPTY_METADATA, QueryModel.from(entity)).query expect: - sql == 'SELECT transform_."project_id_department_id",transform_."project_id_project_id",UPPER(xyz@abc) AS xyz FROM "transform" transform_' + sql == 'SELECT transform_."department_id",transform_."project_id",UPPER(xyz@abc) AS xyz FROM "transform" transform_' } void "test build query with column reader in where2"() { @@ -152,6 +152,6 @@ class Project { def sql = builder.buildQuery(AnnotationMetadata.EMPTY_METADATA, QueryModel.from(entity).eq("xyz", new QueryParameter("xyz"))).query expect: - sql == 'SELECT transform_."project_id_department_id",transform_."project_id_project_id",UPPER(xyz@abc) AS xyz FROM "transform" transform_ WHERE (transform_."xyz" = LOWER(?))' + sql == 'SELECT transform_."department_id",transform_."project_id",UPPER(xyz@abc) AS xyz FROM "transform" transform_ WHERE (transform_."xyz" = LOWER(?))' } } diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy index bdf0f880659..6d60cdbcbc7 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/CompositePrimaryKeySpec.groovy @@ -131,7 +131,7 @@ interface UserRoleRepository extends GenericRepository { def findRoleByUserMethod = repository.findPossibleMethods("findRoleByUser").findFirst().get() then: - getQuery(findRoleByUserMethod) == 'SELECT user_role_id_role_."id",user_role_id_role_."name" FROM "user_role" user_role_ INNER JOIN "role" user_role_id_role_ ON user_role_."id_role_id"=user_role_id_role_."id" WHERE (user_role_."id_user_id" = ?)' + getQuery(findRoleByUserMethod) == 'SELECT user_role_id_role_."id",user_role_id_role_."name" FROM "user_role" user_role_ INNER JOIN "role" user_role_id_role_ ON user_role_."role_id"=user_role_id_role_."id" WHERE (user_role_."user_id" = ?)' getParameterBindingIndexes(findRoleByUserMethod) == ["0"] getParameterPropertyPaths(findRoleByUserMethod) == ["id.user.id"] as String[] getParameterBindingPaths(findRoleByUserMethod) == ["id"] as String[] @@ -140,7 +140,7 @@ interface UserRoleRepository extends GenericRepository { def deleteByIdMethod = repository.findPossibleMethods("deleteById").findFirst().get() then: - getQuery(deleteByIdMethod) == 'DELETE FROM "user_role" WHERE ("id_user_id" = ? AND "id_role_id" = ?)' + getQuery(deleteByIdMethod) == 'DELETE FROM "user_role" WHERE ("user_id" = ? AND "role_id" = ?)' getParameterBindingIndexes(deleteByIdMethod) == ["0", "0"] getParameterPropertyPaths(deleteByIdMethod) == ["id.user.id", "id.role.id"] as String[] getParameterBindingPaths(deleteByIdMethod) == ["user", "role"] as String[] @@ -204,7 +204,7 @@ interface EntityWithIdClassRepository extends CrudRepository { expect: repository != null repository.getRequiredMethod("countByLikeIdImageIdentifier", UUID).stringValue(Query).get() == - 'SELECT COUNT(*) FROM "likes" like_ WHERE (like_."like_id_image_identifier" = ?)' + 'SELECT COUNT(*) FROM "likes" like_ WHERE (like_."image_identifier" = ?)' } void "test jdbc compile embedded id count query"() { @@ -303,7 +303,7 @@ interface LikeRepository extends CrudRepository { expect: repository != null repository.getRequiredMethod("deleteAll", Iterable).stringValue(Query).get() == - 'DELETE FROM "likes" WHERE ("like_id_image_identifier" = ? AND "like_id_user_identifier" = ?)' + 'DELETE FROM "likes" WHERE ("image_identifier" = ? AND "user_identifier" = ?)' } } diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java index d2d1a499b06..cdc0b639bf0 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java @@ -32,6 +32,7 @@ public class Restaurant { private final String name; @Relation(Relation.Kind.EMBEDDED) + @MappedProperty("address_") private final Address address; @Relation(Relation.Kind.EMBEDDED) From c688c3eed6a54d0d6ffb4e9dcd9f212feca389c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:50:58 +0000 Subject: [PATCH 04/26] chore(deps): update softprops/action-gh-release action to v2.2.2 (#3404) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62d3b00065f..e9dace49292 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,6 +160,6 @@ jobs: - name: Upload assets # Upload the artifacts to the existing release. Note that the SLSA provenance will # attest to each artifact file and not the aggregated ZIP file. - uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 with: files: artifacts.zip From 46fc27f3bee6812f98d2a95df00703a7a9010c50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 01:09:41 +0000 Subject: [PATCH 05/26] fix(deps): update dependency org.springframework.boot:spring-boot-gradle-plugin to v3.4.5 (#3408) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e1dd6952f4..444782e6a8a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ jmh = "1.37" ksp-gradle-plugin = "1.9.25-1.0.20" kotlin-gradle-plugin = "1.9.25" jmh-gradle-plugin = "0.7.3" -spring-boot-gradle-plugin = "3.4.4" +spring-boot-gradle-plugin = "3.4.5" spring-dependency-management-gradle-plugin = "1.1.7" shadow-gradle-plugin = "8.0.0" sonatype-scan = "3.0.0" From 2f35eac79f03d1d388b24d23d0a2bcc196b2f39f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 04:21:45 +0000 Subject: [PATCH 06/26] fix(deps): update spring data (#3406) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 444782e6a8a..465ab2bc540 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ groovy = "4.0.26" managed-javax-persistence = "2.2" -spring-data = "3.4.4" +spring-data = "3.4.5" # Lombok @@ -33,7 +33,7 @@ lombok = "1.18.38" # Testing and benchmarking -benchmark-spring-data = "2024.1.4" +benchmark-spring-data = "2024.1.5" spock = "2.2-groovy-4.0" testcontainers = "1.19.4" jmh = "1.37" From e647ccfe8ef8e9e2434cd2a397b833245ba83d40 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 29 Apr 2025 09:13:11 +0200 Subject: [PATCH 07/26] Don't use address prefix for embedded field --- .../data/model/query/builder/SqlQueryBuilderSpec.groovy | 6 +++--- .../io/micronaut/data/processor/sql/BuildQuerySpec.groovy | 8 ++++---- .../io/micronaut/data/processor/sql/BuildTableSpec.groovy | 2 +- .../micronaut/data/processor/sql/BuildUpdateSpec.groovy | 2 +- .../io/micronaut/data/processor/visitors/FindSpec.groovy | 4 ++-- .../java/io/micronaut/data/tck/entities/Restaurant.java | 1 - 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy index 5aa36fef95a..ce6deb1bfa2 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy @@ -169,7 +169,7 @@ interface MyRepository { def encoded = encoder.buildQuery(AnnotationMetadata.EMPTY_METADATA, q) expect: - encoded.query.startsWith('SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`address_street`,restaurant_.`address_zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM') + encoded.query.startsWith('SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`street`,restaurant_.`zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM') } @@ -390,7 +390,7 @@ interface MyRepository { def result = encoder.buildInsert(AnnotationMetadata.EMPTY_METADATA, entity) expect: - result.query == 'INSERT INTO "restaurant" ("name","address_street","address_zip_code","hqaddress_street","hqaddress_zip_code") VALUES (?,?,?,?,?)' + result.query == 'INSERT INTO "restaurant" ("name","street","zip_code","hqaddress_street","hqaddress_zip_code") VALUES (?,?,?,?,?)' result.parameters.equals('1': 'name', '2':'address.street', '3':'address.zipCode', '4':'hqAddress.street', '5':'hqAddress.zipCode') } @@ -401,7 +401,7 @@ interface MyRepository { def result = encoder.buildBatchCreateTableStatement(entity) expect: - result == 'CREATE TABLE "restaurant" ("id" BIGINT PRIMARY KEY AUTO_INCREMENT,"name" VARCHAR(255) NOT NULL,"address_street" VARCHAR(255) NOT NULL,"address_zip_code" VARCHAR(255) NOT NULL,"hqaddress_street" VARCHAR(255),"hqaddress_zip_code" VARCHAR(255));' + result == 'CREATE TABLE "restaurant" ("id" BIGINT PRIMARY KEY AUTO_INCREMENT,"name" VARCHAR(255) NOT NULL,"street" VARCHAR(255) NOT NULL,"zip_code" VARCHAR(255) NOT NULL,"hqaddress_street" VARCHAR(255),"hqaddress_zip_code" VARCHAR(255));' } void "test encode insert statement - custom mapping strategy"() { diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index 55e51fdcfba..422a6ac5a4a 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -902,10 +902,10 @@ interface RestaurantRepository extends GenericRepository { def findByAddressStreetQuery = getQuery(repository.getRequiredMethod("findByAddressStreet", String)) def getMaxAddressStreetByNameQuery = getQuery(repository.getRequiredMethod("getMaxAddressStreetByName", String)) expect: - findByNameQuery == 'SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`address_street`,restaurant_.`address_zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`name` = ?)' - saveQuery == 'INSERT INTO `restaurant` (`name`,`address_street`,`address_zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' - findByAddressStreetQuery == 'SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`address_street`,restaurant_.`address_zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`address_street` = ?)' - getMaxAddressStreetByNameQuery == 'SELECT MAX(restaurant_.`address_street`) FROM `restaurant` restaurant_ WHERE (restaurant_.`name` = ?)' + findByNameQuery == 'SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`street`,restaurant_.`zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`name` = ?)' + saveQuery == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' + findByAddressStreetQuery == 'SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`street`,restaurant_.`zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`street` = ?)' + getMaxAddressStreetByNameQuery == 'SELECT MAX(restaurant_.`street`) FROM `restaurant` restaurant_ WHERE (restaurant_.`name` = ?)' } void "test count query with joins"() { diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildTableSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildTableSpec.groovy index 2130199f722..1aa2d38cc96 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildTableSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildTableSpec.groovy @@ -38,7 +38,7 @@ class BuildTableSpec extends AbstractDataSpec { sql.contains("\"hqaddress_street\" VARCHAR(255),") and:"regular @Embedded does include NOT NULL declaration" - sql.contains("\"address_street\" VARCHAR(255) NOT NULL,") + sql.contains("\"street\" VARCHAR(255) NOT NULL,") } @Unroll diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy index 6c641687fdf..6b89f5bd3a0 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy @@ -300,7 +300,7 @@ interface CompanyRepository extends CrudRepository { // def insertQuery = method.stringValue(Query).get() expect: - updateQuery == 'UPDATE `restaurant` SET `name`=?,`address_street`=?,`address_zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' + updateQuery == 'UPDATE `restaurant` SET `name`=?,`street`=?,`zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' getParameterPropertyPaths(method) == ["name", "address.street", "address.zipCode", "hqAddress.street", "hqAddress.zipCode", "id"] as String[] } diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/FindSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/FindSpec.groovy index 2e3c771d2ac..70f9796bc49 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/FindSpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/FindSpec.groovy @@ -388,8 +388,8 @@ interface RestaurantRepository extends CrudRepository { .stringValue(Query).get() expect: "The query contains the correct embedded property name" - query1.contains('WHERE (restaurant_.`address_zip_code` LIKE ?') - query2.contains('WHERE (LOWER(restaurant_.`address_zip_code`) LIKE LOWER(?)') + query1.contains('WHERE (restaurant_.`zip_code` LIKE ?') + query2.contains('WHERE (LOWER(restaurant_.`zip_code`) LIKE LOWER(?)') } void "test top"() { diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java index cdc0b639bf0..d2d1a499b06 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/Restaurant.java @@ -32,7 +32,6 @@ public class Restaurant { private final String name; @Relation(Relation.Kind.EMBEDDED) - @MappedProperty("address_") private final Address address; @Relation(Relation.Kind.EMBEDDED) From e1c9833df58c10f958120169dff5913f6b4ec6fa Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 29 Apr 2025 10:50:30 +0200 Subject: [PATCH 08/26] Add one more example for embedded and composite id --- .../src/main/java/example/EmbeddedEntity.java | 68 +++++++++++++++++++ .../example/EmbeddedEntityRepository.java | 15 ++++ .../EmbeddedEntityCompositeIdSpec.java | 36 ++++++++++ 3 files changed, 119 insertions(+) create mode 100644 doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java create mode 100644 doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java create mode 100644 doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java diff --git a/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java b/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java new file mode 100644 index 00000000000..e7878ff2c65 --- /dev/null +++ b/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java @@ -0,0 +1,68 @@ +package example; + +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.Embeddable; +import io.micronaut.data.annotation.EmbeddedId; +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.Relation; +import io.micronaut.data.annotation.Relation.Kind; + +@MappedEntity("some_table") +public class EmbeddedEntity { + + @EmbeddedId + private PrimaryKey primaryKey; + + private String col; + + public PrimaryKey getPrimaryKey() { + return primaryKey; + } + + public void setPrimaryKey(PrimaryKey primaryKey) { + this.primaryKey = primaryKey; + } + + public String getCol() { + return col; + } + + public void setCol(String col) { + this.col = col; + } + + @Embeddable + public record PrimaryKey( + int someColumn, + @Relation(Kind.MANY_TO_ONE) OtherEntity otherEntity + ) {} + + @MappedEntity("other_table") + public static class OtherEntity { + + @Id + @AutoPopulated + @GeneratedValue + private Long id; + + private String someColumn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getSomeColumn() { + return someColumn; + } + + public void setSomeColumn(String someColumn) { + this.someColumn = someColumn; + } + } +} diff --git a/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java b/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java new file mode 100644 index 00000000000..9481e37faf9 --- /dev/null +++ b/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java @@ -0,0 +1,15 @@ +package example; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.GenericRepository; + +import java.util.List; + +@JdbcRepository(dialect = Dialect.H2) +public interface EmbeddedEntityRepository extends GenericRepository { + + EmbeddedEntity save(EmbeddedEntity entity); + + List findAll(); +} diff --git a/doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java b/doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java new file mode 100644 index 00000000000..8eb529c96fa --- /dev/null +++ b/doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java @@ -0,0 +1,36 @@ +package example; + +import io.micronaut.context.BeanContext; +import io.micronaut.data.annotation.Query; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +import jakarta.inject.Inject; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +@MicronautTest +class EmbeddedEntityCompositeIdSpec { + + + @Inject + private BeanContext beanContext; + + @Test + void testH2() { + var saveQuery = getQueryFor(EmbeddedEntityRepository.class, "save", EmbeddedEntity.class); + assertEquals("INSERT INTO `some_table` (`col`,`some_column`,`other_entity_id`) VALUES (?,?,?)", saveQuery); + + var loadAllQuery = getQueryFor(EmbeddedEntityRepository.class, "findAll"); + assertEquals("SELECT embedded_entity_.`some_column`,embedded_entity_.`other_entity_id`,embedded_entity_.`col` FROM `some_table` embedded_entity_", loadAllQuery); + } + + private String getQueryFor(Class> repository, String methodName, Class... argumentTypes) { + var definition = beanContext.getBeanDefinition(repository); + var method = definition.getRequiredMethod(methodName, argumentTypes); + return method.stringValue(Query.class).orElse(null); + } + +} From 64b115712de91d2173c5132cee2a55f0e322d3b9 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 29 Apr 2025 11:32:11 +0200 Subject: [PATCH 09/26] Disable gradle caching temporary --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a8787641be4..44c0ddc8531 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ developers=Graeme Rocher githubBranch=master -org.gradle.caching=true +org.gradle.caching=false org.gradle.daemon=false org.gradle.parallel=false org.gradle.jvmargs=-XX:MaxMetaspaceSize=2000m From 70f769febe896621b4ffd8541b5c4bbc64ad47a4 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 29 Apr 2025 12:28:00 +0200 Subject: [PATCH 10/26] Revert gradle caching --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 44c0ddc8531..a8787641be4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ developers=Graeme Rocher githubBranch=master -org.gradle.caching=false +org.gradle.caching=true org.gradle.daemon=false org.gradle.parallel=false org.gradle.jvmargs=-XX:MaxMetaspaceSize=2000m From c3c5883f77036c9239774ad3081a8e17b2517424 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 29 Apr 2025 12:53:29 +0200 Subject: [PATCH 11/26] Add more tests for repo --- .../groovy/io/micronaut/data/jdbc/h2/H2EmbeddedSpec.groovy | 6 ++++++ .../io/micronaut/data/jdbc/h2/H2RestaurantRepository.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedSpec.groovy index 4905c72cb1e..c3d76528f7b 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedSpec.groovy @@ -43,6 +43,12 @@ class H2EmbeddedSpec extends Specification { restaurant.address.street == 'Smith St.' restaurant.address.zipCode == '1234' + when:"Find restaurant by street name" + restaurant = restaurantRepository.findByAddressStreet("Smith St.").orElse(null) + then:"Found restaurant" + restaurant + restaurant.name == "Joe's Cafe" + when:"Max by embedded property" def maxStreet = restaurantRepository.getMaxAddressStreetByName("Fred's Cafe") def minStreet = restaurantRepository.getMinAddressStreetByName("Fred's Cafe") diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2RestaurantRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2RestaurantRepository.java index 4dc40100522..4b192c3a23e 100644 --- a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2RestaurantRepository.java +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2RestaurantRepository.java @@ -17,8 +17,13 @@ import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.entities.Restaurant; import io.micronaut.data.tck.repositories.RestaurantRepository; +import java.util.Optional; + @JdbcRepository(dialect = Dialect.H2) public interface H2RestaurantRepository extends RestaurantRepository { + + Optional findByAddressStreet(String street); } From e9d666b4d53106ad8a0fc7cd3e1b586674ba9fce Mon Sep 17 00:00:00 2001 From: radovanradic Date: Tue, 29 Apr 2025 13:59:03 +0200 Subject: [PATCH 12/26] Remove trigger builds from workflow --- .github/workflows/graalvm-latest.yml | 1 - .github/workflows/gradle.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/graalvm-latest.yml b/.github/workflows/graalvm-latest.yml index 3073d2c0bd4..e3053b0dc87 100644 --- a/.github/workflows/graalvm-latest.yml +++ b/.github/workflows/graalvm-latest.yml @@ -9,7 +9,6 @@ on: branches: - master - '[1-9]+.[0-9]+.x' - - embeddedid-colname pull_request: branches: - master diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 39a83b7c110..2327d05ea86 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -9,7 +9,6 @@ on: branches: - master - '[1-9]+.[0-9]+.x' - - embeddedid-colname pull_request: branches: - master From 03cfb5162fbe94f8e9143c51d710e4017dc73f9f Mon Sep 17 00:00:00 2001 From: Radovan Radic Date: Wed, 30 Apr 2025 17:15:42 +0200 Subject: [PATCH 13/26] Fix issue with CursoredPageable and @EmbeddedId (#3411) * Use all embedded properties to sort cursored pageable * Use PersistentPropertyPath for cursored pageable * Trigger build to see if changes will pass tests * Add test for the changes and remove trigger build * Sonar fixes --- .../data/jdbc/h2/H2EmbeddedIdSpec.groovy | 48 +++++++++++++++++-- .../data/jdbc/h2/ShipmentRepository.java | 4 ++ .../internal/sql/DefaultSqlPreparedQuery.java | 40 +++++++++------- 3 files changed, 72 insertions(+), 20 deletions(-) diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedIdSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedIdSpec.groovy index 935658f6652..21564ef6077 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedIdSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2EmbeddedIdSpec.groovy @@ -18,6 +18,8 @@ package io.micronaut.data.jdbc.h2 import io.micronaut.core.annotation.Introspected import io.micronaut.data.annotation.* import io.micronaut.data.jdbc.annotation.JdbcRepository +import io.micronaut.data.model.CursoredPage +import io.micronaut.data.model.CursoredPageable import io.micronaut.data.model.Page import io.micronaut.data.model.Pageable import io.micronaut.data.model.Sort @@ -137,7 +139,7 @@ class H2EmbeddedIdSpec extends Specification { foundAllOrderByCountryCityDesc[1].field == "test4" when: - def foundAllOrderByDynamic = repository.findAll(Sort.of(Sort.Order.desc("country"), Sort.Order.asc( "city"))) + def foundAllOrderByDynamic = repository.findAll(Sort.of(Sort.Order.desc("shipmentId.country"), Sort.Order.asc( "shipmentId.city"))) then: foundAllOrderByDynamic.size() == 2 @@ -178,13 +180,51 @@ class H2EmbeddedIdSpec extends Specification { ShipmentId id4 = new ShipmentId("g", "h") repository.save(new Shipment(id4, "test4")) - Sort.Order.Direction sortDirection = Sort.Order.Direction.ASC; - Pageable pageable = Pageable.UNPAGED.order(new Sort.Order("shipmentId.city", sortDirection, false)); - def page = repository.findAll(pageable) + Sort.Order.Direction sortDirection = Sort.Order.Direction.ASC; + Pageable pageable = Pageable.UNPAGED.order(new Sort.Order("shipmentId.city", sortDirection, false)); + def page = repository.findAll(pageable) then: page.totalSize == 4 page.content[0].shipmentId.city == "b" + + cleanup: + repository.deleteAll() + } + + void "test cursored pageable"() { + when: + ShipmentId id = new ShipmentId("c1", "a") + repository.save(new Shipment(id, "test")) + + ShipmentId id2 = new ShipmentId("c1", "b") + repository.save(new Shipment(id2, "test2")) + + ShipmentId id3 = new ShipmentId("c1", "c") + repository.save(new Shipment(id3, "test3")) + + ShipmentId id4 = new ShipmentId("c1", "d") + repository.save(new Shipment(id4, "test4")) + + ShipmentId id5 = new ShipmentId("c2", "a1") + repository.save(new Shipment(id5, "test5")) + + CursoredPageable cursoredPageable = CursoredPageable.from(3, Sort.of()); + CursoredPage page = repository.findByShipmentIdCountry("c1", cursoredPageable) + + then: + page.content.size() == 3 + page.hasNext() + + when: + page = repository.findByShipmentIdCountry("c1", page.nextPageable()) + + then: + page.content.size() == 1 + !page.hasNext() + + cleanup: + repository.deleteAll() } } diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/ShipmentRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/ShipmentRepository.java index bd9157a1706..4f20199b3a6 100644 --- a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/ShipmentRepository.java +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/ShipmentRepository.java @@ -16,6 +16,8 @@ package io.micronaut.data.jdbc.h2; import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.CursoredPage; +import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.repository.PageableRepository; import io.micronaut.data.tck.entities.Shipment; import io.micronaut.data.tck.entities.ShipmentId; @@ -33,4 +35,6 @@ public interface ShipmentRepository extends PageableRepository findAllOrderByShipmentIdCityDesc(); List findAllOrderByShipmentIdCountryAndShipmentIdCityDesc(); + + CursoredPage findByShipmentIdCountry(String country, CursoredPageable pageable); } diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java index bb7eeda349b..a86ec4bf699 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlPreparedQuery.java @@ -20,15 +20,19 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; import io.micronaut.data.annotation.TypeRole; import io.micronaut.data.exceptions.DataAccessException; +import io.micronaut.data.model.Association; import io.micronaut.data.model.CursoredPageable; import io.micronaut.data.model.DataType; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Pageable.Cursor; import io.micronaut.data.model.Pageable.Mode; import io.micronaut.data.model.PersistentEntity; +import io.micronaut.data.model.PersistentEntityUtils; import io.micronaut.data.model.PersistentProperty; +import io.micronaut.data.model.PersistentPropertyPath; import io.micronaut.data.model.Sort; import io.micronaut.data.model.Sort.Order; import io.micronaut.data.model.query.builder.AbstractSqlLikeQueryBuilder; @@ -65,7 +69,7 @@ public class DefaultSqlPreparedQuery extends DefaultBindableParametersPreparedQuery implements SqlPreparedQuery, DelegatePreparedQuery { protected List cursorQueryBindings; - protected List> cursorProperties; + protected List cursorProperties; protected final SqlStoredQuery sqlStoredQuery; protected String query; private final boolean bindPageableOrSort; @@ -222,10 +226,14 @@ public static Sort enhanceCursoredSort(Sort sort, boolean isBackwards, Persisten // sorting on the rows. Therefore, we make sure id is present in it. List orders = new ArrayList<>(sort.getOrderBy()); for (PersistentProperty idProperty: persistentEntity.getIdentityProperties()) { - String name = idProperty.getName(); - if (orders.stream().noneMatch(o -> o.getProperty().equals(name))) { - orders.add(Order.asc(name)); - } + PersistentEntityUtils.traversePersistentProperties(idProperty, (associations, property) -> { + String prefix = String.join(".", associations.stream().map(Association::getName).toList()); + String propertyName = property.getName(); + String name = StringUtils.isEmpty(prefix) ? propertyName : prefix + "." + propertyName; + if (orders.stream().noneMatch(o -> o.getProperty().equals(name))) { + orders.add(Order.asc(name)); + } + }); } sort = Sort.of(orders); if (isBackwards) { @@ -303,7 +311,7 @@ private static Sort reverseSort(Sort sort) { @NonNull private String buildCursorPagination(@NonNull CursoredPageable cursoredPageable, int paramIndex, @Nullable String tableAlias) { RuntimePersistentEntity persistentEntity = (RuntimePersistentEntity) getPersistentEntity(); - List> cursorProperties = getCursorProperties(cursoredPageable, persistentEntity); + List cursorPersistentPropertyPaths = getCursorProperties(cursoredPageable, persistentEntity); Optional optionalCursor = cursoredPageable.cursor(); if (optionalCursor.isEmpty()) { return ""; @@ -321,7 +329,7 @@ private String buildCursorPagination(@NonNull CursoredPageable cursoredPageable, cursorQueryBindings = new ArrayList<>(orders.size() * (orders.size() + 1) / 2); for (int i = 0; i < orders.size(); ++i) { cursorBindings.add(new CursoredQueryParameterBinder( - "cursor_" + i, cursorProperties.get(i).getDataType(), cursor.get(i) + "cursor_" + i, cursorPersistentPropertyPaths.get(i).getProperty().getDataType(), cursor.get(i) )); } @@ -359,14 +367,14 @@ private String buildCursorPagination(@NonNull CursoredPageable cursoredPageable, return builder.toString(); } - private List> getCursorProperties(CursoredPageable cursoredPageable, RuntimePersistentEntity persistentEntity) { + private List getCursorProperties(CursoredPageable cursoredPageable, RuntimePersistentEntity persistentEntity) { // Create a sort for the cursored pagination. The sort must produce a unique // sorting on the rows. Therefore, we make sure id is present in it. if (cursorProperties == null) { Sort sort = cursoredPageable.getSort(); cursorProperties = new ArrayList<>(sort.getOrderBy().size()); for (Order order : sort.getOrderBy()) { - cursorProperties.add(persistentEntity.getPropertyByName(order.getProperty())); + cursorProperties.add(persistentEntity.getPropertyPath(order.getProperty())); } } return cursorProperties; @@ -410,20 +418,20 @@ public List createCursors(List results, Pageable pageable, Runti Collections.reverse(results); } CursoredPageable cursoredPageable = enhancePageable((CursoredPageable) pageable, runtimePersistentEntity); - List> cursorProperties = getCursorProperties(cursoredPageable, runtimePersistentEntity); + List cursorPersistentPropertyPaths = getCursorProperties(cursoredPageable, runtimePersistentEntity); List cursors = new ArrayList<>(results.size()); boolean isDto = preparedQuery.isDtoProjection(); for (Object result : results) { - List cursorElements = new ArrayList<>(cursorProperties.size()); - for (RuntimePersistentProperty property : cursorProperties) { + List cursorElements = new ArrayList<>(cursorPersistentPropertyPaths.size()); + for (PersistentPropertyPath property : cursorPersistentPropertyPaths) { if (isDto) { - RuntimePersistentProperty dtoProperty = runtimePersistentEntity.getPropertyByName(property.getName()); + PersistentPropertyPath dtoProperty = runtimePersistentEntity.getPropertyPath(property.getPath()); if (dtoProperty == null) { - throw new IllegalStateException("DTO projection " + runtimePersistentEntity + " must contain property " + property.getName()); + throw new IllegalStateException("DTO projection " + runtimePersistentEntity + " must contain property " + property.getPath()); } - cursorElements.add(dtoProperty.getProperty().get(result)); + cursorElements.add(dtoProperty.getPropertyValue(result)); } else { - cursorElements.add(property.getProperty().get(result)); + cursorElements.add(property.getPropertyValue(result)); } } cursors.add(Cursor.of(cursorElements)); From 4ad478cfab8b2e5461d0e85e761355936f994226 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 30 Apr 2025 17:22:37 +0200 Subject: [PATCH 14/26] Improve the Oracle client info feature. (#3405) 1. Fix an exception that could occur if class/module names were too long. 2. Propagate the Java thread name in CLIENT_INFO if none is set from annotation metadata. 3. Add a useful utility query to the documentation. --- .../OracleClientInfoConnectionCustomizer.java | 47 ++++++++++++------- .../guide/dbc/jdbc/jdbcConfiguration.adoc | 36 +++++++++----- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java b/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java index 3f542e13cd7..a7df41c9779 100644 --- a/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java +++ b/data-connection-jdbc/src/main/java/io/micronaut/data/connection/jdbc/oracle/OracleClientInfoConnectionCustomizer.java @@ -25,6 +25,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.data.connection.ConnectionDefinition; @@ -67,27 +68,21 @@ final class OracleClientInfoConnectionCustomizer implements ConnectionCustomizer private static final String VALUE_MEMBER = "value"; private static final String INTERCEPTED_SUFFIX = "$Intercepted"; - /** - * Constant for the Oracle connection client info client ID property name. - */ private static final String ORACLE_CLIENT_ID = "OCSID.CLIENTID"; - /** - * Constant for the Oracle connection client info module property name. - */ private static final String ORACLE_MODULE = "OCSID.MODULE"; - /** - * Constant for the Oracle connection client info action property name. - */ private static final String ORACLE_ACTION = "OCSID.ACTION"; - /** - * Constant for the Oracle connection database product name. - */ + private static final String ORACLE_CLIENT_INFO = "OCSID.CLIENT_INFO"; private static final String ORACLE_CONNECTION_DATABASE_PRODUCT_NAME = "Oracle"; private static final Logger LOG = LoggerFactory.getLogger(OracleClientInfoConnectionCustomizer.class); private static final Map, String> MODULE_CLASS_MAP = new ConcurrentHashMap<>(100); + // The driver is supposed to expose this via DataBaseMetadata.getClientInfoProperties() but the Oracle driver + // doesn't do so as of release 23.7.0.25.1, so we hard-code it here. This bug is being fixed so a future + // release will supply the correct information. + private static final int MAX_VALUE_LENGTH = 64; + @Nullable private final String applicationName; @@ -105,6 +100,20 @@ final class OracleClientInfoConnectionCustomizer implements ConnectionCustomizer } } + private static String truncate(String name, String value) { + if (value.length() > MAX_VALUE_LENGTH) { + LOG.trace("Truncating client info value '{}' for {} as it is longer than {} chars", value, name, MAX_VALUE_LENGTH); + return value.substring(0, MAX_VALUE_LENGTH); + } else { + return value; + } + } + + private static String preprocessClassName(Class clazz) { + // Oracle imposes a limit of 64 chars on class names, and we can easily blow through that. + return NameUtils.getShortenedName(clazz.getName().replace(INTERCEPTED_SUFFIX, "")); + } + @Override public Function, R> intercept(Function, R> operation) { return connectionStatus -> { @@ -118,22 +127,21 @@ public Function, R> intercept(Function connectionStatus, @NonNull Map connectionClientInfo) { if (CollectionUtils.isNotEmpty(connectionClientInfo)) { Connection connection = connectionStatus.getConnection(); - LOG.trace("Setting connection tracing info to the Oracle connection"); try { for (Map.Entry additionalInfo : connectionClientInfo.entrySet()) { String name = additionalInfo.getKey(); - String value = additionalInfo.getValue(); + // Oracle imposes a limit of 64 chars on class names, and we can easily blow through that. + String value = truncate(name, additionalInfo.getValue()); connection.setClientInfo(name, value); } } catch (SQLClientInfoException e) { - LOG.debug("Failed to set connection tracing info", e); + LOG.warn("Failed to set connection tracing info: {}", connectionClientInfo, e); } } } @@ -191,11 +199,14 @@ private boolean isOracleConnection(Connection connection) { } if (annotationMetadata instanceof MethodInvocationContext methodInvocationContext) { clientInfoAttributes.putIfAbsent(ORACLE_MODULE, - MODULE_CLASS_MAP.computeIfAbsent(methodInvocationContext.getTarget().getClass(), - clazz -> clazz.getName().replace(INTERCEPTED_SUFFIX, "")) + MODULE_CLASS_MAP.computeIfAbsent( + methodInvocationContext.getTarget().getClass(), + OracleClientInfoConnectionCustomizer::preprocessClassName + ) ); clientInfoAttributes.putIfAbsent(ORACLE_ACTION, methodInvocationContext.getName()); } + clientInfoAttributes.putIfAbsent(ORACLE_CLIENT_INFO, Thread.currentThread().getName()); return clientInfoAttributes; } } diff --git a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc index ad5e3e0acae..5e3edb52dd3 100644 --- a/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc +++ b/src/main/docs/guide/dbc/jdbc/jdbcConfiguration.adoc @@ -61,22 +61,36 @@ As seen in the configuration above you should also configure the dialect. Althou IMPORTANT: The dialect setting in configuration does *not* replace the need to ensure the correct dialect is set at the repository. If the dialect is H2 in configuration, the repository should have `@JdbcRepository(dialect = Dialect.H2)` / `@R2dbcRepository(dialect = Dialect.H2)`. Because repositories are computed at compile time, the configuration value is not known at that time. -=== Connection client info tracing +=== Tracing calls from Java to Oracle Database sessions -In order to trace SQL calls using `java.sql.Connection.setClientInfo(String, String)` method, you can -annotate a repository with the ann:data.connection.annotation.ClientInfo[] annotation or ann:data.connection.annotation.ClientInfo.Attribute[] for individual client info. +Specify the configuration property `datasources..enable-oracle-client-info=true` on a per datasource basis to link database sessions with Java thread activity. By default, this will set: -Note that the ann:data.connection.annotation.ClientInfo.Attribute[] annotation can be used on either the class or the method, thus allowing customization of the module or action individually. +- The value of `micronaut.application.name` as the `CLIENTID`. +- The class/interface name of the repository as the `MODULE`, potentially with the package name shortened. +- The method name from the repository as the `ACTION`. +- The JVM thread name as the `CLIENT_INFO`. -For Oracle database, following attributes can be set to the connection client info: `OCSID.MODULE`, `OCSID.ACTION` and `OCSID.CLIENTID` and provided in ann:data.connection.annotation.ClientInfo.Attribute[]. -If some of these attributes are not provided then Micronaut Data Jdbc is going to populate values automatically for Oracle connections: +This metadata appears in the `V$SESSION` view and can be used to figure out what a thread is doing inside the database. The following query is useful to see what SQL is executing: -*** `OCSID.MODULE` will get the value of the class name where annotation `@ClientInfo.Attribute` is added (usually Micronaut Data repository class) -*** `OCSID.ACTION` will get the value of the method name which is annotated with `@ClientInfo.Attribute` annotation -*** `OCSID.CLIENTID` will get the value of the Micronaut application name, if configured +[source,sql] +---- +SELECT + s.sid, + s.serial#, + s.client_identifier as app_name, + s.client_info as thread_name, + s.module as repository_name, + s.action as repository_method, + q.sql_text, + s.event, + s.wait_time, + s.seconds_in_wait +FROM v$session s +LEFT JOIN v$sql q ON s.sql_id = q.sql_id +WHERE s.username = USER +---- -Please note this feature is currently supported only for Oracle database connections. In order to enable Oracle JDBC connection client info to be set, -you need to specify the configuration property `datasources..enable-oracle-client-info=true` on a per datasource basis. +You can control the values of `ACTION`, `MODULE`, `CLIENTID` and `CLIENT_INFO` with the ann:data.connection.annotation.ClientInfo[] annotation on repositories. Note that the ann:data.connection.annotation.ClientInfo.Attribute[] annotation can be used on either the class or the method, thus allowing customization of the module or action individually. You should prefix the key with `OCSID.` e.g. `OCSID.ACTION` when setting client info this way. Oracle imposes a maximum length of 64 characters for these values, therefore thread, class and method names may be shortened or truncated to fit. TIP: See the guide for https://guides.micronaut.io/latest/micronaut-data-jdbc-repository.html[Access a Database with Micronaut Data JDBC] to learn more. From 70b14f54956df2790e03f7fb9367b8f0d585bcbc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 01:11:47 +0000 Subject: [PATCH 15/26] fix(deps): update dependency io.micronaut:micronaut-core-bom to v4.8.13 (#3413) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 465ab2bc540..e213b77211b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -micronaut = "4.8.12" +micronaut = "4.8.13" micronaut-platform = "4.7.6" micronaut-docs = "2.0.0" micronaut-gradle-plugin = "4.4.5" From c5e46468e905496542ee1d1baab103feb0fec584 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 01:06:28 +0000 Subject: [PATCH 16/26] fix(deps): update dependency io.micronaut.coherence:micronaut-coherence-bom to v5.0.6 (#3414) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e213b77211b..4b98cca2a2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ micronaut-multitenancy = "5.5.0" micronaut-validation = "4.9.0" micronaut-logging = "1.6.1" micronaut-flyway = "7.6.1" -micronaut-coherence = "5.0.5" +micronaut-coherence = "5.0.6" groovy = "4.0.26" From 22695c2619eb9c1cb405b017406b6a0ff1f762d3 Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 5 May 2025 06:56:10 +0000 Subject: [PATCH 17/26] [skip ci] Release v4.12.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a8787641be4..91f473cb23e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.12.1-SNAPSHOT +projectVersion=4.12.1 projectGroupId=io.micronaut.data title=Micronaut Data From c771071a7f038024a709c2272e38189064bf545e Mon Sep 17 00:00:00 2001 From: micronaut-build Date: Mon, 5 May 2025 07:04:14 +0000 Subject: [PATCH 18/26] chore: Bump version to 4.12.2-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 91f473cb23e..629ceeb1625 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=4.12.1 +projectVersion=4.12.2-SNAPSHOT projectGroupId=io.micronaut.data title=Micronaut Data From 65e10b690fa321e7d48d11ad833b324ff4816f39 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Fri, 7 Nov 2025 15:16:57 +0100 Subject: [PATCH 19/26] Fix tests --- .../jdbc-example-kotlin/src/main/kotlin/example/Client.kt | 1 + .../src/test/resources/embedded-relations.sql | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/Client.kt b/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/Client.kt index cc7441d7514..72bf5931440 100644 --- a/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/Client.kt +++ b/doc-examples/jdbc-example-kotlin/src/main/kotlin/example/Client.kt @@ -8,6 +8,7 @@ data class Client( @field:Id @GeneratedValue val id: Long? = null, val name: String, @Relation(value = Relation.Kind.EMBEDDED) + @MappedProperty(value = "relationship") val relationship: Relationship, @DateCreated diff --git a/doc-examples/jdbc-example-kotlin/src/test/resources/embedded-relations.sql b/doc-examples/jdbc-example-kotlin/src/test/resources/embedded-relations.sql index 4b613ef1f76..8d89869b323 100644 --- a/doc-examples/jdbc-example-kotlin/src/test/resources/embedded-relations.sql +++ b/doc-examples/jdbc-example-kotlin/src/test/resources/embedded-relations.sql @@ -6,7 +6,7 @@ create table sample_entity ( id bigint primary key not null, name text, example text, - part_text text + text text ); create table relationship_status From f58c848eeb09dc626ef568572b3243e1fc9e02e8 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Fri, 7 Nov 2025 16:14:05 +0100 Subject: [PATCH 20/26] Revert formatting changes. --- .../EmbeddedAssociationJoinSpec.groovy | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy index 53bb5ffbc1a..e0abb4d5ab6 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/embeddedAssociation/EmbeddedAssociationJoinSpec.groovy @@ -58,85 +58,85 @@ class EmbeddedAssociationJoinSpec extends Specification implements H2TestPropert void 'test one-to-one update'() { given: - ChildEntity child = new ChildEntity(name: "child") - MainEntity main = new MainEntity(name: "test") - main.child = child - child.main = main + ChildEntity child = new ChildEntity(name: "child") + MainEntity main = new MainEntity(name: "test") + main.child = child + child.main = main when: - mainEntityRepository.save(main) - main.name = "diff-name" - child.name = "diff-child" - MainEntity updatedMain = mainEntityRepository.update(main) + mainEntityRepository.save(main) + main.name = "diff-name" + child.name = "diff-child" + MainEntity updatedMain = mainEntityRepository.update(main) then: - updatedMain.name == "diff-name" - updatedMain.child.name == "diff-child" + updatedMain.name == "diff-name" + updatedMain.child.name == "diff-child" } void 'test many-to-many hierarchy'() { given: - MainEntity e = new MainEntity(name: "test", - assoc: [ - new MainEntityAssociation(name: "A"), - new MainEntityAssociation(name: "B"), - ], em: new MainEmbedded( - assoc: [ - new MainEntityAssociation(name: "C"), - new MainEntityAssociation(name: "D"), - ] - )) + MainEntity e = new MainEntity(name: "test", + assoc: [ + new MainEntityAssociation(name: "A"), + new MainEntityAssociation(name: "B"), + ], em: new MainEmbedded( + assoc: [ + new MainEntityAssociation(name: "C"), + new MainEntityAssociation(name: "D"), + ] + )) when: - mainEntityRepository.save(e) - e = mainEntityRepository.findById(e.id).get() - Sort.Order.Direction sortDirection = Sort.Order.Direction.ASC; - Pageable pageable = Pageable.UNPAGED.order(new Sort.Order("child.name", sortDirection, false)); - mainEntityRepository.findAll(pageable).totalPages == 1 - PredicateSpecification predicate = null - mainEntityRepository.findAllByCriteria(predicate, pageable).totalPages == 1 + mainEntityRepository.save(e) + e = mainEntityRepository.findById(e.id).get() + Sort.Order.Direction sortDirection = Sort.Order.Direction.ASC; + Pageable pageable = Pageable.UNPAGED.order(new Sort.Order("child.name", sortDirection, false)); + mainEntityRepository.findAll(pageable).totalPages == 1 + PredicateSpecification predicate = null + mainEntityRepository.findAllByCriteria(predicate, pageable).totalPages == 1 then: - e.id - e.assoc.size() == 2 - e.assoc[0].name == "A" - e.assoc[1].name == "B" - e.em - e.em.assoc.size() == 2 - e.em.assoc[0].name == "C" - e.em.assoc[1].name == "D" + e.id + e.assoc.size() == 2 + e.assoc[0].name == "A" + e.assoc[1].name == "B" + e.em + e.em.assoc.size() == 2 + e.em.assoc[0].name == "C" + e.em.assoc[1].name == "D" when: - mainEntityRepository.update(e) - e = mainEntityRepository.findById(e.id).get() + mainEntityRepository.update(e) + e = mainEntityRepository.findById(e.id).get() then: - e.id - e.assoc.size() == 2 - e.assoc[0].name == "A" - e.assoc[1].name == "B" - e.em.assoc.size() == 2 - e.em.assoc[0].name == "C" - e.em.assoc[1].name == "D" + e.id + e.assoc.size() == 2 + e.assoc[0].name == "A" + e.assoc[1].name == "B" + e.em.assoc.size() == 2 + e.em.assoc[0].name == "C" + e.em.assoc[1].name == "D" when: - def o = new OneMainEntity(one: e) - o = oneMainEntityRepository.save(o) - o = oneMainEntityRepository.findById(o.id).get() + def o = new OneMainEntity(one: e) + o = oneMainEntityRepository.save(o) + o = oneMainEntityRepository.findById(o.id).get() then: - o.one.id - o.one.assoc.size() == 2 - o.one.assoc[0].name == "A" - o.one.assoc[1].name == "B" - o.one.em.assoc.size() == 2 - o.one.em.assoc[0].name == "C" - o.one.em.assoc[1].name == "D" + o.one.id + o.one.assoc.size() == 2 + o.one.assoc[0].name == "A" + o.one.assoc[1].name == "B" + o.one.em.assoc.size() == 2 + o.one.em.assoc[0].name == "C" + o.one.em.assoc[1].name == "D" when: - def oem = new OneMainEntityEm(id: new EmId(one: e), name: "Embedded is crazy") - oem = oneMainEntityEmRepository.save(oem) - oem = oneMainEntityEmRepository.findById(oem.id).get() + def oem = new OneMainEntityEm(id: new EmId(one: e), name: "Embedded is crazy") + oem = oneMainEntityEmRepository.save(oem) + oem = oneMainEntityEmRepository.findById(oem.id).get() then: - oem.name == "Embedded is crazy" - oem.id.one.id - oem.id.one.assoc.size() == 2 - oem.id.one.assoc[0].name == "A" - oem.id.one.assoc[1].name == "B" - oem.id.one.em.assoc.size() == 2 - oem.id.one.em.assoc[0].name == "C" - oem.id.one.em.assoc[1].name == "D" + oem.name == "Embedded is crazy" + oem.id.one.id + oem.id.one.assoc.size() == 2 + oem.id.one.assoc[0].name == "A" + oem.id.one.assoc[1].name == "B" + oem.id.one.em.assoc.size() == 2 + oem.id.one.em.assoc[0].name == "C" + oem.id.one.em.assoc[1].name == "D" } void 'embedded with generated values are saved'() { From 70cc61f3b83d4dbcf146e10b10d288ca34543aea Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 9 Nov 2025 11:15:23 +0100 Subject: [PATCH 21/26] Example for EmbeddedId naming strategy change --- .../data/processor/sql/BuildQuerySpec.groovy | 35 +++++++++++++++++++ .../data/processor/entity/OtherEntity.java | 15 ++++++++ .../data/processor/entity/SomeEntity.java | 21 +++++++++++ 3 files changed, 71 insertions(+) create mode 100644 data-processor/src/test/java/io/micronaut/data/processor/entity/OtherEntity.java create mode 100644 data-processor/src/test/java/io/micronaut/data/processor/entity/SomeEntity.java diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index 128d55ff4a6..1d8997b13b1 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -27,6 +27,7 @@ import io.micronaut.data.model.entities.Invoice import io.micronaut.data.model.query.builder.sql.Dialect import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder import io.micronaut.data.processor.entity.ActivityPeriodEntity +import io.micronaut.data.processor.entity.SomeEntity import io.micronaut.data.processor.visitors.AbstractDataSpec import io.micronaut.data.runtime.criteria.RuntimeCriteriaBuilder import io.micronaut.data.tck.entities.Author @@ -2305,4 +2306,38 @@ interface ProductRepository extends GenericRepository { getExtendedStockOperationsMethod.classValue(DataMethod, "interceptor").get() == FindAllInterceptor selectCustomStringMethod.classValue(DataMethod, "interceptor").get() == FindOneInterceptor } + + void "test EmbeddedId naming strategy"() { + given: + def repository = buildRepository('test.SomeEntityRepository', """ +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.data.processor.entity.SomeEntity; +import jakarta.persistence.Embeddable; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import java.util.Optional; + +@JdbcRepository(dialect = Dialect.H2) +interface SomeEntityRepository extends GenericRepository { + + Optional findById(SomeEntity.PrimaryKey id); + + SomeEntity save(SomeEntity entity); + + List findAll(); +} + +""") + + def findByIdQuery = getQuery(repository.getRequiredMethod("findById", SomeEntity.PrimaryKey)) + def saveQuery = getQuery(repository.getRequiredMethod("save", SomeEntity)) + def findAllQuery = getQuery(repository.getRequiredMethod("findAll")) + expect: + findByIdQuery == 'SELECT some_entity_.`some_column`,some_entity_.`other_entity_id`,some_entity_.`col` FROM `some_table` some_entity_ WHERE (some_entity_.`some_column` = ? AND some_entity_.`other_entity_id` = ?)' + saveQuery == 'INSERT INTO `some_table` (`col`,`some_column`,`other_entity_id`) VALUES (?,?,?)' + findAllQuery == 'SELECT some_entity_.`some_column`,some_entity_.`other_entity_id`,some_entity_.`col` FROM `some_table` some_entity_' + } } diff --git a/data-processor/src/test/java/io/micronaut/data/processor/entity/OtherEntity.java b/data-processor/src/test/java/io/micronaut/data/processor/entity/OtherEntity.java new file mode 100644 index 00000000000..c2806690d26 --- /dev/null +++ b/data-processor/src/test/java/io/micronaut/data/processor/entity/OtherEntity.java @@ -0,0 +1,15 @@ +package io.micronaut.data.processor.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity(name = "other_table") +@Table(name = "other_table") +public record OtherEntity( + @Id + @GeneratedValue + Long id, + String someColumn) { +} diff --git a/data-processor/src/test/java/io/micronaut/data/processor/entity/SomeEntity.java b/data-processor/src/test/java/io/micronaut/data/processor/entity/SomeEntity.java new file mode 100644 index 00000000000..8d1f5823029 --- /dev/null +++ b/data-processor/src/test/java/io/micronaut/data/processor/entity/SomeEntity.java @@ -0,0 +1,21 @@ +package io.micronaut.data.processor.entity; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity(name = "some_table") +@Table(name = "some_table") +public record SomeEntity(@EmbeddedId + PrimaryKey primaryKey, + String col) { + + @Embeddable + public record PrimaryKey( + int someColumn, + @ManyToOne OtherEntity otherEntity + ) { + } +} From 55d5da7e153aa1f50e6427610011c2798d2ef02c Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 9 Nov 2025 11:31:45 +0100 Subject: [PATCH 22/26] Keep backward compatible EmbeddedId naming --- .../jdbc/h2/composite/CompositeSpec.groovy | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy index 36aeea12401..fc367414f3f 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy @@ -344,7 +344,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { def statements = encoder.buildCreateTableStatements(builder.runtimeEntityRegistry.getEntity(Settlement)) then: - statements.join("\n") == 'CREATE TABLE "comp_settlement" ("code" VARCHAR(255) NOT NULL,"code_id" INT NOT NULL,"county_id" INT NOT NULL,"county_state_id" INT NOT NULL,"description" VARCHAR(255) NOT NULL,"settlement_type_id" BIGINT NOT NULL,"zone_id" BIGINT NOT NULL,"is_enabled" BOOLEAN NOT NULL, PRIMARY KEY("code","code_id","county_id","county_state_id"));' + statements.join("\n") == 'CREATE TABLE "comp_settlement" ("code" VARCHAR(255) NOT NULL,"code_id" INT NOT NULL,"id_county_id_id" INT NOT NULL,"id_county_id_state_id" INT NOT NULL,"description" VARCHAR(255) NOT NULL,"settlement_type_id" BIGINT NOT NULL,"zone_id" BIGINT NOT NULL,"is_enabled" BOOLEAN NOT NULL, PRIMARY KEY("code","code_id","id_county_id_id","id_county_id_state_id"));' } void "test build create Citizen"() { @@ -363,7 +363,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { def res = builder.createCriteriaInsert(Settlement).build(new SqlQueryBuilder()) then: - res.query == 'INSERT INTO "comp_settlement" ("description","settlement_type_id","zone_id","is_enabled","code","code_id","county_id","county_state_id") VALUES (?,?,?,?,?,?,?,?)' + res.query == 'INSERT INTO "comp_settlement" ("description","settlement_type_id","zone_id","is_enabled","code","code_id","id_county_id_id","id_county_id_state_id") VALUES (?,?,?,?,?,?,?,?)' res.parameters == [ '1': 'description', '2': 'settlementType.id', @@ -386,7 +386,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { def res = query.build(new SqlQueryBuilder()) then: - res.query == 'UPDATE "comp_settlement" SET "code"=?,"code_id"=?,"county_id"=?,"county_state_id"=?,"description"=?,"settlement_type_id"=?,"zone_id"=?,"is_enabled"=? WHERE ("code" = ? AND "code_id" = ? AND "county_id" = ? AND "county_state_id" = ?)' + res.query == 'UPDATE "comp_settlement" SET "code"=?,"code_id"=?,"id_county_id_id"=?,"id_county_id_state_id"=?,"description"=?,"settlement_type_id"=?,"zone_id"=?,"is_enabled"=? WHERE ("code" = ? AND "code_id" = ? AND "id_county_id_id" = ? AND "id_county_id_state_id" = ?)' res.parameters == [ '1': 'id.code', '2': 'id.codeId', @@ -409,7 +409,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { def root = query.from(Settlement) def q = query.where(builder.equal(root.id(), builder.parameter(SettlementPk))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."county_id",settlement_."county_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled" FROM "comp_settlement" settlement_ WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."county_id" = ? AND settlement_."county_state_id" = ?)' + q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."id_county_id_id",settlement_."id_county_id_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled" FROM "comp_settlement" settlement_ WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."id_county_id_id" = ? AND settlement_."id_county_id_state_id" = ?)' q.parameters == [ '1': 'id.code', '2': 'id.codeId', @@ -424,7 +424,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { def root = query.from(Settlement) def q = query.where(builder.equal(root.id(), new SettlementPk(code: "Kode", codeId: 123))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."county_id",settlement_."county_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled" FROM "comp_settlement" settlement_ WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."county_id" = ? AND settlement_."county_state_id" = ?)' + q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."id_county_id_id",settlement_."id_county_id_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled" FROM "comp_settlement" settlement_ WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."id_county_id_id" = ? AND settlement_."id_county_id_state_id" = ?)' q.parameters == [ '1': 'id.code', '2': 'id.codeId', @@ -443,7 +443,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { root.join("zone", Join.Type.FETCH) def q = query.where(builder.equal(root.id(), builder.parameter(Object))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."county_id",settlement_."county_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."county_id" = ? AND settlement_."county_state_id" = ?)' + q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."id_county_id_id",settlement_."id_county_id_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."id_county_id_id" = ? AND settlement_."id_county_id_state_id" = ?)' q.parameters == [ '1': 'id.code', '2': 'id.codeId', @@ -460,7 +460,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { root.fetch("zone") def q = query.where(builder.equal(root.id(), builder.parameter(Object))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."county_id",settlement_."county_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."county_id" = ? AND settlement_."county_state_id" = ?)' + q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."id_county_id_id",settlement_."id_county_id_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."id_county_id_id" = ? AND settlement_."id_county_id_state_id" = ?)' q.parameters == [ '1': 'id.code', '2': 'id.codeId', @@ -478,7 +478,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { root.join("id.county", Join.Type.FETCH) def q = query.where(builder.equal(root.id(), builder.parameter(Object))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."county_id",settlement_."county_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_id_county_."county_name" AS id_county_county_name,settlement_id_county_."is_enabled" AS id_county_is_enabled,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_country" settlement_id_county_ ON settlement_."county_id"=settlement_id_county_."id" AND settlement_."county_state_id"=settlement_id_county_."state_id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."county_id" = ? AND settlement_."county_state_id" = ?)' + q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."id_county_id_id",settlement_."id_county_id_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_id_county_."county_name" AS id_county_county_name,settlement_id_county_."is_enabled" AS id_county_is_enabled,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_country" settlement_id_county_ ON settlement_."id_county_id_id"=settlement_id_county_."id" AND settlement_."id_county_id_state_id"=settlement_id_county_."state_id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."id_county_id_id" = ? AND settlement_."id_county_id_state_id" = ?)' q.parameters == [ '1': 'id.code', '2': 'id.codeId', @@ -496,7 +496,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { root.fetch("id.county") def q = query.where(builder.equal(root.id(), builder.parameter(Object))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."county_id",settlement_."county_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_id_county_."county_name" AS id_county_county_name,settlement_id_county_."is_enabled" AS id_county_is_enabled,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_country" settlement_id_county_ ON settlement_."county_id"=settlement_id_county_."id" AND settlement_."county_state_id"=settlement_id_county_."state_id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."county_id" = ? AND settlement_."county_state_id" = ?)' + q.query == 'SELECT settlement_."code",settlement_."code_id",settlement_."id_county_id_id",settlement_."id_county_id_state_id",settlement_."description",settlement_."settlement_type_id",settlement_."zone_id",settlement_."is_enabled",settlement_settlement_type_."name" AS settlement_type_name,settlement_id_county_."county_name" AS id_county_county_name,settlement_id_county_."is_enabled" AS id_county_is_enabled,settlement_zone_."name" AS zone_name FROM "comp_settlement" settlement_ INNER JOIN "comp_zone" settlement_zone_ ON settlement_."zone_id"=settlement_zone_."id" INNER JOIN "comp_country" settlement_id_county_ ON settlement_."id_county_id_id"=settlement_id_county_."id" AND settlement_."id_county_id_state_id"=settlement_id_county_."state_id" INNER JOIN "comp_sett_type" settlement_settlement_type_ ON settlement_."settlement_type_id"=settlement_settlement_type_."id" WHERE (settlement_."code" = ? AND settlement_."code_id" = ? AND settlement_."id_county_id_id" = ? AND settlement_."id_county_id_state_id" = ?)' q.parameters == [ '1': 'id.code', '2': 'id.codeId', @@ -512,7 +512,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { root.join("settlements", Join.Type.FETCH) def q = query.where(builder.equal(root.id(), builder.parameter(Object))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT citizen_."id",citizen_."name",citizen_settlements_."code" AS settlements_code,citizen_settlements_."code_id" AS settlements_code_id,citizen_settlements_."county_id" AS settlements_county_id,citizen_settlements_."county_state_id" AS settlements_county_state_id,citizen_settlements_."description" AS settlements_description,citizen_settlements_."settlement_type_id" AS settlements_settlement_type_id,citizen_settlements_."zone_id" AS settlements_zone_id,citizen_settlements_."is_enabled" AS settlements_is_enabled FROM "comp_citizen" citizen_ INNER JOIN "citizen_settlement" citizen_settlements_citizen_settlement_ ON citizen_."id"=citizen_settlements_citizen_settlement_."citizen_id" INNER JOIN "comp_settlement" citizen_settlements_ ON citizen_settlements_citizen_settlement_."settlement_id_code"=citizen_settlements_."code" AND citizen_settlements_citizen_settlement_."settlement_id_code_id"=citizen_settlements_."code_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_id"=citizen_settlements_."county_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_state_id"=citizen_settlements_."county_state_id" WHERE (citizen_."id" = ?)' + q.query == 'SELECT citizen_."id",citizen_."name",citizen_settlements_."code" AS settlements_code,citizen_settlements_."code_id" AS settlements_code_id,citizen_settlements_."id_county_id_id" AS settlements_id_county_id_id,citizen_settlements_."id_county_id_state_id" AS settlements_id_county_id_state_id,citizen_settlements_."description" AS settlements_description,citizen_settlements_."settlement_type_id" AS settlements_settlement_type_id,citizen_settlements_."zone_id" AS settlements_zone_id,citizen_settlements_."is_enabled" AS settlements_is_enabled FROM "comp_citizen" citizen_ INNER JOIN "citizen_settlement" citizen_settlements_citizen_settlement_ ON citizen_."id"=citizen_settlements_citizen_settlement_."citizen_id" INNER JOIN "comp_settlement" citizen_settlements_ ON citizen_settlements_citizen_settlement_."settlement_id_code"=citizen_settlements_."code" AND citizen_settlements_citizen_settlement_."settlement_id_code_id"=citizen_settlements_."code_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_id"=citizen_settlements_."id_county_id_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_state_id"=citizen_settlements_."id_county_id_state_id" WHERE (citizen_."id" = ?)' q.parameters == [ '1': 'id' ] @@ -525,7 +525,7 @@ class CompositeSpec extends Specification implements H2TestPropertyProvider { root.fetch("settlements") def q = query.where(builder.equal(root.id(), builder.parameter(Object))).build(new SqlQueryBuilder()) then: - q.query == 'SELECT citizen_."id",citizen_."name",citizen_settlements_."code" AS settlements_code,citizen_settlements_."code_id" AS settlements_code_id,citizen_settlements_."county_id" AS settlements_county_id,citizen_settlements_."county_state_id" AS settlements_county_state_id,citizen_settlements_."description" AS settlements_description,citizen_settlements_."settlement_type_id" AS settlements_settlement_type_id,citizen_settlements_."zone_id" AS settlements_zone_id,citizen_settlements_."is_enabled" AS settlements_is_enabled FROM "comp_citizen" citizen_ INNER JOIN "citizen_settlement" citizen_settlements_citizen_settlement_ ON citizen_."id"=citizen_settlements_citizen_settlement_."citizen_id" INNER JOIN "comp_settlement" citizen_settlements_ ON citizen_settlements_citizen_settlement_."settlement_id_code"=citizen_settlements_."code" AND citizen_settlements_citizen_settlement_."settlement_id_code_id"=citizen_settlements_."code_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_id"=citizen_settlements_."county_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_state_id"=citizen_settlements_."county_state_id" WHERE (citizen_."id" = ?)' + q.query == 'SELECT citizen_."id",citizen_."name",citizen_settlements_."code" AS settlements_code,citizen_settlements_."code_id" AS settlements_code_id,citizen_settlements_."id_county_id_id" AS settlements_id_county_id_id,citizen_settlements_."id_county_id_state_id" AS settlements_id_county_id_state_id,citizen_settlements_."description" AS settlements_description,citizen_settlements_."settlement_type_id" AS settlements_settlement_type_id,citizen_settlements_."zone_id" AS settlements_zone_id,citizen_settlements_."is_enabled" AS settlements_is_enabled FROM "comp_citizen" citizen_ INNER JOIN "citizen_settlement" citizen_settlements_citizen_settlement_ ON citizen_."id"=citizen_settlements_citizen_settlement_."citizen_id" INNER JOIN "comp_settlement" citizen_settlements_ ON citizen_settlements_citizen_settlement_."settlement_id_code"=citizen_settlements_."code" AND citizen_settlements_citizen_settlement_."settlement_id_code_id"=citizen_settlements_."code_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_id"=citizen_settlements_."id_county_id_id" AND citizen_settlements_citizen_settlement_."settlement_id_county_id_state_id"=citizen_settlements_."id_county_id_state_id" WHERE (citizen_."id" = ?)' q.parameters == [ '1': 'id' ] @@ -588,7 +588,7 @@ class State { @Embeddable class CountyPk { - @MappedProperty(value = "id") + @MappedProperty("id") Integer id @MappedProperty(value = "state_id") @Relation(Relation.Kind.MANY_TO_ONE) @@ -598,6 +598,7 @@ class CountyPk { @MappedEntity("comp_country") class County { @EmbeddedId + @MappedProperty("id") CountyPk id @MappedProperty String countyName @@ -620,6 +621,7 @@ class SettlementPk { @MappedEntity("comp_settlement") class Settlement { @EmbeddedId + @MappedProperty("id") SettlementPk id @MappedProperty String description From 884b52188c61e630fff57eb8f9f2e305ce1615fd Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 9 Nov 2025 16:20:16 +0100 Subject: [PATCH 23/26] Add breaking changes documentation about composite id naming (@EmbeddedId) --- src/main/docs/guide/breaks.adoc | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/main/docs/guide/breaks.adoc b/src/main/docs/guide/breaks.adoc index 77116fcf433..4878cd6b277 100644 --- a/src/main/docs/guide/breaks.adoc +++ b/src/main/docs/guide/breaks.adoc @@ -37,4 +37,42 @@ The new implementation has a newly added connection management, allowing to shar Async and reactive repositories are no longer throw `EmptyResultException` if the entity is not found. +== 5.0.0 +=== Composite Id Naming Strategy + +Before Micronaut Data 5.0.0 version, for classes using composite key using `@EmbeddedId` annotation +Micronaut Data would generate column names using field name annotated with `@EmbeddedId` annotation. + +For such entity class: + +snippet::example.Project[project-base="doc-examples/jdbc-example", source="main"] + +and composite id class: + +snippet::example.ProjectId[project-base="doc-examples/jdbc-example", source="main"] + +create table before Micronaut Data 5.0.0 SQL script would look like this: + +[source,sql] +---- +CREATE TABLE `project` (`project_id_department_id` INT NOT NULL,`project_id_project_id` INT NOT NULL,`name` VARCHAR(255) NOT NULL, PRIMARY KEY(`project_id_department_id`,`project_id_project_id`)); +---- + +which is not correct since `project_id_` prefix is not really needed. After 5.0.0, create table SQL script will look like this: + +[source,sql] +---- +CREATE TABLE `project` (`department_id` INT NOT NULL,`project_id` INT NOT NULL,`name` VARCHAR(255) NOT NULL, PRIMARY KEY(`department_id`,`project_id`)); +---- + +If you have such tables in your system, after 5.0.0 you might need to annotate your composite id like this: + +[source,java] +---- +@EmbeddedId +@MappedProperty("project_id_") +private ProjectId projectId; +---- + +which will generate column names to be backward compatible. From c9ef288aa5a5611dfdf7e7585aec22c8a21bb295 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 9 Nov 2025 17:26:19 +0100 Subject: [PATCH 24/26] Updated breaking changes documentation and tests. --- .../document/tck/entities/Restaurant.java | 5 + .../jdbc/h2/composite/CompositeSpec.groovy | 2 +- .../document/mongodb/MongoEmbeddedSpec.groovy | 18 ++- .../data/processor/sql/BuildQuerySpec.groovy | 4 +- src/main/docs/guide/breaks.adoc | 111 +++++++++++++++++- 5 files changed, 132 insertions(+), 8 deletions(-) diff --git a/data-document-tck/src/main/java/io/micronaut/data/document/tck/entities/Restaurant.java b/data-document-tck/src/main/java/io/micronaut/data/document/tck/entities/Restaurant.java index db353793e11..16ac22d6f04 100644 --- a/data-document-tck/src/main/java/io/micronaut/data/document/tck/entities/Restaurant.java +++ b/data-document-tck/src/main/java/io/micronaut/data/document/tck/entities/Restaurant.java @@ -38,8 +38,13 @@ public class Restaurant { private Address hqAddress; public Restaurant(String name, Address address) { + this(name, address, null); + } + + public Restaurant(String name, Address address, Address hqAddress) { this.name = name; this.address = address; + this.hqAddress = hqAddress; } public String getId() { diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy index fc367414f3f..3657bee88cf 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy @@ -588,7 +588,7 @@ class State { @Embeddable class CountyPk { - @MappedProperty("id") + @MappedProperty(value = "id") Integer id @MappedProperty(value = "state_id") @Relation(Relation.Kind.MANY_TO_ONE) diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec.groovy index 83e0d83a5f2..ea087b429ec 100644 --- a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec.groovy +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/MongoEmbeddedSpec.groovy @@ -37,14 +37,16 @@ class MongoEmbeddedSpec extends Specification implements MongoTestPropertyProvid void "test save and retrieve entity with embedded"() { when:"An entity is saved" - restaurantRepository.save(new Restaurant("Fred's Cafe", new Address("High St.", "7896"))) - def restaurant = restaurantRepository.save(new Restaurant("Joe's Cafe", new Address("Smith St.", "1234"))) + def firstRestaurant = restaurantRepository.save(new Restaurant("Fred's Cafe", new Address("High St.", "7896"))) + def restaurant = restaurantRepository.save(new Restaurant("Joe's Cafe", new Address("Smith St.", "1234"), new Address("5th Boulevard", "1235"))) then:"The entity was saved" restaurant restaurant.id restaurant.address.street == 'Smith St.' restaurant.address.zipCode == '1234' + restaurant.hqAddress.street == '5th Boulevard' + restaurant.hqAddress.zipCode == '1235' when:"The entity is retrieved" restaurant = restaurantRepository.findById(restaurant.id).orElse(null) @@ -53,7 +55,17 @@ class MongoEmbeddedSpec extends Specification implements MongoTestPropertyProvid restaurant.id restaurant.address.street == 'Smith St.' restaurant.address.zipCode == '1234' - restaurant.hqAddress == null + restaurant.hqAddress.street == '5th Boulevard' + restaurant.hqAddress.zipCode == '1235' + + when:"First restaurant is retrieved" + def loadedFirstRestaurant = restaurantRepository.findById(firstRestaurant.id).orElse(null) + + then:"The embedded is populated correctly for first restaurant" + loadedFirstRestaurant.id == firstRestaurant.id + loadedFirstRestaurant.address.street == firstRestaurant.address.street + loadedFirstRestaurant.address.zipCode == firstRestaurant.address.zipCode + !loadedFirstRestaurant.hqAddress when:"The object is updated with non-null value" restaurant.hqAddress = new Address("John St.", "4567") diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index 1d8997b13b1..fb0992ca364 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -1710,6 +1710,7 @@ class CountyPk { @MappedEntity("comp_country") class County { @EmbeddedId + @MappedProperty(value = "id") CountyPk id; @MappedProperty String countyName; @@ -1734,6 +1735,7 @@ class SettlementPk { @MappedEntity("comp_settlement") class Settlement { @EmbeddedId + @MappedProperty(value = "id") SettlementPk id; @MappedProperty String description; @@ -1769,7 +1771,7 @@ class Zone { when: def update = repository.findPossibleMethods("update").findFirst().get() then: - getQuery(update) == "UPDATE `comp_settlement` SET `description`=?,`settlement_type_id`=?,`zone_id`=?,`is_enabled`=? WHERE (`code` = ? AND `code_id` = ? AND `county_id` = ? AND `county_state_id` = ?)" + getQuery(update) == "UPDATE `comp_settlement` SET `description`=?,`settlement_type_id`=?,`zone_id`=?,`is_enabled`=? WHERE (`code` = ? AND `code_id` = ? AND `id_county_id_id` = ? AND `id_county_id_state_id` = ?)" } void "test combined id"() { diff --git a/src/main/docs/guide/breaks.adoc b/src/main/docs/guide/breaks.adoc index 4878cd6b277..13e59a019b5 100644 --- a/src/main/docs/guide/breaks.adoc +++ b/src/main/docs/guide/breaks.adoc @@ -39,7 +39,7 @@ Async and reactive repositories are no longer throw `EmptyResultException` if th == 5.0.0 -=== Composite Id Naming Strategy +=== Embedded Fields Naming Strategy Before Micronaut Data 5.0.0 version, for classes using composite key using `@EmbeddedId` annotation Micronaut Data would generate column names using field name annotated with `@EmbeddedId` annotation. @@ -56,14 +56,24 @@ create table before Micronaut Data 5.0.0 SQL script would look like this: [source,sql] ---- -CREATE TABLE `project` (`project_id_department_id` INT NOT NULL,`project_id_project_id` INT NOT NULL,`name` VARCHAR(255) NOT NULL, PRIMARY KEY(`project_id_department_id`,`project_id_project_id`)); +CREATE TABLE `project` ( + `project_id_department_id` INT NOT NULL, + `project_id_project_id` INT NOT NULL, + `name` VARCHAR(255) NOT NULL, + PRIMARY KEY(`project_id_department_id`,`project_id_project_id`) +); ---- which is not correct since `project_id_` prefix is not really needed. After 5.0.0, create table SQL script will look like this: [source,sql] ---- -CREATE TABLE `project` (`department_id` INT NOT NULL,`project_id` INT NOT NULL,`name` VARCHAR(255) NOT NULL, PRIMARY KEY(`department_id`,`project_id`)); +CREATE TABLE `project` ( + `department_id` INT NOT NULL, + `project_id` INT NOT NULL, + `name` VARCHAR(255) NOT NULL, + PRIMARY KEY(`department_id`,`project_id`) +); ---- If you have such tables in your system, after 5.0.0 you might need to annotate your composite id like this: @@ -76,3 +86,98 @@ private ProjectId projectId; ---- which will generate column names to be backward compatible. + +Similar, for given mapped entity with embedded fields: + +[source,java] +---- +@MappedEntity +public record Restaurant( + @GeneratedValue + @Id + Long id, + String name, + @Relation(Relation.Kind.EMBEDDED) + Address address, + @Relation(Relation.Kind.EMBEDDED) + @Nullable + Address hqAddress +) { +} + +@Embeddable +public record Address( + String street, + String zipCode +) { +} +---- + +before Micronaut Data 5.0.0 create table SQL script would look like this: + +[source,sql] +---- +CREATE TABLE `restaurant` ( + `id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `address_street` VARCHAR(255) NOT NULL, + `address_zip_code` VARCHAR(255) NOT NULL, + `hq_address_street` VARCHAR(255), + `hq_address_zip_code` VARCHAR(255) +); +---- + +And to keep behavior backward compatible then you would need to map `Restaurant` entity like this: + +[source,java] +---- +@MappedEntity +public record Restaurant( + @GeneratedValue + @Id + Long id, + String name, + @Relation(Relation.Kind.EMBEDDED) + @MappedProperty("address") + Address address, + @Relation(Relation.Kind.EMBEDDED) + @MappedProperty("hq_address") + @Nullable + Address hqAddress +) { +} +---- + +Suggested entity after Micronaut Data 5.0.0 should look like this: + +[source,java] +---- +@MappedEntity +public record Restaurant( + @GeneratedValue + @Id + Long id, + String name, + @Relation(Relation.Kind.EMBEDDED) + Address address, + @Relation(Relation.Kind.EMBEDDED) + @MappedProperty("hq_address") + @Nullable + Address hqAddress +) { +} +---- + +in order to make distinction between two embedded fields of the same type. This entity would generate create table SQL script: + +[source,sql] +---- +CREATE TABLE `restaurant` ( + `id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `street` VARCHAR(255) NOT NULL, + `zip_code` VARCHAR(255) NOT NULL, + `hq_address_street` VARCHAR(255), + `hq_address_zip_code` VARCHAR(255) +); +---- From 0d08c3afb103f79cda230f2ac45b20295e988b1f Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 9 Nov 2025 17:32:57 +0100 Subject: [PATCH 25/26] Remove duplicated test example --- .../src/main/java/example/EmbeddedEntity.java | 68 ------------------- .../example/EmbeddedEntityRepository.java | 15 ---- .../EmbeddedEntityCompositeIdSpec.java | 36 ---------- 3 files changed, 119 deletions(-) delete mode 100644 doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java delete mode 100644 doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java delete mode 100644 doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java diff --git a/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java b/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java deleted file mode 100644 index e7878ff2c65..00000000000 --- a/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntity.java +++ /dev/null @@ -1,68 +0,0 @@ -package example; - -import io.micronaut.data.annotation.AutoPopulated; -import io.micronaut.data.annotation.Embeddable; -import io.micronaut.data.annotation.EmbeddedId; -import io.micronaut.data.annotation.GeneratedValue; -import io.micronaut.data.annotation.Id; -import io.micronaut.data.annotation.MappedEntity; -import io.micronaut.data.annotation.Relation; -import io.micronaut.data.annotation.Relation.Kind; - -@MappedEntity("some_table") -public class EmbeddedEntity { - - @EmbeddedId - private PrimaryKey primaryKey; - - private String col; - - public PrimaryKey getPrimaryKey() { - return primaryKey; - } - - public void setPrimaryKey(PrimaryKey primaryKey) { - this.primaryKey = primaryKey; - } - - public String getCol() { - return col; - } - - public void setCol(String col) { - this.col = col; - } - - @Embeddable - public record PrimaryKey( - int someColumn, - @Relation(Kind.MANY_TO_ONE) OtherEntity otherEntity - ) {} - - @MappedEntity("other_table") - public static class OtherEntity { - - @Id - @AutoPopulated - @GeneratedValue - private Long id; - - private String someColumn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getSomeColumn() { - return someColumn; - } - - public void setSomeColumn(String someColumn) { - this.someColumn = someColumn; - } - } -} diff --git a/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java b/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java deleted file mode 100644 index 9481e37faf9..00000000000 --- a/doc-examples/jdbc-example-java/src/main/java/example/EmbeddedEntityRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package example; - -import io.micronaut.data.jdbc.annotation.JdbcRepository; -import io.micronaut.data.model.query.builder.sql.Dialect; -import io.micronaut.data.repository.GenericRepository; - -import java.util.List; - -@JdbcRepository(dialect = Dialect.H2) -public interface EmbeddedEntityRepository extends GenericRepository { - - EmbeddedEntity save(EmbeddedEntity entity); - - List findAll(); -} diff --git a/doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java b/doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java deleted file mode 100644 index 8eb529c96fa..00000000000 --- a/doc-examples/jdbc-example-java/src/test/java/example/EmbeddedEntityCompositeIdSpec.java +++ /dev/null @@ -1,36 +0,0 @@ -package example; - -import io.micronaut.context.BeanContext; -import io.micronaut.data.annotation.Query; -import io.micronaut.data.repository.GenericRepository; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import org.junit.jupiter.api.Test; - -import jakarta.inject.Inject; - -import static org.junit.jupiter.api.Assertions.assertEquals; - - -@MicronautTest -class EmbeddedEntityCompositeIdSpec { - - - @Inject - private BeanContext beanContext; - - @Test - void testH2() { - var saveQuery = getQueryFor(EmbeddedEntityRepository.class, "save", EmbeddedEntity.class); - assertEquals("INSERT INTO `some_table` (`col`,`some_column`,`other_entity_id`) VALUES (?,?,?)", saveQuery); - - var loadAllQuery = getQueryFor(EmbeddedEntityRepository.class, "findAll"); - assertEquals("SELECT embedded_entity_.`some_column`,embedded_entity_.`other_entity_id`,embedded_entity_.`col` FROM `some_table` embedded_entity_", loadAllQuery); - } - - private String getQueryFor(Class> repository, String methodName, Class... argumentTypes) { - var definition = beanContext.getBeanDefinition(repository); - var method = definition.getRequiredMethod(methodName, argumentTypes); - return method.stringValue(Query.class).orElse(null); - } - -} From 3883beddeaf610bd6478e218dc967212455ac58f Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 22 Dec 2025 12:40:40 +0100 Subject: [PATCH 26/26] Fix jakarta data test in data-processor --- .../sql/JakartaDataBuildQuerySpec.groovy | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/JakartaDataBuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/JakartaDataBuildQuerySpec.groovy index 9a1b940d3d1..25a2e9e3b20 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/JakartaDataBuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/JakartaDataBuildQuerySpec.groovy @@ -53,9 +53,9 @@ interface RestaurantRepoSave { def saveAllArray = repository.getRequiredMethod("customSaveArray", Restaurant[]) then: - getQuery(saveOne) == 'INSERT INTO `restaurant` (`name`,`address_street`,`address_zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' - getQuery(saveAllList) == 'INSERT INTO `restaurant` (`name`,`address_street`,`address_zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' - getQuery(saveAllArray) == 'INSERT INTO `restaurant` (`name`,`address_street`,`address_zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' + getQuery(saveOne) == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' + getQuery(saveAllList) == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' + getQuery(saveAllArray) == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' } void "test Jakarta Data @Insert on repository without base interface (single, list, array)"() { @@ -89,9 +89,9 @@ interface RestaurantRepoInsert { def insertAllArray = repository.getRequiredMethod("customInsertArray", Restaurant[]) then: - getQuery(insertOne) == 'INSERT INTO `restaurant` (`name`,`address_street`,`address_zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' - getQuery(insertAllList) == 'INSERT INTO `restaurant` (`name`,`address_street`,`address_zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' - getQuery(insertAllArray) == 'INSERT INTO `restaurant` (`name`,`address_street`,`address_zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' + getQuery(insertOne) == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' + getQuery(insertAllList) == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' + getQuery(insertAllArray) == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)' } void "test Jakarta Data @Update on repository without base interface (single, list, array)"() { @@ -125,9 +125,9 @@ interface RestaurantRepoUpdate { def updateAllArray = repository.getRequiredMethod("customUpdateArray", Restaurant[]) then: - getQuery(updateOne) == 'UPDATE `restaurant` SET `name`=?,`address_street`=?,`address_zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' - getQuery(updateAllList) == 'UPDATE `restaurant` SET `name`=?,`address_street`=?,`address_zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' - getQuery(updateAllArray) == 'UPDATE `restaurant` SET `name`=?,`address_street`=?,`address_zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' + getQuery(updateOne) == 'UPDATE `restaurant` SET `name`=?,`street`=?,`zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' + getQuery(updateAllList) == 'UPDATE `restaurant` SET `name`=?,`street`=?,`zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' + getQuery(updateAllArray) == 'UPDATE `restaurant` SET `name`=?,`street`=?,`zip_code`=?,`hqaddress_street`=?,`hqaddress_zip_code`=? WHERE (`id` = ?)' } void "test Jakarta Data @Delete on repository without base interface (single, list, array)"() {