Home > Enterprise >  Migration from NamingStrategy in Hibernate
Migration from NamingStrategy in Hibernate

Time:01-22

I'm working on a Java project that was started many years ago, and all of the original developers are long gone. One of the classes I inherited is an implementation of the Hibernate NamingStrategy interface.

At this point NamingStrategy is deprecated and I'd like to transition to something that's still supported going forward. Unfortunately, I can't figure out how to do this. I've seen two families of suggestions on the topic: most people online recommend the use of ImplicitNamingStrategy and PhysicalNamingStrategy, while the Hibernate Javadocs recommend the use of NamingStrategyDelegator.

In both cases, the information available to an unsophisticated Hibernate user is not sufficient to understand how to do the migration. Just as an example: NamingStrategy has 10 methods, each of which takes String arguments. PhysicalNamingStrategy has 5 methods, none of which takes a String argument.

Can anyone assist me in figuring out how to address this safely? In case it helps, I attach our NamingStrategy implementation below.

package gov.nasa.ziggy.services.database;

import javax.persistence.Column;
import javax.persistence.Table;

import org.hibernate.AssertionFailure;
import org.hibernate.cfg.NamingStrategy;
import org.hibernate.internal.util.StringHelper;

import gov.nasa.ziggy.util.StringUtils;
import gov.nasa.ziggy.util.StringUtils.TextCase;

/**
 * This class implements {@link org.hibernate.cfg.NamingStrategy} for the database naming
 * conventions defined for the Ziggy project. Essentially, this consists of converting the Java
 * camel-case names to an all-caps with underscores format.
 * <p>
 * This code is based on {@link org.hibernate.cfg.ImprovedNamingStrategy}, the main difference being
 * that names that are explicitly defined in the code annotations (like {@link Table},
 * {@link Column}, etc.) are not modified.
 *
 */
public class ZiggyNamingStrategy implements NamingStrategy {
    /**
     * A convenient singleton instance
     */
    public static final NamingStrategy INSTANCE = new ZiggyNamingStrategy();

    /**
     * Return the unqualified class name, mixed case converted to underscores
     */
    @Override
    public String classToTableName(String className) {
        return addUnderscores(StringHelper.unqualify(className));
    }

    /**
     * Return the full property path with underscore seperators, mixed case converted to underscores
     */
    @Override
    public String propertyToColumnName(String propertyName) {
        return addUnderscores(StringHelper.unqualify(propertyName));
    }

    /**
     * This method is called for names explicitly defined in the annotations. Leave the name alone!
     */
    @Override
    public String tableName(String tableName) {
        return tableName;
    }

    /**
     * This method is called for names explicitly defined in the annotations. Leave the name alone!
     */
    @Override
    public String columnName(String columnName) {
        return columnName;
    }

    @Override
    public String collectionTableName(String ownerEntity, String ownerEntityTable,
        String associatedEntity, String associatedEntityTable, String propertyName) {
        String s = tableName(ownerEntityTable   '_'   propertyToColumnName(propertyName));
        System.out.println("collection table name "   s);
        return s;
    }

    /**
     * Return the argument
     */
    @Override
    public String joinKeyColumnName(String joinedColumn, String joinedTable) {
        String s = addUnderscores(joinedTable)   "_"   addUnderscores(joinedColumn);
        System.out.println("joinkey column name "   s);
        return s;
    }

    /**
     * Return the property name or propertyTableName
     */
    @Override
    public String foreignKeyColumnName(String propertyName, String propertyEntityName,
        String propertyTableName, String referencedColumnName) {
        String header = propertyName != null ? StringHelper.unqualify(propertyName)
            : propertyTableName;
        if (header == null) {
            throw new AssertionFailure("NamingStrategy not properly filled");
        }
        String s = addUnderscores(propertyTableName)   "_"   addUnderscores(referencedColumnName);
        if (s.equals("PI_PS_NAME_NAME")) {
            System.out.println("foreign key column name "   s);
        }
        return s;
    }

    /**
     * Return the column name or the unqualified property name
     */
    @Override
    public String logicalColumnName(String columnName, String propertyName) {
        String s = StringHelper.isNotEmpty(columnName) ? columnName
            : StringHelper.unqualify(propertyName);
        if (columnName == null) {
            System.out
                .println("logical column name "   columnName   "  "   propertyName   "  "   s);
        }
        return s;
    }

    /**
     * Returns either the table name if explicit or if there is an associated table, the
     * concatenation of owner entity table and associated table otherwise the concatenation of owner
     * entity table and the unqualified property name
     */
    @Override
    public String logicalCollectionTableName(String tableName, String ownerEntityTable,
        String associatedEntityTable, String propertyName) {
        if (tableName != null) {
            return tableName;
        }
        // use of a stringbuffer to workaround a JDK bug
        String s = new StringBuffer(ownerEntityTable).append("_")
            .append(associatedEntityTable != null ? associatedEntityTable
                : StringHelper.unqualify(propertyName))
            .toString();
        System.out.println("logical collection table name "   s);
        return s;
    }

    /**
     * Return the column name if explicit or the concatenation of the property name and the
     * referenced column
     */
    @Override
    public String logicalCollectionColumnName(String columnName, String propertyName,
        String referencedColumn) {
        String s = StringHelper.isNotEmpty(columnName) ? columnName
            : StringHelper.unqualify(propertyName)   "_"   referencedColumn;
        if (!s.equals(columnName)) {
            System.out.println("logical collection column name "   columnName   "  "   propertyName
                  "  "   referencedColumn   "  "   s);
        }
        return s;
    }

    protected String addUnderscores(String original) {
        return StringUtils.camelCaseToUnderscores(original, TextCase.UPPER);
    }

}

CodePudding user response:

So I very recently rewrote the Javadoc for these interfaces, to make it easier to understand the difference. But that new Javadoc isn't available online yet, because the release of 6.2 final slipped by a week.

So let me copy/paste the explanation from the Javadoc here, HTH.

Package org.hibernate.boot.model.naming

This API allows intervention by generic code in the process of determining the names of database objects (tables, columns, and constraints).

Name determination happens in two phases:

  1. Logical naming is the process of applying naming rules to determine the names of objects which were not explicitly assigned names in the O/R mapping.

    Here, this is the responsibility of an implementation of org.hibernate.boot.model.naming.ImplicitNamingStrategy.

  2. Physical naming is the process of applying additional rules to transform a logical name into an actual "physical" name that will be used in the database. For example, the rules might include things like using standardized abbreviations, or trimming the length of identifiers.

    Here, this is the responsibility of an implementation of PhysicalNamingStrategy.

ImplicitNamingStrategy

A set of rules for determining the logical name of a mapped relational database object when the mapping for an element of the Java domain model is not explicitly specified, neither in annotations of the Java code, nor in an XML-based mapping document.

For example, if a Java class annotated @Entity has no @Table annotation, then determinePrimaryTableName is called with an ImplicitEntityNameSource providing access to information about the Java class and its entity name.

On the other hand, when a logical name is explicitly specified, for example, using @Table to specify the table name, or @Column to specify a column name, the PhysicalNamingStrategy is not called and has no opportunity to intervene in the determination of the logical name.

However, a further level of processing is applied to the resulting logical names by a PhysicalNamingStrategy in order to determine the "finally final" physical names in the relational database schema. Whenever reasonable, the use of a custom ImplicitNamingStrategy is highly recommended in preference to tedious and repetitive explicit table and column name mappings. It's anticipated that most projects using Hibernate will feature a custom implementation of ImplicitNamingStrategy.

An ImplicitNamingStrategy may be selected using the configuration property "hibernate.implicit_naming_strategy".

PhysicalNamingStrategy

A set of rules for determining the physical names of objects in a relational database schema from the logical names specified by the object/relational mappings.

  • A physical name is a name used to interact with the database, and will always be used in generated SQL, both DML and DDL.
  • A logical name is a name used to within annotations of Java code and XML mapping documents.

Logical names provide an additional level of indirection between the mappings and the database schema, and a PhysicalNamingStrategy even allows the use of more "natural" naming within the mappings in cases where the relational schema features especially inelegant legacy naming conventions. For example, it could shield the mappings from old-fashioned practices like prefixing table names with TBL_.

Note, however, that handwritten native SQL must be written in terms of physical names, so the abstraction here is in some sense "incomplete".

A PhysicalNamingStrategy may be selected using the configuration property "hibernate.physical_naming_strategy".

CodePudding user response:

Short story

Stay clear of naming strategies in Hibernate, this feature is unstable, unreliable, unmaintainable and, furthermore, it increases technical debt (that is exactly what you actually get: at this point NamingStrategy is deprecated and I'd like to transition to something that's still supported going forward), moreover, even Hibernate team admits this feature is unstable:

@Incubating
public interface PhysicalNamingStrategy {

@Incubating:

Marks certain of packages, types, etc. as incubating, potentially recursively. Incubating indicates something that is still being actively developed and therefore may change at a later time; a "tech preview".

Users of these types and methods are considered early adopters who help shape the final definition of these types/methods, along with the needs of consumers.

The best option is to place valid/actual @Table and @Column annotations over related entities/fields and close that technical debt - that is your responsibility, as a developer, to keep the code maintainable.

Long story

  1. naming strategies are not portable across JPA implementations, for example if you try to find how to implement the same in eclipselink you may find something like:
    @Override
    public void customize(Session session) throws SQLException {
        for (ClassDescriptor descriptor : session.getDescriptors().values()) {
            // Only change the table name for non-embedable entities with no
            // @Table already
            if (!descriptor.getTables().isEmpty() && descriptor.getAlias().equalsIgnoreCase(descriptor.getTableName())) {
                String tableName = addUnderscores(descriptor.getTableName());
                descriptor.setTableName(tableName);
                for (IndexDefinition index : descriptor.getTables().get(0).getIndexes()) {
                    index.setTargetTable(tableName);
                }
            }
            for (DatabaseMapping mapping : descriptor.getMappings()) {
                // Only change the column name for non-embedable entities with
                // no @Column already

                if (mapping instanceof AggregateObjectMapping) {
                    for (Association association : ((AggregateObjectMapping) mapping).getAggregateToSourceFieldAssociations()) {
                        DatabaseField field = (DatabaseField) association.getValue();
                        field.setName(addUnderscores(field.getName()));
                        
                        for (DatabaseMapping attrMapping : session.getDescriptor(((AggregateObjectMapping) mapping).getReferenceClass()).getMappings()) {
                            if (attrMapping.getAttributeName().equalsIgnoreCase((String) association.getKey())) {
                                ((AggregateObjectMapping) mapping).addFieldTranslation(field, addUnderscores(attrMapping.getAttributeName()));
                                ((AggregateObjectMapping) mapping).getAggregateToSourceFields().remove(association.getKey());
                                break;
                            }
                        }
                    }
                } else if (mapping instanceof ObjectReferenceMapping) {
                    for (DatabaseField foreignKey : ((ObjectReferenceMapping) mapping).getForeignKeyFields()) {
                        foreignKey.setName(addUnderscores(foreignKey.getName()));
                    }
                } else if (mapping instanceof DirectMapMapping) {
                    for (DatabaseField referenceKey : ((DirectMapMapping) mapping).getReferenceKeyFields()) {
                        referenceKey.setName(addUnderscores(referenceKey.getName()));
                    }
                    for (DatabaseField sourceKey : ((DirectMapMapping) mapping).getSourceKeyFields()) {
                        sourceKey.setName(addUnderscores(sourceKey.getName()));
                    }
                } else {
                    DatabaseField field = mapping.getField();
                    if (field != null && !mapping.getAttributeName().isEmpty() && field.getName().equalsIgnoreCase(mapping.getAttributeName())) {
                        field.setName(addUnderscores(mapping.getAttributeName()));
                    }
                }
            }
        }
    }

Sorry, but that is not kind of code I would like to maintain, on the other hand, I can definitely say that eclipselink is not JPA implementation I would like to deal with - the problem is it strictly follows JPA specification (Hibernate is definitely more flexible and extensible). However, you need to keep in mind that naming strategies applies globally, so, when you adopting third-party library a plugin, based on JPA, you might get "surprised" that that library or plugin conflicts with your naming strategy.

  1. predictable names make sense:
  • when you diagnose performance issues, DBAs typically tell you what queries are slow, after that you need somehow determine what code produces those slow queries (there we need to say "thanks" to spring-data-jpa for their naming convention), and that takes a time to match SQL query to JPQL query, and usage of naming strategies is not your fried there. If you think micrometer/grafana is a good option, I have bad news for you
  • naming strategies somehow cover table and column names, but do not cover index names, which is actually important from performance perspective: try to guess what columns are covered by index with name like UK_jymdjmt0g2vxkk0fp56xlhjud. Need to ask DBA? OK, your code is unmaintainable...
  1. the feature is broken

Little quiz:

Q: what you need to specify in @Column definition when InheritanceType=JOINED:

    /**
     * (Optional) The name of the table that contains the column. 
     * If absent the column is assumed to be in the primary table.
     */
    String table() default "";

A: Hibernate accepts both logical and physical table names.

Q: what do you need specify in @Synchronize annotation if you created a "view" and you would like to keep view state in sync with persistence context:

@Target(TYPE)
@Retention(RUNTIME)
public @interface Synchronize {
    /**
     * Table names.
     */
    String[] value();
}

A: in Hibernate 4 you was need to specify physical table names, in Hibernate 5.1 you need to specify logical table names

Q: you need to trigger a SQL procedure/update statement and you would like to keep its execution in sync with persistence context, what do you need to specify for org.hibernate.query.native.spaces hint:

    /**
     * Hint for specifying query spaces to be applied to a NativeQuery.
     *
     * Passed value can be any of:<ul>
     *     <li>List of the spaces</li>
     *     <li>array of the spaces</li>
     *     <li>String as "whitespace"-separated list of the spaces</li>
     * </ul>
     *
     * Note that the passed space need not match any real spaces/tables in
     * the underlying query.  This can be used to completely circumvent
     * the auto-flush checks as well as any cache invalidation that might
     * occur as part of a flush.  See {@link org.hibernate.query.SynchronizeableQuery}
     * and {@link FlushMode#MANUAL} for more information.
     *
     * @see org.hibernate.query.SynchronizeableQuery
     * @see #HINT_FLUSH_MODE
     */
    String HINT_NATIVE_SPACES = "org.hibernate.query.native.spaces";

A: you need to specify everything you might find feasible: physical table names, logical table names, entity names, cache region names, etc.

  • Related