-
Notifications
You must be signed in to change notification settings - Fork 3.6k
6.0 Type redesign
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.
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
andMutabilityPlan#assemble
(?beforeAssemble?) -
replace/(deep)copy is fulfilled by
MutabilityPlan#deepCopy
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 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 |
-
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
andValueExtractor
.
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
Since those original choices I have been trying to remove the distinction between the first 2. The new |
The lifecycle of a TypeConfiguration
is as follows:
-
It is created during bootstrap
-
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. -
The process of building the
org.hibernate.boot.Metadata
will further populate it as we build the metamodel of the domain-model. -
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).
-
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.