Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,23 @@
import java.util.Properties;

import jakarta.annotation.Nonnull;
import jakarta.persistence.EnumType;

import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.mapping.BasicValue;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Table;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.grails.orm.hibernate.cfg.ColumnConfig;
import org.grails.orm.hibernate.cfg.IdentityEnumType;
import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
import org.grails.orm.hibernate.cfg.PropertyConfig;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateBasicProperty;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEnumProperty;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty;
import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;
import org.grails.orm.hibernate.cfg.domainbinding.util.GrailsEnumType;

import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.ENUM_CLASS_PROP;

public class EnumTypeBinder {

private static final Logger LOG = LoggerFactory.getLogger(EnumTypeBinder.class);
private final MetadataBuildingContext metadataBuildingContext;
private final ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher;
private final IndexBinder indexBinder;
Expand Down Expand Up @@ -77,60 +70,22 @@ protected EnumTypeBinder(
}

public BasicValue bindEnumType(@Nonnull HibernateEnumProperty property, String path) {
String columnName = columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(property, path, null);
String columnName = property.resolveEnumColumnName(namingStrategy, columnNameForPropertyAndPathFetcher, path);
BasicValue simpleValue = new BasicValue(metadataBuildingContext, property.getTable());
bindEnumType(property, property.getType(), simpleValue, columnName);
return simpleValue;
}

public BasicValue bindEnumTypeForColumn(@Nonnull HibernateBasicProperty property) {
String columnName = property.joinTableColumName(namingStrategy);
BasicValue simpleValue = new BasicValue(metadataBuildingContext, property.getTable());
bindEnumType(property, property.getComponentType(), simpleValue, columnName);
return simpleValue;
}

protected void bindEnumType(
HibernatePersistentProperty property, Class<?> propertyType, BasicValue simpleValue, String columnName) {
Class<?> propertyType = property.getEnumType();
PropertyConfig pc = property.getHibernateMappedForm();
Properties enumProperties = new Properties();
enumProperties.put(ENUM_CLASS_PROP, propertyType.getName());
String typeName = property.getTypeName(propertyType);
if (typeName != null) {
simpleValue.setTypeName(typeName);
} else {
switch (GrailsEnumType.fromString(pc.getEnumType())) {
case DEFAULT, STRING -> {
// Hibernate 7 native string enum mapping: store by Enum.name() as VARCHAR.
simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
simpleValue.setEnumerationStyle(EnumType.STRING);
}
case ORDINAL -> {
// Hibernate 7 native ordinal enum mapping: store by Enum.ordinal() as INTEGER.
simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
simpleValue.setEnumerationStyle(EnumType.ORDINAL);
}
case IDENTITY -> simpleValue.setTypeName(IdentityEnumType.class.getName());
default -> throw new IllegalArgumentException("Unknown enum type: " + pc.getEnumType());
}
GrailsEnumType.fromString(pc.getEnumType()).configure(simpleValue, propertyType);
}
Properties enumProperties = new Properties();
enumProperties.put(ENUM_CLASS_PROP, propertyType.getName());
simpleValue.setTypeParameters(enumProperties);

Column column = new Column();
boolean isTablePerHierarchySubclass = property.getHibernateOwner().isTablePerHierarchySubclass();
if (isTablePerHierarchySubclass) {
// Properties on subclasses in a table-per-hierarchy strategy must be nullable.
if (LOG.isDebugEnabled()) {
LOG.debug(
"[GrailsDomainBinder] Sub class property [{}] for column name [{}] forced to nullable",
property.getName(),
columnName);
}
column.setNullable(true);
} else {
column.setNullable(property.isNullable());
}

column.setNullable(property.isEnumColumnNullable());
column.setValue(simpleValue);
column.setName(columnName);
Table t = simpleValue.getTable();
Expand All @@ -142,5 +97,7 @@ protected void bindEnumType(
indexBinder.bindIndex(columnName, column, columnConfig, t);
columnConfigToColumnBinder.bindColumnConfigToColumn(column, columnConfig, pc);
}
return simpleValue;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ public Value bindProperty(

Value value;

if (currentGrailsProp instanceof HibernateEnumProperty hibernateEnumProperty) {
if (currentGrailsProp instanceof HibernateEnumProperty hibernateEnumProperty &&
!(currentGrailsProp instanceof HibernateToManyProperty)) {
Comment thread
jdaugherty marked this conversation as resolved.
Outdated
// A hasMany-of-enum property is also a HibernateEnumProperty, but it must still go
// through collectionBinder.bindCollection() below so its join table gets created;
// EnumTypeBinder only binds its element column, from BasicCollectionElementBinder.
value = enumTypeBinder.bindEnumType(hibernateEnumProperty, path);
} else if (currentGrailsProp.isUserButNotCollectionType()) {
value = simpleValueBinder.bindBasicValue(currentGrailsProp, parentProperty, path);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.grails.orm.hibernate.cfg.domainbinding.hibernate;

import java.beans.PropertyDescriptor;

import org.grails.datastore.mapping.model.MappingContext;
import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;

/**
* Hibernate basic collection element property whose element type is an enum. Created by {@link
* HibernateMappingFactory#createBasicCollection} when the collection's element type is an enum.
*/
public class HibernateBasicEnumProperty extends HibernateBasicProperty implements HibernateEnumProperty {
Comment thread
jdaugherty marked this conversation as resolved.

public HibernateBasicEnumProperty(
GrailsHibernatePersistentEntity entity, MappingContext context, PropertyDescriptor property) {
super(entity, context, property);
}

@Override
public Class<?> getEnumType() {
return getComponentType();
}

@Override
public String resolveEnumColumnName(
PersistentEntityNamingStrategy namingStrategy,
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher,
String path) {
return joinTableColumName(namingStrategy);
}

/** A hasMany element column is always nullable, matching the non-enum sibling binding path. */
@Override
public boolean isEnumColumnNullable() {
Comment thread
jdaugherty marked this conversation as resolved.
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.beans.PropertyDescriptor;

import org.hibernate.mapping.Collection;
import org.hibernate.mapping.Table;

import org.grails.datastore.mapping.model.MappingContext;
import org.grails.datastore.mapping.model.types.mapping.BasicWithMapping;
Expand All @@ -45,4 +46,16 @@ public Collection getHibernateCollection() {
public void setHibernateCollection(Collection collection) {
this.collection = collection;
}

/**
* For a basic (scalar or enum) collection element, the property's table is the
* collection's join table rather than the owning entity's table. Before the collection
* table has been assigned (e.g. while it is itself being computed), falls back to the
* owning entity's table, matching the pre-collection-binding default.
*/
@Override
public Table getTable() {
Table collectionTable = collection != null ? collection.getCollectionTable() : null;
return collectionTable != null ? collectionTable : getPersistentClass().getTable();
}
Comment thread
jdaugherty marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,46 @@
*/
package org.grails.orm.hibernate.cfg.domainbinding.hibernate;

import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;

/**
* Marker interface for Hibernate persistent properties whose Java type is an enum.
Comment thread
jdaugherty marked this conversation as resolved.
Outdated
*
* <p>Two concrete subtypes exist, corresponding to the two creation paths in {@link
* <p>Three concrete subtypes exist, corresponding to the three creation paths in {@link
* HibernateMappingFactory}:
*
* <ul>
* <li>{@link HibernateSimpleEnumProperty} — plain enum with no custom type marshaller
* <li>{@link HibernateCustomEnumProperty} — enum backed by a custom type marshaller
* <li>{@link HibernateBasicEnumProperty} — enum element of a {@code hasMany} basic collection
* </ul>
*
* <p>Use {@code instanceof HibernateEnumProperty} instead of {@code isEnumType()} to branch on
* enum properties at binding time.
* enum properties at binding time. Each implementation resolves its own enum class and column
* name so {@link org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder} can bind any
* of them through a single code path.
*/
public interface HibernateEnumProperty extends HibernatePersistentProperty {}
public interface HibernateEnumProperty extends HibernatePersistentProperty {

/** The enum class to bind: the property's own type, or a basic collection's element type. */
default Class<?> getEnumType() {
return getType();
}

/** Resolves the column name to bind the enum value under. */
default String resolveEnumColumnName(
PersistentEntityNamingStrategy namingStrategy,
ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher,
String path) {
return columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(this, path, null);
}

/**
* Whether the enum column should allow NULL. Subclass properties in a table-per-hierarchy
* strategy must be nullable; otherwise this follows the property's own nullable constraint.
*/
default boolean isEnumColumnNullable() {
return getHibernateOwner().isTablePerHierarchySubclass() || isNullable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,13 @@ class HibernateMappingFactory extends AbstractGormMappingFactory<Mapping, Proper
PersistentEntity entity, MappingContext context, PropertyDescriptor property, Class collectionType) {
if (entity instanceof GrailsHibernatePersistentEntity) {
GrailsHibernatePersistentEntity ghpEntity = (GrailsHibernatePersistentEntity) entity
HibernateBasicProperty basic = new HibernateBasicProperty(ghpEntity, context, property)
boolean isEnumCollection = collectionType != null && collectionType.isEnum()
HibernateBasicProperty basic = isEnumCollection
? new HibernateBasicEnumProperty(ghpEntity, context, property)
: new HibernateBasicProperty(ghpEntity, context, property)
Comment thread
jdaugherty marked this conversation as resolved.
basic.setMapping(createPropertyMapping(basic, entity))
CustomTypeMarshaller customTypeMarshaller = findCustomType(context, property.propertyType)
if (collectionType != null && collectionType.isEnum()) {
if (isEnumCollection) {
customTypeMarshaller = findCustomType(context, collectionType)
if (customTypeMarshaller == null) {
customTypeMarshaller = findCustomType(context, Enum)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,14 @@ default String joinTableColumName(PersistentEntityNamingStrategy namingStrategy)
String columnName;
if (present) {
columnName = joinColumnMappingOptional.get().getName();
} else if (referencedType.isEnum()) {
// Use the enum's simple name, not its fully-qualified name, so the column
// isn't named after the enum's package.
columnName = namingStrategy.resolveColumnName(referencedType.getSimpleName());
Comment thread
jdaugherty marked this conversation as resolved.
Comment thread
jdaugherty marked this conversation as resolved.
} else {
var clazz = namingStrategy.resolveColumnName(referencedType.getName());
var prop = namingStrategy.resolveTableName(getName());
columnName = referencedType.isEnum() ?
clazz :
new BackticksRemover().apply(prop) + UNDERSCORE + new BackticksRemover().apply(clazz);
columnName = new BackticksRemover().apply(prop) + UNDERSCORE + new BackticksRemover().apply(clazz);
}
return columnName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@
import org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder;
import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateBasicProperty;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEnumProperty;
import org.grails.orm.hibernate.cfg.domainbinding.util.SimpleValueColumnFetcher;

import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.EMPTY_PATH;

/** Binds the element value for a basic (scalar or enum) collection. */
public class BasicCollectionElementBinder {

Expand Down Expand Up @@ -61,10 +64,10 @@ public BasicCollectionElementBinder(

/** Creates and binds a {@link BasicValue} element for the given basic collection property. */
public BasicValue bind(@Nonnull HibernateBasicProperty property) {
String columnName = property.joinTableColumName(namingStrategy);
if (property.isEnum()) {
return enumTypeBinder.bindEnumTypeForColumn(property);
if (property instanceof HibernateEnumProperty hibernateEnumProperty) {
return enumTypeBinder.bindEnumType(hibernateEnumProperty, EMPTY_PATH);
} else {
String columnName = property.joinTableColumName(namingStrategy);
final Class<?> referencedType = property.getComponentType();
String typeName = property.getTypeName(referencedType);
Collection collection = property.getCollection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,42 @@
*/
package org.grails.orm.hibernate.cfg.domainbinding.util;

import jakarta.persistence.EnumType;

import org.hibernate.MappingException;
import org.hibernate.mapping.BasicValue;

import org.grails.orm.hibernate.cfg.IdentityEnumType;

public enum GrailsEnumType {
DEFAULT("default"),
STRING("string"),
ORDINAL("ordinal"),
IDENTITY("identity");
DEFAULT("default") {
@Override
public void configure(BasicValue simpleValue, Class<?> propertyType) {
STRING.configure(simpleValue, propertyType);
}
},
// Hibernate 7 native string enum mapping: store by Enum.name() as VARCHAR.
STRING("string") {
@Override
public void configure(BasicValue simpleValue, Class<?> propertyType) {
simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
simpleValue.setEnumerationStyle(EnumType.STRING);
}
},
// Hibernate 7 native ordinal enum mapping: store by Enum.ordinal() as INTEGER.
ORDINAL("ordinal") {
@Override
public void configure(BasicValue simpleValue, Class<?> propertyType) {
simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
simpleValue.setEnumerationStyle(EnumType.ORDINAL);
}
},
IDENTITY("identity") {
@Override
public void configure(BasicValue simpleValue, Class<?> propertyType) {
simpleValue.setTypeName(IdentityEnumType.class.getName());
}
};

private final String type;

Expand All @@ -48,4 +77,7 @@ public static GrailsEnumType fromString(String value) {
public String getType() {
return type;
}

/** Configures the given {@link BasicValue} to store {@code propertyType} per this enum type. */
public abstract void configure(BasicValue simpleValue, Class<?> propertyType);
}
Loading
Loading