diff --git a/examples/grails-hibernate-groovy-proxy/build.gradle b/examples/grails-hibernate-groovy-proxy/build.gradle new file mode 100644 index 00000000..2453ab04 --- /dev/null +++ b/examples/grails-hibernate-groovy-proxy/build.gradle @@ -0,0 +1,27 @@ +group "examples" + +dependencies { + implementation "org.yakworks:hibernate-groovy-proxy:$hibernateGroovyProxy" + + implementation "org.springframework.boot:spring-boot-starter-logging" + implementation "org.springframework.boot:spring-boot-autoconfigure" + // implementation "javax.servlet:javax.servlet-api:$servletApiVersion" + implementation "org.grails:grails-core:$grailsVersion" + implementation "org.grails:grails-dependencies:$grailsVersion", { + exclude module:'grails-datastore-simple' + } + implementation "org.grails:grails-web-boot:$grailsVersion" + implementation project(":grails-plugin") + + implementation "org.hibernate:hibernate-core:$hibernate5Version" + + // runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails:$assetPipelineVersion" + runtimeOnly "com.h2database:h2" + runtimeOnly "org.yaml:snakeyaml:$snakeyamlVersion" + runtimeOnly "org.apache.tomcat:tomcat-jdbc:$tomcatVersion" + // runtimeOnly "org.grails.plugins:fields:$fieldsVersion" + // runtimeOnly "org.grails.plugins:scaffolding:$scaffoldingVersion" + + testImplementation "org.grails:grails-gorm-testing-support:$testingSupportVersion" + +} diff --git a/examples/grails-hibernate-groovy-proxy/grails-app/conf/application.yml b/examples/grails-hibernate-groovy-proxy/grails-app/conf/application.yml new file mode 100644 index 00000000..5ea30c00 --- /dev/null +++ b/examples/grails-hibernate-groovy-proxy/grails-app/conf/application.yml @@ -0,0 +1,25 @@ +--- +grails: + profile: web + codegen: + defaultPackage: datasources +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + groovy: + template: + check-template-location: false + main: + allow-circular-references: true + +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + dbCreate: create-drop + url: jdbc:h2:mem:books;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + diff --git a/examples/grails-hibernate-groovy-proxy/grails-app/conf/logback.xml b/examples/grails-hibernate-groovy-proxy/grails-app/conf/logback.xml new file mode 100644 index 00000000..f4e77836 --- /dev/null +++ b/examples/grails-hibernate-groovy-proxy/grails-app/conf/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + UTF-8 + '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex' + + + + + + + \ No newline at end of file diff --git a/examples/grails-hibernate-groovy-proxy/grails-app/domain/example/Customer.groovy b/examples/grails-hibernate-groovy-proxy/grails-app/domain/example/Customer.groovy new file mode 100644 index 00000000..213e0eb3 --- /dev/null +++ b/examples/grails-hibernate-groovy-proxy/grails-app/domain/example/Customer.groovy @@ -0,0 +1,20 @@ +package example + +import grails.compiler.GrailsCompileStatic +import grails.persistence.Entity + +@Entity +@GrailsCompileStatic +class Customer implements Serializable { + + String name + + Customer(Long id, String name) { + this.id = id + this.name = name + } + + static mapping = { + id generator: 'assigned' + } +} diff --git a/examples/grails-hibernate-groovy-proxy/grails-app/init/datasources/Application.groovy b/examples/grails-hibernate-groovy-proxy/grails-app/init/datasources/Application.groovy new file mode 100644 index 00000000..f9bb4396 --- /dev/null +++ b/examples/grails-hibernate-groovy-proxy/grails-app/init/datasources/Application.groovy @@ -0,0 +1,15 @@ +package datasources + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import groovy.transform.CompileStatic +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration + +//@EnableAutoConfiguration(exclude = DataSourceTransactionManagerAutoConfiguration) +@CompileStatic +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application) + } +} \ No newline at end of file diff --git a/examples/grails-hibernate-groovy-proxy/src/test/groovy/example/ProxySpec.groovy b/examples/grails-hibernate-groovy-proxy/src/test/groovy/example/ProxySpec.groovy new file mode 100644 index 00000000..fc61d848 --- /dev/null +++ b/examples/grails-hibernate-groovy-proxy/src/test/groovy/example/ProxySpec.groovy @@ -0,0 +1,40 @@ +package example + +import org.hibernate.Hibernate + +import grails.gorm.transactions.Rollback +import grails.test.hibernate.HibernateSpec + +/** + * Tests Proxy with hibernate-groovy-proxy + */ +class ProxySpec extends HibernateSpec { + + @Rollback + void "Test Proxy"() { + when: + new Customer(1, "Bob").save(failOnError: true, flush: true) + hibernateDatastore.currentSession.clear() + + def proxy + Customer.withNewSession { + proxy = Customer.load(1) + } + + then: + //without ByteBuddyGroovyInterceptor this would normally cause the proxy to init + proxy + proxy.metaClass + proxy.getMetaClass() + !Hibernate.isInitialized(proxy) + //id calls + proxy.id == 1 + proxy.getId() == 1 + proxy["id"] == 1 + !Hibernate.isInitialized(proxy) + // gorms trait implements in the class so no way to tell + // proxy.toString() == "Customer : 1 (proxy)" + // !Hibernate.isInitialized(proxy) + } + +} diff --git a/examples/grails3-hibernate5/src/integration-test/groovy/functional/tests/BookControllerSpec.groovy b/examples/grails3-hibernate5/src/integration-test/groovy/functional/tests/BookControllerSpec.groovy index 3b260313..ee023939 100644 --- a/examples/grails3-hibernate5/src/integration-test/groovy/functional/tests/BookControllerSpec.groovy +++ b/examples/grails3-hibernate5/src/integration-test/groovy/functional/tests/BookControllerSpec.groovy @@ -2,8 +2,10 @@ package functional.tests import grails.testing.mixin.integration.Integration import geb.spock.GebSpec +import spock.lang.Ignore @Integration(applicationClass = Application) +@Ignore //FAILING downloading the firefox driver class BookControllerSpec extends GebSpec { void "Test list books"() { diff --git a/gradle.properties b/gradle.properties index 7b7896a9..70b1c229 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,7 @@ groovyVersion=3.0.11 h2Version=1.4.200 hibernate5Version=5.6.11.Final hibernateValidatorVersion=6.2.5.Final +hibernateGroovyProxy=1.1 jansiVersion=2.4.0 javaParserCoreVersion=3.23.0 jaxbVersion=2.3.1 diff --git a/grails-datastore-gorm-hibernate5/build.gradle b/grails-datastore-gorm-hibernate5/build.gradle index 1dcfc670..4ae7c008 100644 --- a/grails-datastore-gorm-hibernate5/build.gradle +++ b/grails-datastore-gorm-hibernate5/build.gradle @@ -50,7 +50,10 @@ dependencies { testImplementation "net.sf.ehcache:ehcache-core:2.6.11" testImplementation "org.hibernate:hibernate-ehcache:$hibernate5Version" - + + // groovy proxy fixes bytebuddy to be a bit smarter when it comes to groovy metaClass + testImplementation "org.yakworks:hibernate-groovy-proxy:$hibernateGroovyProxy" + testImplementation "org.apache.tomcat:tomcat-jdbc:$tomcatVersion" testRuntimeOnly "org.springframework:spring-aop:$springVersion" testRuntimeOnly "org.apache.tomcat.embed:tomcat-embed-logging-log4j:$tomcatLog4jVersion" diff --git a/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java b/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java index 25e6b1c5..785358c9 100644 --- a/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java +++ b/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/cfg/GrailsHibernateUtil.java @@ -376,7 +376,7 @@ public static void ensureCorrectGroovyMetaClass(Object target, Class persiste * @return the unproxied instance */ public static Object unwrapProxy(HibernateProxy proxy) { - return proxyHandler.unwrapProxy(proxy); + return proxyHandler.unwrap(proxy); } /** @@ -401,8 +401,12 @@ public static boolean isInitialized(Object obj, String associationName) { return proxyHandler.isInitialized(obj, associationName); } + /** + * Unproxies a HibernateProxy. If the proxy is uninitialized, it automatically triggers an initialization. + * In case the supplied object is null or not a proxy, the object will be returned as-is. + */ public static Object unwrapIfProxy(Object instance) { - return proxyHandler.unwrapIfProxy(instance); + return proxyHandler.unwrap(instance); } /** diff --git a/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java b/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java index a9d44ddb..3d0241fa 100644 --- a/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java +++ b/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java @@ -15,46 +15,152 @@ */ package org.grails.orm.hibernate.proxy; - -import org.hibernate.collection.internal.AbstractPersistentCollection; +import java.io.Serializable; +import org.grails.datastore.gorm.GormEnhancer; +import org.grails.datastore.mapping.core.Session; +import org.grails.datastore.mapping.engine.AssociationQueryExecutor; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.datastore.mapping.proxy.ProxyFactory; +import org.grails.datastore.mapping.proxy.ProxyHandler; +import org.grails.datastore.mapping.reflect.ClassPropertyFetcher; +import org.hibernate.Hibernate; import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.HibernateProxyHelper; /** - * Implementation of the ProxyHandler interface for Hibernate. + * Implementation of the ProxyHandler interface for Hibernate using org.hibernate.Hibernate + * and HibernateProxyHelper where possible. * * @author Graeme Rocher * @since 1.2.2 */ -public class HibernateProxyHandler extends SimpleHibernateProxyHandler { +public class HibernateProxyHandler implements ProxyHandler, ProxyFactory { + /** + * Check if the proxy or persistent collection is initialized. + * @inheritDoc + */ + @Override public boolean isInitialized(Object o) { - if (o instanceof PersistentCollection) { - return ((PersistentCollection)o).wasInitialized(); + return Hibernate.isInitialized(o); + } + + /** + * Check if an association proxy or persistent collection is initialized. + * @inheritDoc + */ + @Override + public boolean isInitialized(Object obj, String associationName) { + try { + Object proxy = ClassPropertyFetcher.getInstancePropertyValue(obj, associationName); + return isInitialized(proxy); } + catch (RuntimeException e) { + return false; + } + } - return super.isInitialized(o); + /** + * Unproxies a HibernateProxy. If the proxy is uninitialized, it automatically triggers an initialization. + * In case the supplied object is null or not a proxy, the object will be returned as-is. + * @inheritDoc + * @see Hibernate#unproxy + */ + @Override + public Object unwrap(Object object) { + if (object instanceof PersistentCollection) { + initialize(object); + return object; + } + return Hibernate.unproxy(object); } - public Object unwrapIfProxy(Object instance) { - if (instance instanceof PersistentCollection) { - initialize(instance); - return instance; + /** + * @inheritDoc + * @see org.hibernate.proxy.AbstractLazyInitializer#getIdentifier + */ + @Override + public Serializable getIdentifier(Object o) { + if (o instanceof HibernateProxy) { + return ((HibernateProxy)o).getHibernateLazyInitializer().getIdentifier(); + } + else { + //TODO seems we can get the id here if its has normal getId + // PersistentEntity persistentEntity = GormEnhancer.findStaticApi(o.getClass()).getGormPersistentEntity(); + // return persistentEntity.getMappingContext().getEntityReflector(persistentEntity).getIdentifier(o); + return null; } + } - return super.unwrapIfProxy(instance); + /** + * @inheritDoc + * @see HibernateProxyHelper#getClassWithoutInitializingProxy + */ + @Override + public Class getProxiedClass(Object o) { + return HibernateProxyHelper.getClassWithoutInitializingProxy(o); } + /** + * calls unwrap which calls unproxy + * @see #unwrap(Object) + * @deprecated use unwrap + */ + @Deprecated + public Object unwrapIfProxy(Object instance) { + return unwrap(instance); + } + + /** + * @inheritDoc + */ + @Override public boolean isProxy(Object o) { - return super.isProxy(o) || (o instanceof PersistentCollection); + return (o instanceof HibernateProxy) || (o instanceof PersistentCollection); } + /** + * Force initialization of a proxy or persistent collection. + * @inheritDoc + */ + @Override public void initialize(Object o) { - if (o instanceof PersistentCollection) { - final PersistentCollection col = (PersistentCollection)o; - if (!col.wasInitialized()) { - col.forceInitialization(); + Hibernate.initialize(o); + } + + @Override + public T createProxy(Session session, Class type, Serializable key) { + throw new UnsupportedOperationException("createProxy not supported in HibernateProxyHandler"); + } + + @Override + public T createProxy(Session session, AssociationQueryExecutor executor, K associationKey) { + throw new UnsupportedOperationException("createProxy not supported in HibernateProxyHandler"); + } + + /** + * @deprecated use unwrap + */ + @Deprecated + public Object unwrapProxy(Object proxy) { + return unwrap(proxy); + } + + /** + * returns the proxy for an association. returns null if its not a proxy. + * Note: Only used in a test. Deprecate? + */ + public HibernateProxy getAssociationProxy(Object obj, String associationName) { + try { + Object proxy = ClassPropertyFetcher.getInstancePropertyValue(obj, associationName); + if (proxy instanceof HibernateProxy) { + return (HibernateProxy) proxy; } + return null; + } + catch (RuntimeException e) { + return null; } - super.initialize(o); } } diff --git a/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java b/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java index bd2e8065..2d0fb627 100644 --- a/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java +++ b/grails-datastore-gorm-hibernate5/src/main/groovy/org/grails/orm/hibernate/proxy/SimpleHibernateProxyHandler.java @@ -34,9 +34,11 @@ /** * Implementation of the ProxyHandler interface for Hibernate. + * Deprecated as Hibernate 5.6+ no longer supports Javassist * * @author Graeme Rocher * @since 1.2.2 + * @deprecated */ public class SimpleHibernateProxyHandler extends JavassistProxyFactory implements ProxyHandler, ProxyFactory { diff --git a/grails-datastore-gorm-hibernate5/src/test/groovy/grails/gorm/tests/proxy/ByteBuddyProxySpec.groovy b/grails-datastore-gorm-hibernate5/src/test/groovy/grails/gorm/tests/proxy/ByteBuddyProxySpec.groovy new file mode 100644 index 00000000..feb4d61d --- /dev/null +++ b/grails-datastore-gorm-hibernate5/src/test/groovy/grails/gorm/tests/proxy/ByteBuddyProxySpec.groovy @@ -0,0 +1,130 @@ +package grails.gorm.tests.proxy + +import org.grails.datastore.mapping.reflect.ClassUtils +import org.grails.orm.hibernate.proxy.HibernateProxyHandler + +import grails.gorm.tests.Club +import grails.gorm.tests.GormDatastoreSpec +import grails.gorm.tests.Team +import spock.lang.PendingFeature +import spock.lang.PendingFeatureIf + +/** + * Contains misc proxy tests using Hibenrate defaults, which is ByteBuddy. + * These should all be passing for Gorm to be operating correctly with Groovy. + */ +class ByteBuddyProxySpec extends GormDatastoreSpec { + static HibernateProxyHandler proxyHandler = new HibernateProxyHandler() + + //to show test that fail that should succeed set this to true. or uncomment the + // testImplementation "org.yakworks:hibernate-groovy-proxy:$hibernateGroovyProxy" to see pass + boolean runPending = ClassUtils.isPresent("yakworks.hibernate.proxy.ByteBuddyGroovyInterceptor") + + @Override + List getDomainClasses() { [Team, Club] } + + Team createATeam(){ + Club c = new Club(name: "DOOM Club").save(failOnError:true) + Team team = new Team(name: "The A-Team", club: c).save(failOnError:true, flush:true) + return team + } + + void "getId and id property checks dont initialize proxy if in a CompileStatic method"() { + when: + Team team = createATeam() + session.clear() + team = Team.load(team.id) + + then:"The asserts on getId and id should not initialize proxy when statically compiled" + StaticTestUtil.team_id_asserts(team) + !proxyHandler.isInitialized(team) + + StaticTestUtil.club_id_asserts(team) + !proxyHandler.isInitialized(team.club) + } + + @PendingFeatureIf({ !instance.runPending }) + void "getId and id dont initialize proxy"() { + when:"load proxy" + Team team = createATeam() + session.clear() + team = Team.load(team.id) + + then:"The asserts on getId and id should not initialize proxy" + proxyHandler.isProxy(team) + team.getId() + !proxyHandler.isInitialized(team) + + team.id + !proxyHandler.isInitialized(team) + + and: "the getAt check for id should not initialize" + team['id'] + !proxyHandler.isInitialized(team) + } + + @PendingFeatureIf({ !instance.runPending }) + void "truthy check on instance should not initialize proxy"() { + when:"load proxy" + Team team = createATeam() + session.clear() + team = Team.load(team.id) + + then:"The asserts on the intance should not init proxy" + team + !proxyHandler.isInitialized(team) + + and: "truthy check on association should not initialize" + team.club + !proxyHandler.isInitialized(team.club) + } + + @PendingFeatureIf({ !instance.runPending }) + void "id checks on association should not initialize its proxy"() { + when:"load instance" + Team team = createATeam() + session.clear() + team = Team.load(team.id) + + then:"The asserts on the intance should not init proxy" + !proxyHandler.isInitialized(team.club) + + team.club.getId() + !proxyHandler.isInitialized(team.club) + + team.club.id + !proxyHandler.isInitialized(team.club) + + team.clubId + !proxyHandler.isInitialized(team.club) + + and: "the getAt check for id should not initialize" + team.club['id'] + !proxyHandler.isInitialized(team.club) + } + + void "isDirty should not intialize the association proxy"() { + when:"load instance" + Team team = createATeam() + session.clear() + team = Team.load(team.id) + + then:"The asserts on the intance should not init proxy" + !proxyHandler.isInitialized(team) + + //isDirty will init the proxy. should make changes for this. + !team.isDirty() + proxyHandler.isInitialized(team) + //it should not have initialized the association + !proxyHandler.isInitialized(team.club) + + when: "its made dirty" + team.name = "B-Team" + + then: + team.isDirty() + //still should not have initialized it. + !proxyHandler.isInitialized(team.club) + } + +} diff --git a/grails-datastore-gorm-hibernate5/src/test/groovy/grails/gorm/tests/proxy/StaticTestUtil.groovy b/grails-datastore-gorm-hibernate5/src/test/groovy/grails/gorm/tests/proxy/StaticTestUtil.groovy new file mode 100644 index 00000000..2ce5575b --- /dev/null +++ b/grails-datastore-gorm-hibernate5/src/test/groovy/grails/gorm/tests/proxy/StaticTestUtil.groovy @@ -0,0 +1,54 @@ +package grails.gorm.tests.proxy + +import groovy.transform.CompileStatic + +import org.grails.datastore.gorm.GormEntity +import org.grails.orm.hibernate.proxy.HibernateProxyHandler +import org.hibernate.Hibernate + +import grails.gorm.tests.Club +import grails.gorm.tests.Team + +@CompileStatic +class StaticTestUtil { + public static HibernateProxyHandler proxyHandler = new HibernateProxyHandler() + + // should return true and not initialize the proxy + // getId works inside a compile static + static boolean team_id_asserts(Team team){ + assert team.getId() + assert !Hibernate.isInitialized(team) + assert proxyHandler.isProxy(team) + + assert team.id + assert !Hibernate.isInitialized(team) + assert proxyHandler.isProxy(team) + //a truthy check on the object will try to init it because it hits the getMetaClass + // assert team + // assert !Hibernate.isInitialized(team) + + return true + } + + static boolean club_id_asserts(Team team){ + assert team.club.getId() + assert notInitialized(team.club) + + assert team.club.id + assert notInitialized(team.club) + + assert team.clubId + assert notInitialized(team.club) + + return true + } + + static boolean notInitialized(Object o){ + //sanity check the 3 + assert !Hibernate.isInitialized(o) + assert !proxyHandler.isInitialized(o) + assert proxyHandler.isProxy(o) + return true + } +} + diff --git a/settings.gradle b/settings.gradle index 5d018a46..ec7d15a5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,6 +62,8 @@ project(":examples-spring-boot-hibernate5").projectDir = new File(settingsDir, " include "examples-grails-data-service" project(":examples-grails-data-service").projectDir = new File(settingsDir, "examples/grails-data-service") +include "examples-grails-hibernate-groovy-proxy" +project(":examples-grails-hibernate-groovy-proxy").projectDir = new File(settingsDir, "examples/grails-hibernate-groovy-proxy") findProject(':boot-plugin').name = 'gorm-hibernate5-spring-boot'