Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .agents/skills/hibernate-developer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ This skill is for the Grails framework's Hibernate 7 integration module, not for
- `SimpleValueBinder`: binds simple properties.
- `SimpleValueColumnBinder`: binds columns to simple values.
- `ComponentBinder`, `ComponentPropertyBinder`: embedded/component binding.
- `CompositeUserTypeBinder`: binds a property mapped with a Hibernate `CompositeUserType` as a `Component` (one column per attribute of the user type's `embeddable()` class, in alphabetical attribute order, with `returnedClass()` as the component class); a `BasicValue` cannot carry a composite user type. The attributes and the column of each one come from `CompositeUserTypeColumns` (in `domainbinding.util`), which `ColumnNameForPropertyAndPathFetcher.getColumnNamesForPropertyAndPath` also uses, so a uniqueness group that names such a property spans all of its columns. Its columns are bound with `ColumnBinder.bindColumnWithoutUniqueness`, which takes the type of the attribute and defines the column from its own configuration (the constraints of the property describe the whole value; `PropertyConfig.getScale()` and friends throw on a multi-column property), and its uniqueness with one `CreateKeyForProps.createKeyForColumns` key across the bound `Column` objects themselves (which keeps their quoting), never a unique key per column. The user type is instantiated only through `CompositeUserTypeColumns.userTypeOf`, which is CDI-aware when it has the `MetadataBuildingContext`, so pass the context to `ColumnNameForPropertyAndPathFetcher`, `ColumnBinder` and `CompositeUserTypeColumns` wherever it is available. `GrailsPropertyBinder` checks for a composite user type before the enum branch, since an enum property may map one.
- `EnumTypeBinder`: enum mapping.

### Generators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.grails.orm.hibernate.cfg.domainbinding.binder;

import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.dialect.Dialect;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Table;
Expand Down Expand Up @@ -65,17 +66,48 @@ public ColumnBinder(
this.indexBinder = indexBinder;
}

/**
* Creates a binder whose uniqueness groups instantiate a composite user type through its default constructor,
* as Hibernate does outside a CDI container, when a group names a property mapped with one.
*
* @param namingStrategy the naming strategy of the column names
* @param dialect the dialect of the numeric defaults
* @see #ColumnBinder(MetadataBuildingContext, PersistentEntityNamingStrategy, Dialect)
*/
public ColumnBinder(PersistentEntityNamingStrategy namingStrategy, Dialect dialect) {
this(null, namingStrategy, dialect);
}

/**
* Creates a binder whose uniqueness groups instantiate a composite user type as the binder of its property
* does when a group names a property mapped with one: as a managed bean when the building options allow
* extensions in CDI, through its default constructor otherwise.
*
* @param metadataBuildingContext the context of the binding, or {@code null} to instantiate every composite
* user type through its default constructor
* @param namingStrategy the naming strategy of the column names
* @param dialect the dialect of the numeric defaults
* @since 8.0.1
*/
public ColumnBinder(
MetadataBuildingContext metadataBuildingContext,
PersistentEntityNamingStrategy namingStrategy,
Dialect dialect) {
this(
new ColumnNameForPropertyAndPathFetcher(
namingStrategy, new DefaultColumnNameFetcher(namingStrategy), new BackticksRemover()),
columnNameFetcher(metadataBuildingContext, namingStrategy),
new StringColumnConstraintsBinder(),
new NumericColumnConstraintsBinder(dialect),
new CreateKeyForProps(new ColumnNameForPropertyAndPathFetcher(
namingStrategy, new DefaultColumnNameFetcher(namingStrategy), new BackticksRemover())),
new CreateKeyForProps(columnNameFetcher(metadataBuildingContext, namingStrategy)),
new IndexBinder());
}

private static ColumnNameForPropertyAndPathFetcher columnNameFetcher(
MetadataBuildingContext metadataBuildingContext, PersistentEntityNamingStrategy namingStrategy) {
return new ColumnNameForPropertyAndPathFetcher(
namingStrategy, new DefaultColumnNameFetcher(namingStrategy), new BackticksRemover(),
metadataBuildingContext);
}

/**
* Binds a Column instance to the Hibernate meta model
*
Expand All @@ -95,6 +127,82 @@ public void bindColumn(
Table table,
String typeName) {

String columnName = bindColumnDefinition(property, parentProperty, column, cc, path, table, typeName);
createKeyForProps.createKeyForProps(property, path, table, columnName);

// Apply uniqueness last to ensure it isn't overridden by downstream binders
PropertyConfig mappedFormFinal = property.getHibernateMappedForm();
column.setUnique(mappedFormFinal.isUnique() && !mappedFormFinal.isUniqueWithinGroup());
}

/**
* Binds one of the columns of a property that spans several columns, such as one mapped with a composite
* user type, as {@link #bindColumn} does, except for what describes the whole value of the property rather
* than the column. The column is not unique on its own and no unique key is created for it, since two
* different values of the property can share the value of one column; the caller applies the uniqueness once,
* across all the columns of the property, through {@link CreateKeyForProps#createKeyForColumns}. The
* constraints of the property, such as its size, its bounds and its scale, are not applied to the column
* either: it takes its length, precision and scale from its own configuration, and a numeric column without a
* configured precision gets the default precision of the dialect, as the column of a numeric property without
* constraints does.
*
* @param property The Grails domain class property
* @param parentProperty parent property
* @param column The column to bind
* @param cc The configuration of the column
* @param path the path
* @param table The table name
* @param typeName the property's resolved Hibernate type name
* @param columnType the type of the value the column holds, such as the type of the attribute of the
* embeddable class of a composite user type
*/
public void bindColumnWithoutUniqueness(
HibernatePersistentProperty property,
HibernatePersistentProperty parentProperty,
Column column,
ColumnConfig cc,
String path,
Table table,
String typeName,
Class<?> columnType) {
bindColumnDefinition(property, parentProperty, column, cc, path, table, typeName, columnType);
column.setUnique(false);
}

/**
* Binds everything but the uniqueness of the one column of a property, whose constraints apply to the column.
*
* @return the name of the column of the property
*/
private String bindColumnDefinition(
HibernatePersistentProperty property,
HibernatePersistentProperty parentProperty,
Column column,
ColumnConfig cc,
String path,
Table table,
String typeName) {
return bindColumnDefinition(property, parentProperty, column, cc, path, table, typeName, null);
}

/**
* Binds everything but the uniqueness of the column.
*
* @param columnType the type of the value the column holds when it is one of several columns of the property,
* which is then defined from its own configuration; {@code null} for the one column of a property, which is
* defined from the constraints of the property
* @return the name of the column of the property
*/
private String bindColumnDefinition(
HibernatePersistentProperty property,
HibernatePersistentProperty parentProperty,
Column column,
ColumnConfig cc,
String path,
Table table,
String typeName,
Class<?> columnType) {

if (cc != null) {
column.setComment(cc.getComment());
column.setDefaultValue(cc.getDefaultValue());
Expand All @@ -113,19 +221,13 @@ public void bindColumn(
} else {
column.setName(columnName);
column.setNullable(property.isNullable() || (parentProperty != null && parentProperty.isNullable()));
// Use the constraints for this property to more accurately define
// the column's length, precision, and scale
Class<?> type = property.getType();
if (type != null && (String.class.isAssignableFrom(type) || byte[].class.isAssignableFrom(type))) {
PropertyConfig mappedForm = property.getHibernateMappedForm();
stringColumnConstraintsBinder.bindStringColumnConstraints(column, mappedForm, typeName);
} else if (type != null && Number.class.isAssignableFrom(type)) {
PropertyConfig mappedForm = property.getHibernateMappedForm();
numericColumnConstraintsBinder.bindNumericColumnConstraints(column, cc, mappedForm, type);
if (columnType != null) {
bindAttributeColumnConstraints(column, cc, columnType);
} else {
bindPropertyColumnConstraints(property, column, cc, typeName);
}
}

createKeyForProps.createKeyForProps(property, path, table, columnName);
indexBinder.bindIndex(columnName, column, cc, table);

var owner = property.getHibernateOwner();
Expand All @@ -141,12 +243,37 @@ public void bindColumn(
}
}

// Apply uniqueness last to ensure it isn't overridden by downstream binders
PropertyConfig mappedFormFinal = property.getHibernateMappedForm();
column.setUnique(mappedFormFinal.isUnique() && !mappedFormFinal.isUniqueWithinGroup());

if (LOG.isDebugEnabled()) {
LOG.debug("Bound property [{}] to column name [{}] in table [{}]", property.getName(), column.getName(), table.getName());
}
return columnName;
}

/**
* Uses the constraints of the property to define the length, the precision and the scale of its one column
* more accurately.
*/
private void bindPropertyColumnConstraints(
HibernatePersistentProperty property, Column column, ColumnConfig cc, String typeName) {
Class<?> type = property.getType();
if (type != null && (String.class.isAssignableFrom(type) || byte[].class.isAssignableFrom(type))) {
PropertyConfig mappedForm = property.getHibernateMappedForm();
stringColumnConstraintsBinder.bindStringColumnConstraints(column, mappedForm, typeName);
} else if (type != null && Number.class.isAssignableFrom(type)) {
PropertyConfig mappedForm = property.getHibernateMappedForm();
numericColumnConstraintsBinder.bindNumericColumnConstraints(column, cc, mappedForm, type);
}
}

/**
* Defines one of several columns of a property from its own configuration alone: the constraints of the
* property, such as its size, its bounds and its scale, describe the whole value, not the column. A numeric
* column without a configured precision gets the default precision of the dialect, as the column of a numeric
* property without constraints does.
*/
private void bindAttributeColumnConstraints(Column column, ColumnConfig cc, Class<?> columnType) {
if (Number.class.isAssignableFrom(columnType)) {
numericColumnConstraintsBinder.bindNumericColumnConstraints(column, cc, null, columnType);
}
}
}
Loading
Loading