Skip to content

6.0 Type redesign

Steve Ebersole edited this page Aug 9, 2016 · 7 revisions

One of the major features of 6.0 will be the unification of Hibernate and JPA type systems (as well the SQM type system) under org.hibernate.type.spi.Type contract, which will replace the older org.hibernate.type.Type interface (UserType tbd). This page is intended as a redesign proposal for the new org.hibernate.type.spi.Type interface and its sub-interfaces.

Type

In general the legacy Type contract defined:

  • access to the Java type (Class)

  • the number of mapped columns (w/ access to Mapping)

  • the types of each mapped column (w/ access to Mapping)

  • default Sizes for each mapped column (w/ access to Mapping)

  • dictated Sizes (UUID, e.g.) for each mapped column (w/ access to Mapping)

  • calculation of "column nullness" based on an instance

  • calculation of hashCode value based on an instance

  • calculation of equality/sameness

  • calculation of dirtyness

  • comparison

  • assemble/disassemble (+ beforeAssemble)

  • replace/(deep)copy

  • read/write

Even in 5.0 and earlier we had already started breaking the Type impls themselves down following a more composition pattern, mainly via:

  • JavaTypeDescriptor - descriptor for things related to the Java type (String, Integer, Address, etc)

  • SqlTypeDescriptor - descriptor for things related to the SQL/JDBC type (VARCHAR, CLOB, etc)

  • MutabilityPlan - defines a number of capabilities based on the "mutability" of a particular Java type: how to make a deep copy of an instance (to isolate internal state mutation), e.g.

  • etc

6.0 builds on that by removing the "pulled up" methods in favor of exposing the delegates/components where possible. The proposal for the new Type contract is as follows:

package org.hibernate.type.spi;

/**
 * The common "type" contract in the unified type system.
 */
public interface Type<T> extends org.hibernate.sqm.domain.Type, javax.persistence.metamodel.Type<T> {
	/**
	 * Enumerated values for the classification of the Type.
	 */
	enum Classification {
		/**
		 * Represents basic types (Strings, Integers, enums, etc).  Types classified as
		 * BASIC will be castable to {@link BasicType}
		 */
		BASIC( PersistenceType.BASIC ),
		/**
		 * Represents composite values (what JPA calls embedded/embeddable).  Types classified as
		 * COMPOSITE will be castable to {@link CompositeType}
		 */
		COMPOSITE( PersistenceType.EMBEDDABLE ),
		/**
		 * Represents reverse-discriminated values (where the discriminator is on the FK side of the association).
		 * Types classified as ANY will be castable to {@link AnyType}
		 */
		ANY( null ),
		/**
		 * Represents an entity value (either as a root, one-to-one or many-to-one).  Types classified
		 * as ENTITY will be castable to {@link EntityType}
		 */
		ENTITY( PersistenceType.ENTITY ),
		MAPPED_SUPERCLASS( PersistenceType.MAPPED_SUPERCLASS ),
		/**
		 * Represents a plural attribute, including the FK.   Types classified as COLLECTION
		 * will be castable to {@link CollectionType}
		 */
		COLLECTION( null );

		private final PersistenceType jpaPersistenceType;

		Classification(PersistenceType jpaPersistenceType) {
			this.jpaPersistenceType = jpaPersistenceType;
		}

		public PersistenceType getJpaPersistenceType() {
			return jpaPersistenceType;
		}

		public static Classification fromJpaPersistenceType(PersistenceType jpaPersistenceType) {
			switch ( jpaPersistenceType ) {
				case BASIC: {
					return BASIC;
				}
				case MAPPED_SUPERCLASS: {
					return MAPPED_SUPERCLASS;
				}
				case EMBEDDABLE: {
					return COMPOSITE;
				}
				case ENTITY: {
					return ENTITY;
				}
				default: {
					return null;
				}
			}
		}
	}

	/**
	 * Return the classification of this Type.
	 *
	 * @return The Type's classification/categorization
	 */
	Classification getClassification();

	@Override
	default PersistenceType getPersistenceType() {
		return this.getClassification().getJpaPersistenceType();
	}

	/**
	 * Returns the abbreviated name of the Type.  Mostly used historically for short-name
	 * referencing of the Type in {@code hbm.xml} mappings.
	 *
	 * @return The Type name
	 */
	String getName();

	/**
	 * Obtain a descriptor for the Java side of a value mapping.
	 *
	 * @return The Java type descriptor.
	 */
	JavaTypeDescriptor getJavaTypeDescriptor();

	/**
	 * The mutability of this type.  Generally follows
	 * {@link #getJavaTypeDescriptor()} -> {@link JavaTypeDescriptor#getMutabilityPlan()}
	 *
	 * @return The type's mutability
	 */
	MutabilityPlan getMutabilityPlan();

	/**
	 * The comparator for this type.  Generally follows
	 * {@link #getJavaTypeDescriptor()} -> {@link JavaTypeDescriptor#getComparator()}
	 *
	 * @return The type's comparator
	 */
	Comparator getComparator();

	/**
	 * Generate a representation of the value for logging purposes.
	 *
	 * @param value The value to be logged
	 * @param factory The session factory
	 *
	 * @return The loggable representation
	 *
	 * @throws HibernateException An error from Hibernate
	 */
	String toLoggableString(Object value, SessionFactoryImplementor factory);
}

That covers everything listed in the list of common Type capabilities, except for read and write. Let’s come back to these, as they deserve a separate discussion.

To explain some of the more non-obvious capability coverage…​

  • calculation of hashCode value is fulfilled by JavaTypeDescriptor#extractHashCode

  • calculation of equality/sameness is fulfilled by JavaTypeDescriptor#areEqual

  • calculation of dirtyness - ?

  • comparison is fulfilled by JavaTypeDescriptor#getComparator - however there is some question as to whether we may want to allow this to be overridden at the Type level like we do for MutabilityPlan: the main use case being support of T/SQL ROWVERSION datatype which is an auto-incrementing datatype represented as a byte[]. In other words, it is a byte[] with a specific comparison requirement.

  • assemble/disassemble (+ beforeAssemble) is fulfilled by MutabilityPlan#disassemble and MutabilityPlan#assemble (?beforeAssemble?)

  • replace/(deep)copy is fulfilled by MutabilityPlan#deepCopy

BasicType

BasicType is the only sub-interface I have worked at all so far. I like the shape it is in relative to Type (read/write questions aside).

package org.hibernate.type.spi;

interface BasicType<T>
        extends Type<T>, javax.persistence.metamodel.BasicType<T>, org.hibernate.sqm.domain.BasicType<T> {
	@Override
	JavaTypeDescriptor<T> getJavaTypeDescriptor();

	@Override
	MutabilityPlan<T> getMutabilityPlan();

	@Override
	Comparator<T> getComparator();

	/**
	 * Describes the column mapping for this BasicType.
	 *
	 * @return The column mapping for this BasicType
	 */
	ColumnMapping getColumnMapping();

	/**
	 * The converter applied to this type, if one.
	 *
	 * @return The applied converter.
	 */
	AttributeConverter<T,?> getAttributeConverter();

	@Override
	default Classification getClassification() {
		return Classification.BASIC;
	}

	@Override
	default String getName() {
		return getTypeName();
	}

	@Override
	default Class<T> getJavaType() {
		return getJavaTypeDescriptor().getJavaTypeClass();
	}

	@Override
	@SuppressWarnings("unchecked")
	default String toLoggableString(Object value, SessionFactoryImplementor factory) {
		return getJavaTypeDescriptor().extractLoggableRepresentation( (T) value );
	}
}

Notice that this design assumes a single column. That is a change from legacy BasicType which allowed multiple columns.

Note

The keys returned from the legacy BasicType#getRegistrationKeys really are meant for start up mapping for implicit resolution of BasicTypes. Same thing for @TypeDef. Regardless of where the BasicType and its registration keys come from, the keys are only needed during bootstrap. For this reason we will be removing the BasicType#getRegistrationKeys method and isolating that to some bootstrap cross-reference.

Really there ought to be 2 distinct BasicType "registries", or 2 different views of registered BasicTypes.

The first is based on these "registration keys" and would be used for "type def" resolutions (e.g., explicit reference by short name). This does not allow creation of new BasicType registry entries; it merely looks for existing entries.

The second form would accept the planned org.hibernate.type.spi.basic.BasicTypeParameters "context"

CompositeType

To do

EntityType

To do

AnyType

To do

CollectionType

To do

Read and write capabilities

  • reads should be position based (assume contiguous?), not name based. this was a finding of the perf team. Relatedly, SQL rendering can omit the selected column aliases thereby producing more compact SQL. Double perf win.

  • writes, for the most part, work OK in 5.x already

  • where possible should leverage ValueBinder and ValueExtractor.

TypeConfiguration

org.hibernate.type.spi.TypeConfiguration is intended to model a Type "configuration set" in that it is the central point of building, storing and accessing Type information. Additionally it offers a convenient "isolated scoping" of the Type instances which allows us to easily incorporate Mapping and SessionFactory access to the Types that need them.

Note
A Bit of History

Many methods on the legacy Type contract accept Session and/or SessionFactory and/or Mapping. Even worse, these methods grew organically initially not accepting either and then eventually being overloaded to account for:

  • passing none

  • passing SessionFactory/Mapping

  • passing Session

Since those original choices I have been trying to remove the distinction between the first 2. org.hibernate.type.TypeFactory.TypeScope was the initial attempt at that. Basically it is a delegate that is scoped to the TypeFactory and after the SessionFactory is built that SF is injected into the TypeScope. Some types accept the TypeScope as a parameter and use it for access to the SF if they need it. I think in general that is a better paradigm than overloaded methods.

The new TypeConfiguration is also designed to expose the Mapping object, which should always be present.

The lifecycle of a TypeConfiguration is as follows:

  1. It is created during bootstrap

  2. Likely we will need some "priming" code for standard BasicTypes. We also need to adapt org.hibernate.boot.model.TypeContributor to work with the new Types and perform that as part of priming.

  3. The process of building the org.hibernate.boot.Metadata will further populate it as we build the metamodel of the domain-model.

  4. The process of building the SessionFactory will "scope" the TypeConfiguration to the SF (this process also registers a SessionFactoryObserver to get a closing callback to "unscope" the TypeConfiguration).

  5. The SessionFactory is closed, "unscoping" the TypeConfiguration

What does that all mean? As an example, take a method like Type#getColumnSpan(Mapping). So instead of:

@Override
public int getColumnSpan(Mapping mapping) {
    // access Mapping
    return mapping....
}

we’d have:

@Override
public int getColumnSpan() {
    // access Mapping
    return getTypeScope().getMapping()...
}

For methods taking Session, there is not much we can do as far as removing Session as an argument. But these generally are limited to reading, writing and making copies. What I would suggest for these is to remove the overloads; either a method takes a Session or it doesn’t. If it needs access to the SessionFactory to implement something, it can just access them through the TypeConfiguration. The built-in Type impls take the TypeConfiguration as ctor args. Custom Type impls can also receive the TypeConfiguration by implementing the optional TypeConfigurationAware contract.