jamesfredley commented on code in PR #16695: URL: https://github.com/apache/grails-core/pull/16695#discussion_r4339129889
########## grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeUserTypeBinder.java: ########## @@ -0,0 +1,295 @@ +/* + * 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.binder; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.TreeMap; + +import org.hibernate.MappingException; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.Table; +import org.hibernate.property.access.internal.PropertyAccessStrategyCompositeUserTypeImpl; +import org.hibernate.property.access.spi.PropertyAccessStrategy; +import org.hibernate.usertype.CompositeUserType; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.ReflectionUtils; + +import org.grails.datastore.mapping.model.config.GormProperties; +import org.grails.datastore.mapping.reflect.ClassPropertyFetcher; +import org.grails.orm.hibernate.cfg.ColumnConfig; +import org.grails.orm.hibernate.cfg.GrailsHibernateUtil; +import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy; +import org.grails.orm.hibernate.cfg.PropertyConfig; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty; +import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover; + +/** + * Binds a property mapped with a Hibernate {@link CompositeUserType} as a {@link Component}. + * <p> + * Hibernate resolves the type of a {@link BasicValue} as a {@link org.hibernate.type.BasicType} or a + * {@link org.hibernate.usertype.UserType} only. A composite user type spans one column per attribute of its + * {@linkplain CompositeUserType#embeddable() embeddable class}, so it is a component whose type name is the + * user type, as Hibernate binds a {@link org.hibernate.annotations.CompositeType}: one property per attribute, + * in alphabetical attribute order, read through {@link CompositeUserType#getPropertyValue(Object, int)} with the + * index of the attribute in that order and assembled through + * {@link CompositeUserType#instantiate(org.hibernate.metamodel.spi.ValueAccess)}. The columns of the property + * mapping are assigned to the attributes in the same order; an attribute without a configured column is named + * after the property and the attribute. + * + * @since 8.0.1 + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class CompositeUserTypeBinder { + + private static final Logger LOG = LoggerFactory.getLogger(CompositeUserTypeBinder.class); + private static final String UNDERSCORE = "_"; + + private final MetadataBuildingContext metadataBuildingContext; + private final PersistentEntityNamingStrategy namingStrategy; + private final ColumnConfigToColumnBinder columnConfigToColumnBinder; + private final ColumnBinder columnBinder; + private final BackticksRemover backticksRemover; + + public CompositeUserTypeBinder( + MetadataBuildingContext metadataBuildingContext, + PersistentEntityNamingStrategy namingStrategy, + JdbcEnvironment jdbcEnvironment) { + this( + metadataBuildingContext, + namingStrategy, + new ColumnConfigToColumnBinder(), + new ColumnBinder(namingStrategy, jdbcEnvironment.getDialect()), + new BackticksRemover()); + } + + protected CompositeUserTypeBinder( + MetadataBuildingContext metadataBuildingContext, + PersistentEntityNamingStrategy namingStrategy, + ColumnConfigToColumnBinder columnConfigToColumnBinder, + ColumnBinder columnBinder, + BackticksRemover backticksRemover) { + this.metadataBuildingContext = metadataBuildingContext; + this.namingStrategy = namingStrategy; + this.columnConfigToColumnBinder = columnConfigToColumnBinder; + this.columnBinder = columnBinder; + this.backticksRemover = backticksRemover; + } + + /** + * Binds the property as a component of its composite user type. + * + * @param property the property, mapped with a composite user type + * @param parentProperty the embedded property that holds the property, or {@code null} + * @param path the path of the property within its entity + * @return the component + * @throws MappingException when the property maps more columns than its embeddable class has attributes + */ + public Component bindCompositeUserType( + @NonNull HibernatePersistentProperty property, + HibernatePersistentProperty parentProperty, + String path) { + PersistentClass owner = property.getPersistentClass(); + Class<? extends CompositeUserType<?>> userTypeClass = property.getCompositeUserType(); + if (userTypeClass == null) { + throw new MappingException("Property [" + property.getName() + "] of class [" + owner.getClassName() + + "] is not mapped with a " + CompositeUserType.class.getSimpleName()); + } + CompositeUserType<?> userType = instantiate(userTypeClass); + Class<?> embeddable = userType.embeddable(); + PropertyConfig propertyConfig = property.getHibernateMappedForm(); + Table table = property.getTable(); + + Component component = new Component(metadataBuildingContext, owner); + component.setRoleName(GrailsHibernateUtil.qualify( + property.getHibernateOwner().getJavaClass().getName(), property.getNameForPropertyAndPath(path))); + component.setTypeName(userTypeClass.getName()); + component.setComponentClassName(embeddable.getName()); Review Comment: This registers the embeddable template as the component class. When `embeddable()` is not `returnedClass()`, Hibernate 7's `ComponentType.getReturnedClass()` and the JPA attribute type then describe `CompositeMoneyTemplate` instead of `CompositeMoney`. Whole-value HQL, criteria, and parameter binding see the template. Hibernate's own binder keeps the value class on the component and uses `embeddable()` only to discover attributes. Please set the component class to `userType.returnedClass().getName()` and keep `embeddable` for attribute discovery. The template tests currently assert the wrong class, and their queries use the value class as the template, so they do not catch this. Add a public GORM equality query, criteria equality, and dynamic finder that use the separate template type and assert the JPA attribute Java type is the value class. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
