You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2025/08/29 02:50:05 UTC

(grails-core) 02/04: More fixes and consolidation and testing

This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch merge-hibernate6
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 8eb19bf37e5c7b2a7bdf30c2a3b8e2ebedfba94d
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Thu Aug 28 21:38:04 2025 -0500

    More fixes and consolidation and testing
---
 .../orm/hibernate/AbstractHibernateDatastore.java  |   2 +-
 .../AbstractHibernateGormInstanceApi.groovy        | 480 ---------------------
 .../orm/hibernate/HibernateGormInstanceApi.groovy  | 371 +++++++++++++++-
 .../grails/orm/hibernate/cfg/InstanceProxy.groovy  |   6 +-
 .../support/ClosureEventTriggeringInterceptor.java |   2 +-
 .../specs/validation/UniqueWithHasOneSpec.groovy   |  13 +-
 .../hibernate/HibernateGormInstanceApiSpec.groovy  | 172 ++++++++
 .../data/testing/tck/tests/InheritanceSpec.groovy  |   2 +-
 .../data/testing/tck/tests/ValidationSpec.groovy   |   4 +-
 9 files changed, 550 insertions(+), 502 deletions(-)

diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java
index 6ac2bf79e8..458dd4832b 100644
--- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java
+++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateDatastore.java
@@ -312,7 +312,7 @@ public abstract class AbstractHibernateDatastore extends AbstractDatastore imple
     public void destroy() {
         if (!this.destroyed) {
             super.destroy();
-            AbstractHibernateGormInstanceApi.resetInsertActive();
+            HibernateGormInstanceApi.resetInsertActive();
             try {
                 connectionSources.close();
             } catch (IOException e) {
diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy
deleted file mode 100644
index cd477b9f74..0000000000
--- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy
+++ /dev/null
@@ -1,480 +0,0 @@
-/*
- * Copyright 2013 the original author or authors.
- *
- * Licensed 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
- *
- *      http://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
-
-import grails.gorm.validation.CascadingValidator
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import jakarta.persistence.FlushModeType
-import org.grails.datastore.gorm.GormValidateable
-import org.grails.datastore.mapping.core.Datastore
-import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
-import org.grails.datastore.mapping.model.config.GormProperties
-import org.grails.datastore.mapping.model.types.Embedded
-import org.grails.datastore.mapping.proxy.ProxyHandler
-import org.grails.datastore.mapping.reflect.ClassUtils
-import org.grails.datastore.mapping.reflect.EntityReflector
-import org.grails.orm.hibernate.support.HibernateRuntimeUtils
-import org.grails.datastore.gorm.GormInstanceApi
-import org.grails.datastore.mapping.engine.event.ValidationEvent
-import org.grails.datastore.mapping.model.PersistentEntity
-import org.grails.datastore.mapping.model.PersistentProperty
-import org.grails.datastore.mapping.model.types.Association
-import org.grails.datastore.mapping.model.types.ToOne
-import org.hibernate.*
-import org.springframework.beans.BeanWrapperImpl
-import org.springframework.beans.InvalidPropertyException
-import org.springframework.dao.DataAccessException
-import org.springframework.validation.Errors
-import org.springframework.validation.Validator
-
-
-/**
- * Abstract extension of the {@link GormInstanceApi} class that provides common logic shared by Hibernate 3 and Hibernate 4
- *
- * @author Graeme Rocher
- * @param <D>
- */
-@CompileStatic
-abstract class AbstractHibernateGormInstanceApi<D> extends GormInstanceApi<D> {
-    private static final String ARGUMENT_VALIDATE = "validate"
-    private static final String ARGUMENT_DEEP_VALIDATE = "deepValidate"
-    private static final String ARGUMENT_FLUSH = "flush"
-    private static final String ARGUMENT_INSERT = "insert"
-    private static final String ARGUMENT_MERGE = "merge"
-    private static final String ARGUMENT_FAIL_ON_ERROR = "failOnError"
-    private static final Class DEFERRED_BINDING
-
-    static {
-        try {
-            DEFERRED_BINDING = Class.forName('grails.validation.DeferredBindingActions')
-        } catch (Throwable e) {
-            DEFERRED_BINDING = null
-        }
-    }
-
-    protected static final Object[] EMPTY_ARRAY = []
-    /**
-     * When a domain instance is saved without validation, we put it
-     * into this thread local variable. Any code that needs to know
-     * whether the domain instance should be validated can just check
-     * the value. Note that this only works because the session is
-     * flushed when a domain instance is saved without validation.
-     */
-    static final ThreadLocal<Boolean> insertActiveThreadLocal = new ThreadLocal<Boolean>()
-
-
-
-    protected SessionFactory sessionFactory
-    protected ClassLoader classLoader
-    protected IHibernateTemplate hibernateTemplate
-
-    boolean autoFlush
-
-    protected AbstractHibernateGormInstanceApi(Class<D> persistentClass, AbstractHibernateDatastore datastore, ClassLoader classLoader, IHibernateTemplate hibernateTemplate) {
-        super(persistentClass, datastore)
-        this.classLoader = classLoader
-        sessionFactory = datastore.getSessionFactory()
-        this.hibernateTemplate = hibernateTemplate
-        this.autoFlush = datastore.autoFlush
-        this.failOnError = datastore.failOnError
-        this.markDirty = datastore.markDirty
-    }
-
-    @Override
-    D save(D target, Map arguments) {
-
-        PersistentEntity domainClass = persistentEntity
-        runDeferredBinding()
-        boolean shouldFlush = shouldFlush(arguments)
-        boolean shouldValidate = shouldValidate(arguments, persistentEntity)
-
-        HibernateRuntimeUtils.autoAssociateBidirectionalOneToOnes(domainClass, target)
-
-        boolean deepValidate = true
-        if (arguments?.containsKey(ARGUMENT_DEEP_VALIDATE)) {
-            deepValidate = ClassUtils.getBooleanFromMap(ARGUMENT_DEEP_VALIDATE, arguments)
-        }
-
-        if (shouldValidate) {
-            Validator validator = datastore.mappingContext.getEntityValidator(domainClass)
-
-            Errors errors = HibernateRuntimeUtils.setupErrorsProperty(target)
-
-            if (validator) {
-                datastore.applicationEventPublisher?.publishEvent new ValidationEvent(datastore, target)
-
-                if (validator instanceof CascadingValidator) {
-                    ((CascadingValidator)validator).validate target, errors, deepValidate
-                } else if (validator instanceof org.grails.datastore.gorm.validation.CascadingValidator) {
-                    ((org.grails.datastore.gorm.validation.CascadingValidator) validator).validate target, errors, deepValidate
-                } else {
-                    validator.validate target, errors
-                }
-
-                if (errors.hasErrors()) {
-                    handleValidationError(domainClass,target,errors)
-                    if (shouldFail(arguments)) {
-                        throw validationException.newInstance("Validation Error(s) occurred during save()", errors)
-                    }
-                    return null
-                }
-
-                setObjectToReadWrite(target)
-            }
-        }
-
-        // this piece of code will retrieve a persistent instant
-        // of a domain class property is only the id is set thus
-        // relieving this burden off the developer
-        autoRetrieveAssociations datastore, domainClass, target
-
-        // Once we get here we've either validated this object or skipped validation, either way
-        // we don't need to validate again for the rest of this save.
-        GormValidateable validateable = (GormValidateable) target
-        validateable.skipValidation(true)
-
-        try {
-            if (shouldInsert(arguments)) {
-                return performInsert(target, shouldFlush)
-            }
-            else if(shouldMerge(arguments)) {
-                return performMerge(target, shouldFlush)
-            }
-            else {
-                if (target instanceof DirtyCheckable && markDirty) {
-                    target.markDirty()
-                }
-                return performSave(target, shouldFlush)
-            }
-        } finally {
-            // After save, we have to make sure this entity is setup to validate again. It's possible it will
-            // be validated again if this save didn't flush, but without checking it's dirty state we can't really
-            // know for sure that it hasn't changed and need to err on the side of caution.
-            validateable.skipValidation(false)
-        }
-    }
-
-    @CompileDynamic
-    private void runDeferredBinding() {
-        DEFERRED_BINDING?.runActions()
-    }
-
-    @Override
-    D merge(D instance, Map params) {
-        Map args = new HashMap(params)
-        args[ARGUMENT_MERGE] = true
-        return save(instance, args)
-    }
-
-    @Override
-    D insert(D instance, Map params) {
-        Map args = new HashMap(params)
-        args[ARGUMENT_INSERT] = true
-        return save(instance, args)
-    }
-
-    @Override
-    void discard(D instance) {
-        hibernateTemplate.evict instance
-    }
-
-    @Override
-    void delete(D instance, Map params = Collections.emptyMap()) {
-        boolean flush = shouldFlush(params)
-        try {
-            hibernateTemplate.execute { Session session ->
-                session.delete instance
-                if(flush) {
-                    session.flush()
-                }
-            }
-        }
-        catch (DataAccessException e) {
-            try {
-                hibernateTemplate.execute { Session session ->
-                    session.flushMode = FlushModeType.COMMIT
-                }
-            }
-            finally {
-                throw e
-            }
-        }
-    }
-
-    @Override
-    boolean isAttached(D instance) {
-        hibernateTemplate.contains instance
-    }
-
-    @Override
-    boolean instanceOf(D instance, Class cls) {
-        return super.instanceOf(instance, cls)
-    }
-
-    @Override
-    D lock(D instance) {
-        hibernateTemplate.lock(instance, LockMode.PESSIMISTIC_WRITE)
-        instance
-    }
-
-    @Override
-    D attach(D instance) {
-        hibernateTemplate.lock(instance, LockMode.NONE)
-        return instance
-    }
-
-    @Override
-    D refresh(D instance) {
-        hibernateTemplate.refresh(instance)
-        return instance
-    }
-
-    protected D performSave(final D target, final boolean flush) {
-        hibernateTemplate.execute { Session session ->
-            session.saveOrUpdate target
-            if (flush) {
-                flushSession session
-            }
-            return target
-        }
-    }
-
-    protected D performMerge(final D target, final boolean flush) {
-        hibernateTemplate.execute { Session session ->
-            Object merged = session.merge(target)
-            session.lock(merged, LockMode.NONE)
-            if (flush) {
-                flushSession session
-            }
-            return (D)merged
-        }
-    }
-
-    protected D performInsert(final D target, final boolean shouldFlush) {
-        hibernateTemplate.execute { Session session ->
-            try {
-                markInsertActive()
-                session.save target
-                if (shouldFlush) {
-                    flushSession session
-                }
-                return target
-            } finally {
-                resetInsertActive()
-            }
-
-        }
-    }
-
-    protected void flushSession(Session session) throws HibernateException {
-        try {
-            session.flush()
-        } catch (HibernateException e) {
-            // session should not be flushed again after a data access exception!
-            session.setFlushMode FlushModeType.COMMIT
-            throw e
-        }
-    }
-    /**
-     * Performs automatic association retrieval
-     * @param entity The domain class to retrieve associations for
-     * @param target The target object
-     */
-    @SuppressWarnings("unchecked")
-    private void autoRetrieveAssociations(Datastore datastore, PersistentEntity entity, Object target) {
-        EntityReflector reflector = datastore.mappingContext.getEntityReflector(entity)
-        IHibernateTemplate t = this.hibernateTemplate
-        for (PersistentProperty prop in entity.associations) {
-            if(prop instanceof ToOne && !(prop instanceof Embedded)) {
-                ToOne toOne = (ToOne)prop
-
-                def propertyName = prop.name
-                def propValue = reflector.getProperty(target, propertyName)
-                if (propValue == null || t.contains(propValue)) {
-                    continue
-                }
-
-                PersistentEntity otherSide = toOne.associatedEntity
-                if (otherSide == null) {
-                    continue
-                }
-
-                def identity = otherSide.identity
-                if(identity == null) {
-                    continue
-                }
-
-                def otherSideReflector = datastore.mappingContext.getEntityReflector(otherSide)
-                try {
-                    def id = (Serializable)otherSideReflector.getProperty(propValue, identity.name);
-                    if (id) {
-                        final Object associatedInstance = t.get(prop.type, id)
-                        if (associatedInstance) {
-                            reflector.setProperty(target, propertyName, associatedInstance)
-                        }
-                    }
-                }
-                catch (InvalidPropertyException ipe) {
-                    // property is not accessable
-                }
-            }
-
-        }
-    }
-
-    /**
-     * Checks whether validation should be performed
-     * @return true if the domain class should be validated
-     * @param arguments  The arguments to the validate method
-     * @param domainClass The domain class
-     */
-    private boolean shouldValidate(Map arguments, PersistentEntity entity) {
-        if (!entity) {
-            return false
-        }
-
-        if (arguments?.containsKey(ARGUMENT_VALIDATE)) {
-            return ClassUtils.getBooleanFromMap(ARGUMENT_VALIDATE, arguments)
-        }
-        return true
-    }
-
-    private boolean shouldInsert(Map arguments) {
-        ClassUtils.getBooleanFromMap(ARGUMENT_INSERT, arguments)
-    }
-
-    private boolean shouldMerge(Map arguments) {
-        ClassUtils.getBooleanFromMap(ARGUMENT_MERGE, arguments)
-    }
-
-
-    protected boolean shouldFlush(Map map) {
-        if (map?.containsKey(ARGUMENT_FLUSH)) {
-            return ClassUtils.getBooleanFromMap(ARGUMENT_FLUSH, map)
-        }
-        return autoFlush
-    }
-
-    protected boolean shouldFail(Map map) {
-        if (map?.containsKey(ARGUMENT_FAIL_ON_ERROR)) {
-            return ClassUtils.getBooleanFromMap(ARGUMENT_FAIL_ON_ERROR, map)
-        }
-        return failOnError
-    }
-
-    /**
-     * Sets the flush mode to manual. which ensures that the database changes are not persisted to the database
-     * if a validation error occurs. If save() is called again and validation passes the code will check if there
-     * is a manual flush mode and flush manually if necessary
-     *
-     * @param domainClass The domain class
-     * @param target The target object that failed validation
-     * @param errors The Errors instance  @return This method will return null signaling a validation failure
-     */
-    protected Object handleValidationError(PersistentEntity entity, final Object target, Errors errors) {
-        // if a validation error occurs set the object to read-only to prevent a flush
-        setObjectToReadOnly target
-        if (entity) {
-            for (Association association in entity.associations) {
-                if (association instanceof ToOne && !association instanceof Embedded) {
-                        def bean = new BeanWrapperImpl(target)
-                        def propertyValue = bean.getPropertyValue(association.name)
-                        if (propertyValue != null) {
-                            setObjectToReadOnly propertyValue
-                        }
-                }
-            }
-        }
-        setErrorsOnInstance target, errors
-        return null
-    }
-
-    /**
-     * Sets the target object to read-only using the given SessionFactory instance. This
-     * avoids Hibernate performing any dirty checking on the object
-     *
-     *
-     * @param target The target object
-     * @param sessionFactory The SessionFactory instance
-     */
-    void setObjectToReadOnly(Object target) {
-        hibernateTemplate.execute { Session session ->
-            if (session.contains(target)) {
-                session.setReadOnly target, true
-                session.flushMode = FlushModeType.COMMIT
-            }
-        }
-    }
-    /**
-     * Sets the target object to read-write, allowing Hibernate to dirty check it and auto-flush changes.
-     *
-     * @see #setObjectToReadOnly(Object)
-     *
-     * @param target The target object
-     * @param sessionFactory The SessionFactory instance
-     */
-    abstract void setObjectToReadWrite(Object target)
-
-    /**
-     * Associates the Errors object on the instance
-     *
-     * @param target The target instance
-     * @param errors The Errors object
-     */
-    @CompileDynamic
-    protected void setErrorsOnInstance(Object target, Errors errors) {
-        if(target instanceof GormValidateable) {
-            ((GormValidateable)target).setErrors(errors)
-        }
-        else {
-            target."$GormProperties.ERRORS" = errors
-        }
-    }
-
-    /**
-     * Called by org.grails.orm.hibernate.metaclass.SavePersistentMethod's performInsert
-     * to set a ThreadLocal variable that determines the value for getAssumedUnsaved().
-     */
-    static void markInsertActive() {
-        insertActiveThreadLocal.set(Boolean.TRUE);
-    }
-
-    /**
-     * Clears the ThreadLocal variable set by markInsertActive().
-     */
-    static void resetInsertActive() {
-        insertActiveThreadLocal.remove();
-    }
-
-    /**
-     * Increments the entities version number in order to force an update
-     * @param target The target entity
-     */
-    @CompileDynamic
-    protected void incrementVersion(Object target) {
-        if (target.hasProperty(GormProperties.VERSION)) {
-            Object version = target."${GormProperties.VERSION}"
-            if (version instanceof Long) {
-                target."${GormProperties.VERSION}" = ++((Long)version)
-            }
-        }
-    }
-
-    SessionFactory getSessionFactory() {
-        return this.sessionFactory
-    }
-}
diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
index 1eb8961fd8..f0d5f96314 100644
--- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
+++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
@@ -15,13 +15,40 @@
  */
 package org.grails.orm.hibernate
 
+import grails.gorm.validation.CascadingValidator
 import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
+import jakarta.persistence.FlushModeType
+import org.grails.datastore.gorm.GormInstanceApi
+import org.grails.datastore.gorm.GormValidateable
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
+import org.grails.datastore.mapping.model.config.GormProperties
+import org.grails.datastore.mapping.model.types.Embedded
+import org.grails.datastore.mapping.proxy.ProxyHandler
+import org.grails.datastore.mapping.reflect.ClassUtils
+import org.grails.datastore.mapping.reflect.EntityReflector
+import org.grails.datastore.mapping.engine.event.ValidationEvent
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.types.Association
+import org.grails.datastore.mapping.model.types.ToOne
 import org.grails.orm.hibernate.cfg.GrailsHibernateUtil
+import org.grails.orm.hibernate.support.HibernateRuntimeUtils
+import org.hibernate.FlushMode
+import org.hibernate.HibernateException
+import org.hibernate.LockMode
+import org.hibernate.Session
+import org.hibernate.SessionFactory
 import org.hibernate.engine.spi.EntityEntry
 import org.hibernate.engine.spi.SessionImplementor
 import org.hibernate.persister.entity.EntityPersister
 import org.hibernate.tuple.NonIdentifierAttribute
+import org.springframework.beans.BeanWrapperImpl
+import org.springframework.beans.InvalidPropertyException
+import org.springframework.dao.DataAccessException
+import org.springframework.validation.Errors
+import org.springframework.validation.Validator
 
 /**
  * The implementation of the GORM instance API contract for Hibernate.
@@ -30,14 +57,348 @@ import org.hibernate.tuple.NonIdentifierAttribute
  * @since 1.0
  */
 @CompileStatic
-class HibernateGormInstanceApi<D> extends AbstractHibernateGormInstanceApi<D> {
+class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
+
+    private static final String ARGUMENT_VALIDATE = "validate"
+    private static final String ARGUMENT_DEEP_VALIDATE = "deepValidate"
+    private static final String ARGUMENT_FLUSH = "flush"
+    private static final String ARGUMENT_INSERT = "insert"
+    private static final String ARGUMENT_MERGE = "merge"
+    private static final String ARGUMENT_FAIL_ON_ERROR = "failOnError"
+    private static final Class DEFERRED_BINDING
+
+    static {
+        try {
+            DEFERRED_BINDING = Class.forName('grails.validation.DeferredBindingActions')
+        } catch (Throwable e) {
+            DEFERRED_BINDING = null
+        }
+    }
+
+    protected static final Object[] EMPTY_ARRAY = []
+    static final ThreadLocal<Boolean> insertActiveThreadLocal = new ThreadLocal<Boolean>()
+
+    protected SessionFactory sessionFactory
+    protected ClassLoader classLoader
+    protected IHibernateTemplate hibernateTemplate
+
+    boolean autoFlush
 
     protected InstanceApiHelper instanceApiHelper
 
     HibernateGormInstanceApi(Class<D> persistentClass, HibernateDatastore datastore, ClassLoader classLoader) {
-        super(persistentClass, datastore, classLoader, null)
-        hibernateTemplate = new GrailsHibernateTemplate(sessionFactory, datastore)
-        instanceApiHelper = new InstanceApiHelper((GrailsHibernateTemplate)hibernateTemplate)
+        super(persistentClass, datastore as Datastore)
+        this.classLoader = classLoader
+        this.sessionFactory = datastore.getSessionFactory()
+        this.hibernateTemplate = new GrailsHibernateTemplate(sessionFactory, datastore)
+        this.autoFlush = datastore.autoFlush
+        this.failOnError = datastore.failOnError
+        this.markDirty = datastore.markDirty
+        this.instanceApiHelper = new InstanceApiHelper((GrailsHibernateTemplate) this.hibernateTemplate)
+    }
+
+    @Override
+    D save(D target, Map arguments) {
+        PersistentEntity domainClass = persistentEntity
+        runDeferredBinding()
+        boolean shouldFlush = shouldFlush(arguments)
+        boolean shouldValidate = shouldValidate(arguments, persistentEntity)
+
+        HibernateRuntimeUtils.autoAssociateBidirectionalOneToOnes(domainClass, target)
+
+        boolean deepValidate = true
+        if (arguments?.containsKey(ARGUMENT_DEEP_VALIDATE)) {
+            deepValidate = ClassUtils.getBooleanFromMap(ARGUMENT_DEEP_VALIDATE, arguments)
+        }
+
+        if (shouldValidate) {
+            Validator validator = datastore.mappingContext.getEntityValidator(domainClass)
+
+            Errors errors = HibernateRuntimeUtils.setupErrorsProperty(target)
+
+            if (validator) {
+                datastore.applicationEventPublisher?.publishEvent new ValidationEvent(datastore, target)
+
+                if (validator instanceof CascadingValidator) {
+                    ((CascadingValidator)validator).validate target, errors, deepValidate
+                } else if (validator instanceof org.grails.datastore.gorm.validation.CascadingValidator) {
+                    ((org.grails.datastore.gorm.validation.CascadingValidator) validator).validate target, errors, deepValidate
+                } else {
+                    validator.validate target, errors
+                }
+
+                if (errors.hasErrors()) {
+                    handleValidationError(domainClass,target,errors)
+                    if (shouldFail(arguments)) {
+                        throw validationException.newInstance("Validation Error(s) occurred during save()", errors)
+                    }
+                    return null
+                }
+
+                setObjectToReadWrite(target)
+            }
+        }
+
+        autoRetrieveAssociations datastore, domainClass, target
+
+        GormValidateable validateable = (GormValidateable) target
+        validateable.skipValidation(true)
+
+        try {
+            if (shouldInsert(arguments)) {
+                return performInsert(target, shouldFlush)
+            }
+            else if(shouldMerge(arguments)) {
+                return performMerge(target, shouldFlush)
+            }
+            else {
+                if (target instanceof DirtyCheckable && markDirty) {
+                    target.markDirty()
+                }
+                return performSave(target, shouldFlush)
+            }
+        } finally {
+            validateable.skipValidation(false)
+        }
+    }
+
+    @CompileDynamic
+    private void runDeferredBinding() {
+        DEFERRED_BINDING?.runActions()
+    }
+
+    @Override
+    D merge(D instance, Map params) {
+        Map args = new HashMap(params)
+        args[ARGUMENT_MERGE] = true
+        return save(instance, args)
+    }
+
+    @Override
+    D insert(D instance, Map params) {
+        Map args = new HashMap(params)
+        args[ARGUMENT_INSERT] = true
+        return save(instance, args)
+    }
+
+    @Override
+    void discard(D instance) {
+        hibernateTemplate.evict instance
+    }
+
+    @Override
+    void delete(D instance, Map params = Collections.emptyMap()) {
+        boolean flush = shouldFlush(params)
+        try {
+            hibernateTemplate.execute { Session session ->
+                session.delete instance
+                if(flush) {
+                    session.flush()
+                }
+            }
+        }
+        catch (DataAccessException e) {
+            try {
+                hibernateTemplate.execute { Session session ->
+                    session.flushMode = FlushModeType.COMMIT
+                }
+            }
+            finally {
+                throw e
+            }
+        }
+    }
+
+    @Override
+    boolean isAttached(D instance) {
+        hibernateTemplate.contains instance
+    }
+
+    @Override
+    D lock(D instance) {
+        hibernateTemplate.lock(instance, LockMode.PESSIMISTIC_WRITE)
+        instance
+    }
+
+    @Override
+    D attach(D instance) {
+        hibernateTemplate.lock(instance, LockMode.NONE)
+        return instance
+    }
+
+    @Override
+    D refresh(D instance) {
+        hibernateTemplate.refresh(instance)
+        return instance
+    }
+
+    protected D performSave(final D target, final boolean flush) {
+        hibernateTemplate.execute { Session session ->
+            session.saveOrUpdate target
+            if (flush) {
+                flushSession session
+            }
+            return target
+        }
+    }
+
+    protected D performMerge(final D target, final boolean flush) {
+        hibernateTemplate.execute { Session session ->
+            Object merged = session.merge(target)
+            session.lock(merged, LockMode.NONE)
+            if (flush) {
+                flushSession session
+            }
+            return (D)merged
+        }
+    }
+
+    protected D performInsert(final D target, final boolean shouldFlush) {
+        hibernateTemplate.execute { Session session ->
+            try {
+                markInsertActive()
+                session.save target
+                if (shouldFlush) {
+                    flushSession session
+                }
+                return target
+            } finally {
+                resetInsertActive()
+            }
+
+        }
+    }
+
+    protected void flushSession(Session session) throws HibernateException {
+        try {
+            session.flush()
+        } catch (HibernateException e) {
+            session.setFlushMode FlushModeType.COMMIT
+            throw e
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void autoRetrieveAssociations(Datastore datastore, PersistentEntity entity, Object target) {
+        EntityReflector reflector = datastore.mappingContext.getEntityReflector(entity)
+        IHibernateTemplate t = this.hibernateTemplate
+        for (PersistentProperty prop in entity.associations) {
+            if(prop instanceof ToOne && !(prop instanceof Embedded)) {
+                ToOne toOne = (ToOne)prop
+
+                def propertyName = prop.name
+                def propValue = reflector.getProperty(target, propertyName)
+                if (propValue == null || t.contains(propValue)) {
+                    continue
+                }
+
+                PersistentEntity otherSide = toOne.associatedEntity
+                if (otherSide == null) {
+                    continue
+                }
+
+                def identity = otherSide.identity
+                if(identity == null) {
+                    continue
+                }
+
+                def otherSideReflector = datastore.mappingContext.getEntityReflector(otherSide)
+                try {
+                    def id = (Serializable)otherSideReflector.getProperty(propValue, identity.name);
+                    if (id) {
+                        final Object associatedInstance = t.get(prop.type, id)
+                        if (associatedInstance) {
+                            reflector.setProperty(target, propertyName, associatedInstance)
+                        }
+                    }
+                }
+                catch (InvalidPropertyException ipe) {
+                    // property is not accessable
+                }
+            }
+
+        }
+    }
+
+    private boolean shouldValidate(Map arguments, PersistentEntity entity) {
+        if (!entity) {
+            return false
+        }
+        if (arguments?.containsKey(ARGUMENT_VALIDATE)) {
+            return ClassUtils.getBooleanFromMap(ARGUMENT_VALIDATE, arguments)
+        }
+        return true
+    }
+
+    private boolean shouldInsert(Map arguments) {
+        ClassUtils.getBooleanFromMap(ARGUMENT_INSERT, arguments)
+    }
+
+    private boolean shouldMerge(Map arguments) {
+        ClassUtils.getBooleanFromMap(ARGUMENT_MERGE, arguments)
+    }
+
+    protected boolean shouldFlush(Map map) {
+        if (map?.containsKey(ARGUMENT_FLUSH)) {
+            return ClassUtils.getBooleanFromMap(ARGUMENT_FLUSH, map)
+        }
+        return autoFlush
+    }
+
+    protected boolean shouldFail(Map map) {
+        if (map?.containsKey(ARGUMENT_FAIL_ON_ERROR)) {
+            return ClassUtils.getBooleanFromMap(ARGUMENT_FAIL_ON_ERROR, map)
+        }
+        return failOnError
+    }
+
+    protected Object handleValidationError(PersistentEntity entity, final Object target, Errors errors) {
+        setObjectToReadOnly target
+        if (entity) {
+            for (Association association in entity.associations) {
+                if (association instanceof ToOne && !association instanceof Embedded) {
+                        def bean = new BeanWrapperImpl(target)
+                        def propertyValue = bean.getPropertyValue(association.name)
+                        if (propertyValue != null) {
+                            setObjectToReadOnly propertyValue
+                        }
+                }
+            }
+        }
+        setErrorsOnInstance target, errors
+        return null
+    }
+
+    @CompileDynamic
+    protected void setErrorsOnInstance(Object target, Errors errors) {
+        if(target instanceof GormValidateable) {
+            ((GormValidateable)target).setErrors(errors)
+        }
+        else {
+            target."$GormProperties.ERRORS" = errors
+        }
+    }
+
+    static void markInsertActive() {
+        insertActiveThreadLocal.set(Boolean.TRUE);
+    }
+
+    static void resetInsertActive() {
+        insertActiveThreadLocal.remove();
+    }
+
+    @CompileDynamic
+    protected void incrementVersion(Object target) {
+        if (target.hasProperty(GormProperties.VERSION)) {
+            Object version = target."${GormProperties.VERSION}"
+            if (version instanceof Long) {
+                target."${GormProperties.VERSION}" = ++((Long)version)
+            }
+        }
+    }
+
+    SessionFactory getSessionFactory() {
+        return this.sessionFactory
     }
 
     /**
@@ -153,12 +514,10 @@ class HibernateGormInstanceApi<D> extends AbstractHibernateGormInstanceApi<D> {
         return entry
     }
 
-    @Override
     void setObjectToReadWrite(Object target) {
         GrailsHibernateUtil.setObjectToReadWrite(target, sessionFactory)
     }
 
-    @Override
     void setObjectToReadOnly(Object target) {
         GrailsHibernateUtil.setObjectToReadyOnly(target, sessionFactory)
     }
diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy
index 5ec0edd541..3a74b36d12 100644
--- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy
+++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/cfg/InstanceProxy.groovy
@@ -17,18 +17,18 @@ package org.grails.orm.hibernate.cfg
 
 import groovy.transform.CompileStatic
 
-import org.grails.orm.hibernate.AbstractHibernateGormInstanceApi
+import org.grails.orm.hibernate.HibernateGormInstanceApi
 import org.grails.orm.hibernate.AbstractHibernateGormValidationApi
 
 @CompileStatic
 class InstanceProxy {
     protected instance
     protected AbstractHibernateGormValidationApi validateApi
-    protected AbstractHibernateGormInstanceApi instanceApi
+    protected HibernateGormInstanceApi instanceApi
 
     protected final Set<String> validateMethods
 
-    InstanceProxy(instance, AbstractHibernateGormInstanceApi instanceApi, AbstractHibernateGormValidationApi validateApi) {
+    InstanceProxy(instance, HibernateGormInstanceApi instanceApi, AbstractHibernateGormValidationApi validateApi) {
         this.instance = instance
         this.instanceApi = instanceApi
         this.validateApi = validateApi
diff --git a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
index 7905ac9c31..fb660c3dd1 100644
--- a/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
+++ b/grails-data-hibernate6/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
@@ -27,7 +27,7 @@ import org.grails.datastore.mapping.model.PersistentEntity;
 import org.grails.datastore.mapping.model.types.Embedded;
 import org.grails.datastore.mapping.proxy.ProxyHandler;
 import org.grails.orm.hibernate.AbstractHibernateDatastore;
-import org.grails.orm.hibernate.AbstractHibernateGormInstanceApi;
+import org.grails.orm.hibernate.HibernateGormInstanceApi;
 import org.hibernate.Hibernate;
 import org.hibernate.HibernateException;
 import org.hibernate.event.spi.AbstractEvent;
diff --git a/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/validation/UniqueWithHasOneSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/validation/UniqueWithHasOneSpec.groovy
index 46cdd36900..c5dd171c1f 100644
--- a/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/validation/UniqueWithHasOneSpec.groovy
+++ b/grails-data-hibernate6/core/src/test/groovy/grails/gorm/specs/validation/UniqueWithHasOneSpec.groovy
@@ -52,26 +52,23 @@ class UniqueWithHasOneSpec extends HibernateGormDatastoreSpec {
 @Entity
 class Foo {
 
-    static hasOne = [bar: Bar]
-
     String name
+    Bar bar
+
+    static hasOne = [bar: Bar]
 
     static constraints = {
         bar nullable: true
-        name unique: "bar"
+        name unique: ["bar"]
     }
-
 }
 
 @Entity
 class Bar {
 
-    static belongsTo = [foo: Foo]
-
     String name
+    static belongsTo = [ foo: Foo]
 
     static constraints = {
-        foo nullable: true
     }
-
 }
diff --git a/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
new file mode 100644
index 0000000000..48b4a5e897
--- /dev/null
+++ b/grails-data-hibernate6/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
@@ -0,0 +1,172 @@
+// HibernateGormInstanceApiSpec.groovy
+package org.grails.orm.hibernate
+
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import grails.gorm.specs.entities.Club
+import grails.persistence.Entity
+import org.springframework.validation.Errors
+
+class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
+
+    void setupSpec() {
+        manager.addAllDomainClasses([HibernateGormInstanceApiSpecPerson])
+    }
+
+
+    def setup() {
+        new HibernateGormInstanceApiSpecPerson(firstName: "Fred", lastName: "Flintstone", age: 40).save(flush: true)
+        new HibernateGormInstanceApiSpecPerson(firstName: "Wilma", lastName: "Flintstone", age: 35).save(flush: true)
+    }
+
+    void "test save with validation success"() {
+        given:
+        def person = new HibernateGormInstanceApiSpecPerson(firstName: "Barney", lastName: "Rubble", age: 38)
+        when:
+        def result = person.save()
+        then:
+        result != null
+        result.id != null
+    }
+
+    void "test save with validation failure"() {
+        given:
+        def person = new HibernateGormInstanceApiSpecPerson(firstName: null, lastName: "Rubble", age: 38)
+        when:
+        def result = person.save()
+        then:
+        result == null
+        person.errors.hasErrors()
+    }
+
+    void "test save with failOnError"() {
+        given:
+        def person = new HibernateGormInstanceApiSpecPerson(firstName: null, lastName: "Rubble", age: 38)
+        when:
+        person.save(failOnError: true)
+        then:
+        thrown(Exception)
+    }
+
+    void "test merge"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Fred")
+        person.lastName = "Smith"
+        when:
+        def merged = person.merge()
+        then:
+        merged.lastName == "Smith"
+    }
+
+    void "test insert"() {
+        given:
+        def person = new HibernateGormInstanceApiSpecPerson(firstName: "Betty", lastName: "Rubble", age: 36)
+        when:
+        def inserted = person.insert()
+        then:
+        inserted.id != null
+    }
+
+    void "test discard"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        when:
+        person.discard()
+        then:
+        !person.isAttached()
+    }
+
+    void "test delete"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Fred")
+        when:
+        person.delete(flush: true)
+        then:
+        HibernateGormInstanceApiSpecPerson.findByFirstName("Fred") == null
+    }
+
+    void "test isAttached"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        expect:
+        person.isAttached()
+    }
+
+    void "test lock and attach"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        when:
+        def locked = person.lock()
+        def attached = person.attach()
+        then:
+        locked == person
+        attached == person
+    }
+
+    void "test refresh"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        person.lastName = "Changed"
+        when:
+        person.refresh()
+        then:
+        person.lastName == "Flintstone"
+    }
+
+    void "test isDirty for field"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        person.lastName = "Changed"
+        expect:
+        person.isDirty("lastName")
+    }
+
+    void "test isDirty for instance"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        person.lastName = "Changed"
+        expect:
+        person.isDirty()
+    }
+
+    void "test getDirtyPropertyNames"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        person.lastName = "Changed"
+        when:
+        def dirtyNames = person.getDirtyPropertyNames()
+        then:
+        dirtyNames.contains("lastName")
+    }
+
+    void "test getPersistentValue"() {
+        given:
+        def person = HibernateGormInstanceApiSpecPerson.findByFirstName("Wilma")
+        def originalLastName = person.lastName
+        person.lastName = "Changed"
+        when:
+        def persistedValue = person.getPersistentValue("lastName")
+        then:
+        persistedValue == originalLastName
+    }
+
+    void "test handleValidationError sets errors"() {
+        given:
+        def person = new HibernateGormInstanceApiSpecPerson(firstName: null, lastName: "Rubble", age: 38)
+        person.save()
+        expect:
+        person.errors instanceof Errors
+        person.errors.hasErrors()
+    }
+}
+
+@Entity
+class HibernateGormInstanceApiSpecPerson {
+    String firstName
+    String lastName
+    Integer age
+
+    static constraints = {
+        firstName nullable: false
+        age min: 0
+    }
+}
\ No newline at end of file
diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/InheritanceSpec.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/InheritanceSpec.groovy
index 21666bbbc5..1e7cc9a01d 100644
--- a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/InheritanceSpec.groovy
+++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/InheritanceSpec.groovy
@@ -30,7 +30,7 @@ import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
 class InheritanceSpec extends GrailsDataTckSpec {
 
     void setupSpec() {
-        manager.domainClasses += [Practice]
+        manager.addAllDomainClasses([Practice,City,Country,Location])
     }
 
     void "Test inheritance with dynamic finder"() {
diff --git a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/ValidationSpec.groovy b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/ValidationSpec.groovy
index 01874dc5a6..59babe6346 100644
--- a/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/ValidationSpec.groovy
+++ b/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/ValidationSpec.groovy
@@ -37,8 +37,8 @@ import spock.lang.Unroll
  */
 class ValidationSpec extends GrailsDataTckSpec {
     void setupSpec() {
-        manager.domainClasses = [ClassWithListArgBeforeValidate, ClassWithNoArgBeforeValidate,
-                                 ClassWithOverloadedBeforeValidate, TestEntity, ChildEntity, Task]
+        manager.addAllDomainClasses([ClassWithListArgBeforeValidate, ClassWithNoArgBeforeValidate,
+                                 ClassWithOverloadedBeforeValidate, TestEntity, ChildEntity, Task])
     }
 
     // Hibernate did not originally have this test and it fails for it