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 212fa663894..60dd27ac041 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 @@ -41,8 +41,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; } @Nullable 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/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/composite/CompositeSpec.groovy index ed6fc23520f..b0a345c363c 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 @@ -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 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 50afe84ef5e..25cf92969cf 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 @@ -65,7 +65,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"() { @@ -74,7 +74,7 @@ class CustomEmbeddedNameMapping extends Specification implements H2TestPropertyP def res = builder.createCriteriaInsert(MyBook).build(new SqlQueryBuilder()) then: - res.query == 'INSERT INTO "MyBook" ("authorFirstName","authorLastName","authorDetailsIncludedNumberAge","id") VALUES (?,?,?,?)' + res.query == 'INSERT INTO "MyBook" ("firstName","lastName","numberAge","id") VALUES (?,?,?,?)' } void "test update"() { @@ -89,7 +89,7 @@ class CustomEmbeddedNameMapping extends Specification implements H2TestPropertyP def res = query.build(new SqlQueryBuilder()) 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 query.where(builder.equal(root.id(), builder.parameter(Object))) def q = query.build(new SqlQueryBuilder()) 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-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); } 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 6b299bb8154..9c6ab906ccf 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 @@ -158,11 +158,13 @@ default String mappedName(List associations, PersistentProperty p 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-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/model/query/builder/SqlQueryBuilderSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/model/query/builder/SqlQueryBuilderSpec.groovy index 59c310c48d1..02112f79ade 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 @@ -233,7 +233,7 @@ interface MyRepository { def encoded = criteriaQuery.build(encoder) 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') } void "test h2 crud"() { @@ -469,7 +469,7 @@ interface MyRepository { def result = builder.createCriteriaInsert(Restaurant).build(new SqlQueryBuilder()) 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') } @@ -480,7 +480,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"() { @@ -525,8 +525,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 (?,?)' ] } @@ -547,8 +547,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"));' ] } @@ -621,7 +621,7 @@ interface MyRepository { def q = query.where(builder.equal(root.id(), builder.parameter(Object))).build(encoder) 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 32b556093c2..14404b7e54b 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 @@ -554,7 +555,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[] @@ -1003,10 +1004,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"() { @@ -1284,7 +1285,7 @@ interface UserRoleRepository extends GenericRepository { def countDistinctQuery = getQuery(repository.getRequiredMethod("countDistinct")) 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 many-to-one with properties starting with the same prefix"() { @@ -1557,7 +1558,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"() { @@ -1817,6 +1818,7 @@ class CountyPk { @MappedEntity("comp_country") class County { @EmbeddedId + @MappedProperty(value = "id") CountyPk id; @MappedProperty String countyName; @@ -1841,6 +1843,7 @@ class SettlementPk { @MappedEntity("comp_settlement") class Settlement { @EmbeddedId + @MappedProperty(value = "id") SettlementPk id; @MappedProperty String description; @@ -2412,7 +2415,7 @@ interface RestaurantRepository extends GenericRepository { def method = repository.getRequiredMethod("find", String) expect: - getQuery(method) == '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` = ?)' + getQuery(method) == 'SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`street`,restaurant_.`zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`street` = ?)' getParameterBindingIndexes(method) == ["0"] as String[] getParameterBindingPaths(method) == [""] as String[] getParameterPropertyPaths(method) == ["address.street"] as String[] @@ -2469,4 +2472,33 @@ interface Repo extends GenericRepository { getOperationType(method) == DataMethod.OperationType.UPDATE getRawQuery(method) == 'REPLACE INTO book (id, title, total_pages) VALUES (?, ?, ?)' } + + 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/groovy/io/micronaut/data/processor/sql/BuildTableSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildTableSpec.groovy index 6a42d83b664..0d0d1442fce 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 @@ -37,7 +37,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/sql/ColumnTransformerSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/ColumnTransformerSpec.groovy index 7308404a00e..d8157b318da 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 @@ -66,7 +66,7 @@ class Project { def sql = builder.createCriteriaInsert(Project).build(new SqlQueryBuilder()).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(?),?,?,?,?)' } void "test build update with column writer"() { @@ -85,7 +85,7 @@ class Project { def sql = query.build(queryBuilder).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: @@ -94,7 +94,7 @@ class Project { def sql = query.where(builder.equal(root.get("name"), builder.parameter(String))).build(queryBuilder).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"() { @@ -119,7 +119,7 @@ class Project { def sql = query.build(new SqlQueryBuilder()).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(?),?,?)' } void "test build update with column writer2"() { @@ -137,7 +137,7 @@ class Project { def sql = query.build(new SqlQueryBuilder()).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"() { @@ -147,6 +147,6 @@ class Project { def sql = query.where(builder.equal(root.get("xyz"), builder.parameter(String))).build(queryBuilder).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 6778b811e08..8514247f225 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 @@ -135,7 +135,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[] @@ -144,7 +144,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[] @@ -208,7 +208,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"() { @@ -295,7 +295,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-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-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 + ) { + } +} 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 diff --git a/src/main/docs/guide/breaks.adoc b/src/main/docs/guide/breaks.adoc index 1dd1abb04a3..c06682c7d88 100644 --- a/src/main/docs/guide/breaks.adoc +++ b/src/main/docs/guide/breaks.adoc @@ -36,6 +36,148 @@ connection.propagate(() -> { NOTE: These changes are required for Micronaut 5 to support propagating context with scoped values. +=== 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. + +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. + +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) +); +---- + == 4.0 === Repositories validation