This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch merge-hibernate6 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 9e326ae8b9f1082f4be97edca18fca7b4d2cde04 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Sun Jul 13 21:22:06 2025 -0500 refactor UniqueNamesGenerator --- .../orm/hibernate/cfg/GrailsDomainBinder.java | 38 +----- .../cfg/domainbinding/UniqueNameGenerator.java | 44 +++++++ .../domainbinding/UniqueNameGeneratorSpec.groovy | 139 +++++++++++++++++++++ 3 files changed, 186 insertions(+), 35 deletions(-) diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java index a4fcc87818..7ab62ed2c9 100644 --- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java +++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsDomainBinder.java @@ -42,6 +42,7 @@ import org.grails.orm.hibernate.cfg.domainbinding.NumericColumnConstraintsBinder import org.grails.orm.hibernate.cfg.domainbinding.SimpleValueBinder; import org.grails.orm.hibernate.cfg.domainbinding.StringColumnConstraintsBinder; import org.grails.orm.hibernate.cfg.domainbinding.TypeNameProvider; +import org.grails.orm.hibernate.cfg.domainbinding.UniqueNameGenerator; import org.hibernate.FetchMode; import org.hibernate.MappingException; import org.hibernate.boot.internal.MetadataBuildingContextRootImpl; @@ -97,11 +98,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; -import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; @@ -1988,40 +1985,11 @@ public class GrailsDomainBinder implements MetadataContributor { uk.addColumns(property.getValue()); } - setGeneratedUniqueName(uk); + new UniqueNameGenerator().setGeneratedUniqueName(uk); table.addUniqueKey(uk); } - private void setGeneratedUniqueName(UniqueKey uk) { - StringBuilder sb = new StringBuilder(uk.getTable().getName()).append('_'); - for (Object col : uk.getColumns()) { - sb.append(((Column) col).getName()).append('_'); - } - - MessageDigest md; - try { - md = MessageDigest.getInstance("MD5"); - } - catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - try { - md.update(sb.toString().getBytes("UTF-8")); - } - catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - - String name = "UK" + new BigInteger(1, md.digest()).toString(16); - if (name.length() > 30) { - // Oracle has a 30-char limit - name = name.substring(0, 30); - } - - uk.setName(name); - } - private boolean canBindOneToOneWithSingleColumnAndForeignKey(Association currentGrailsProp) { if (currentGrailsProp.isBidirectional()) { final Association otherSide = currentGrailsProp.getInverseSide(); @@ -2989,7 +2957,7 @@ public class GrailsDomainBinder implements MetadataContributor { if(LOG.isDebugEnabled()) { LOG.debug("create unique key for " + table.getName() + " columns = " + columns); } - setGeneratedUniqueName(uk); + new UniqueNameGenerator().setGeneratedUniqueName(uk); table.addUniqueKey(uk); } diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGenerator.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGenerator.java new file mode 100644 index 0000000000..ddab2c679c --- /dev/null +++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGenerator.java @@ -0,0 +1,44 @@ +package org.grails.orm.hibernate.cfg.domainbinding; + +import io.micrometer.common.util.StringUtils; +import jakarta.validation.constraints.NotNull; +import org.hibernate.MappingException; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.UniqueKey; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +public class UniqueNameGenerator { + + public void setGeneratedUniqueName(@NotNull UniqueKey uk) { + if(uk.getTable() == null) { + throw new MappingException(String.format("Unique Key %s does not have a table associated with it", uk.getName())); + } + + try { + var fields = new ArrayList<>(List.of(uk.getTable().getName())); + uk.getColumns() + .stream() + .map(Column::getName) + .filter(StringUtils::isNotBlank) + .forEach(fields::add); + var ukString = String.join("_", fields); + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(ukString.getBytes(StandardCharsets.UTF_8)); + String name = "UK" + new BigInteger(1, md.digest()).toString(16); + if (name.length() > 30) { + name = name.substring(0, 30); + } + uk.setName(name); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + } +} diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGeneratorSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGeneratorSpec.groovy new file mode 100644 index 0000000000..45ba1572fa --- /dev/null +++ b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/UniqueNameGeneratorSpec.groovy @@ -0,0 +1,139 @@ +package org.grails.orm.hibernate.cfg.domainbinding + +import org.hibernate.MappingException +import org.hibernate.mapping.Column +import org.hibernate.mapping.Table +import org.hibernate.mapping.UniqueKey +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +class UniqueNameGeneratorSpec extends Specification { + + @Subject + UniqueNameGenerator generator = new UniqueNameGenerator() + + @Unroll + def "should generate a unique name based on table and column names and truncate it"() { + given: "A unique key with a table and several columns" + def table = Mock(Table) + def column1 = Mock(Column) + def column2 = Mock(Column) + def uniqueKey = Mock(UniqueKey) + + table.getName() >> "person" + column1.getName() >> "first_name" + column2.getName() >> "last_name" + + uniqueKey.getTable() >> table + uniqueKey.getColumns() >> [column1, column2] + + def expectedName = generateExpectedName("person", "first_name", "last_name") + + when: "the unique name is generated" + generator.setGeneratedUniqueName(uniqueKey) + + then: "the name is correctly calculated, prefixed, and truncated" + 1 * uniqueKey.setName(expectedName) + } + + @Unroll + def "should not truncate a generated name that is 30 characters or less"() { + given: "A unique key whose hash results in a short name" + def table = Mock(Table) + def column = Mock(Column) + def uniqueKey = Mock(UniqueKey) + + table.getName() >> "short_table" + column.getName() >> "short_col" + uniqueKey.getTable() >> table + uniqueKey.getColumns() >> [column] + + def expectedName = generateExpectedName("short_table", "short_col") + + when: "the unique name is generated" + generator.setGeneratedUniqueName(uniqueKey) + + then: "the name is not truncated because its length is not greater than 30" + 1 * uniqueKey.setName(expectedName) + } + + @Unroll + def "should throw MappingException if the unique key has no associated table"() { + given: "A unique key without a table" + def uniqueKey = Mock(UniqueKey) + uniqueKey.getTable() >> null + uniqueKey.getName() >> "my_uk" // For the exception message + + when: "an attempt is made to generate the name" + generator.setGeneratedUniqueName(uniqueKey) + + then: "a MappingException is thrown with a descriptive message" + def e = thrown(MappingException) + e.message == "Unique Key my_uk does not have a table associated with it" + } + + @Unroll + def "should generate a name based only on the table if no columns are present"() { + given: "A unique key with a table but no columns" + def table = Mock(Table) + def uniqueKey = Mock(UniqueKey) + + table.getName() >> "audit_log" + uniqueKey.getTable() >> table + uniqueKey.getColumns() >> [] + + def expectedName = generateExpectedName("audit_log") + + when: "the unique name is generated" + generator.setGeneratedUniqueName(uniqueKey) + + then: "the name is generated correctly using only the table name" + 1 * uniqueKey.setName(expectedName) + } + + @Unroll + def "should filter out columns with blank or null names"() { + given: "A unique key with valid, blank, and null column names" + def table = Mock(Table) + def column1 = Mock(Column) + def column2 = Mock(Column) + def column3 = Mock(Column) + def uniqueKey = Mock(UniqueKey) + + table.getName() >> "product" + column1.getName() >> "sku" + column2.getName() >> "" // Blank name + column3.getName() >> null // Null name + + uniqueKey.getTable() >> table + uniqueKey.getColumns() >> [column1, column2, column3] + + // Only valid names should be part of the hash + def expectedName = generateExpectedName("product", "sku") + + when: "the unique name is generated" + generator.setGeneratedUniqueName(uniqueKey) + + then: "the blank and null column names are ignored in the calculation" + 1 * uniqueKey.setName(expectedName) + } + + /** + * Helper method that mirrors the core logic of UniqueNameGenerator to create + * a verifiable expected result without using hardcoded "magic" strings. + */ + private String generateExpectedName(String... fields) { + def ukString = fields.join('_') + MessageDigest md = MessageDigest.getInstance("MD5") + md.update(ukString.getBytes(StandardCharsets.UTF_8)) + String name = "UK" + new BigInteger(1, md.digest()).toString(16) + if (name.length() > 30) { + name = name.substring(0, 30) + } + return name + } +} \ No newline at end of file
