diff --git a/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy new file mode 100644 index 00000000000..fcd69ff93e1 --- /dev/null +++ b/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy @@ -0,0 +1,187 @@ +/* + * 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.connections + +import org.hibernate.dialect.H2Dialect +import spock.lang.AutoCleanup +import spock.lang.Issue +import spock.lang.Shared +import spock.lang.Specification + +import grails.gorm.annotation.Entity +import grails.gorm.services.Service +import grails.gorm.services.Where +import grails.gorm.transactions.Transactional +import org.grails.datastore.gorm.GormEnhancer +import org.grails.datastore.gorm.GormEntity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore + +@Issue("https://github.com/apache/grails-core/issues/15416") +class WhereQueryMultiDataSourceSpec extends Specification { + + @Shared Map config = [ + 'dataSource.url':"jdbc:h2:mem:defaultDB;LOCK_TIMEOUT=10000", + 'dataSource.dbCreate': 'create-drop', + 'dataSource.dialect': H2Dialect.name, + 'dataSource.formatSql': 'true', + 'hibernate.flush.mode': 'COMMIT', + 'hibernate.cache.queries': 'true', + 'hibernate.hbm2ddl.auto': 'create-drop', + 'dataSources.secondary':[url:"jdbc:h2:mem:secondaryDB;LOCK_TIMEOUT=10000"], + ] + + @Shared @AutoCleanup HibernateDatastore datastore = new HibernateDatastore( + DatastoreUtils.createPropertyResolver(config), Item + ) + + @Shared ItemQueryService itemQueryService + + void setupSpec() { + itemQueryService = datastore + .getDatastoreForConnection('secondary') + .getService(ItemQueryService) + } + + void setup() { + def api = GormEnhancer.findStaticApi(Item, 'secondary') + api.withNewTransaction { + api.executeUpdate('delete from Item') + } + GormEnhancer.findStaticApi(Item).withNewTransaction { + GormEnhancer.findStaticApi(Item).executeUpdate('delete from Item') + } + } + + void "@Where query routes to secondary datasource"() { + given: + saveToSecondary('Cheap', 10.0) + saveToSecondary('Expensive', 500.0) + + when: + def results = itemQueryService.findByMinAmount(100.0) + + then: + results.size() == 1 + results[0].name == 'Expensive' + } + + void "@Where query does not return data from default datasource"() { + given: 'an item saved to secondary' + saveToSecondary('OnSecondary', 50.0) + + and: 'a different item saved directly to default' + saveToDefault('OnDefault', 999.0) + + when: 'querying via @Where for amount >= 500 on secondary-bound service' + def results = itemQueryService.findByMinAmount(500.0) + + then: 'only secondary data is searched - default item is NOT found' + results.size() == 0 + } + + void "count routes to secondary datasource"() { + given: + saveToSecondary('A', 1.0) + saveToSecondary('B', 2.0) + + and: 'an item on default that should not be counted' + saveToDefault('C', 3.0) + + expect: + itemQueryService.count() == 2 + } + + void "list routes to secondary datasource"() { + given: + saveToSecondary('X', 10.0) + saveToSecondary('Y', 20.0) + + and: 'an item on default that should not be listed' + saveToDefault('Z', 30.0) + + when: + def all = itemQueryService.list() + + then: + all.size() == 2 + } + + void "findByName routes to secondary datasource"() { + given: + saveToSecondary('Unique', 77.0) + + when: + def found = itemQueryService.findByName('Unique') + + then: + found != null + found.name == 'Unique' + found.amount == 77.0 + } + + private void saveToSecondary(String name, Double amount) { + def api = GormEnhancer.findStaticApi(Item, 'secondary') + api.withNewTransaction { + def instanceApi = GormEnhancer.findInstanceApi(Item, 'secondary') + def item = new Item(name: name, amount: amount) + instanceApi.save(item, [flush: true]) + } + } + + private void saveToDefault(String name, Double amount) { + def api = GormEnhancer.findStaticApi(Item) + api.withNewTransaction { + def instanceApi = GormEnhancer.findInstanceApi(Item) + def item = new Item(name: name, amount: amount) + instanceApi.save(item, [flush: true]) + } + } +} + +@Entity +class Item implements GormEntity { + Long id + Long version + String name + Double amount + + static mapping = { + datasource 'ALL' + } + + static constraints = { + name blank: false + amount nullable: false + } +} + +@Service(Item) +@Transactional(connection = 'secondary') +interface ItemQueryService { + + Item findByName(String name) + + Number count() + + List list() + + @Where({ amount >= minAmount }) + List findByMinAmount(Double minAmount) +} diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractDetachedCriteriaServiceImplementor.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractDetachedCriteriaServiceImplementor.groovy index 845aa25fe67..25cf1849ec6 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractDetachedCriteriaServiceImplementor.groovy +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractDetachedCriteriaServiceImplementor.groovy @@ -75,7 +75,7 @@ abstract class AbstractDetachedCriteriaServiceImplementor extends AbstractReadOp body.addStatement( declS(queryVar, ctorX(getDetachedCriteriaType(domainClassNode), args(classX(domainClassNode.plainNodeReference)))) ) - Expression connectionId = findConnectionId(newMethodNode) + Expression connectionId = findConnectionId(abstractMethodNode) if (connectionId != null) { body.addStatement( diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractWhereImplementer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractWhereImplementer.groovy index 7944c32f4ae..01125f4de36 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractWhereImplementer.groovy +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/services/implementers/AbstractWhereImplementer.groovy @@ -96,16 +96,16 @@ abstract class AbstractWhereImplementer extends AbstractReadOperationImplementer body.addStatement( declS(queryVar, ctorX(getDetachedCriteriaType(domainClassNode), args(classX(domainClassNode.plainNodeReference)))) ) - Expression connectionId = findConnectionId(newMethodNode) + body.addStatement( + assignS(queryVar, callX(queryVar, 'build', closureExpression)) + ) + Expression connectionId = findConnectionId(abstractMethodNode) if (connectionId != null) { body.addStatement( assignS(queryVar, callX(queryVar, 'withConnection', connectionId)) ) } - body.addStatement( - assignS(queryVar, callX(queryVar, 'build', closureExpression)) - ) Expression queryExpression = callX(queryVar, getQueryMethodToExecute(domainClassNode, newMethodNode), argsExpression != null ? argsExpression : AstUtils.ZERO_ARGUMENTS) body.addStatement( buildReturnStatement(domainClassNode, abstractMethodNode, newMethodNode, queryExpression) diff --git a/grails-datamapping-core/src/test/groovy/grails/gorm/services/WhereConnectionRoutingSpec.groovy b/grails-datamapping-core/src/test/groovy/grails/gorm/services/WhereConnectionRoutingSpec.groovy new file mode 100644 index 00000000000..46b71f21907 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/grails/gorm/services/WhereConnectionRoutingSpec.groovy @@ -0,0 +1,211 @@ +/* + * 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 grails.gorm.services + +import grails.gorm.transactions.Transactional +import org.grails.datastore.gorm.services.Implemented +import org.grails.datastore.gorm.services.implementers.FindAllWhereImplementer +import org.grails.datastore.gorm.services.implementers.FindOneWhereImplementer +import org.grails.datastore.gorm.services.implementers.CountWhereImplementer +import org.grails.datastore.gorm.services.implementers.CountImplementer +import org.grails.datastore.gorm.services.implementers.FindAllByImplementer +import org.grails.datastore.gorm.services.implementers.FindAllImplementer +import org.grails.datastore.gorm.services.implementers.FindOneByImplementer +import org.grails.datastore.gorm.services.implementers.FindOneImplementer +import spock.lang.Specification + +class WhereConnectionRoutingSpec extends Specification { + + void "test @Where method on interface with @Transactional(connection)"() { + when:"The service transform is applied to an interface with @Transactional(connection)" + Class service = new GroovyClassLoader().parseClass(''' +import grails.gorm.services.Service +import grails.gorm.services.Where +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Transactional + +@Service(Foo) +@Transactional(connection = 'secondary') +interface FooService { + + @Where({ title ==~ pattern }) + Foo findByTitle(String pattern) +} +@Entity +class Foo { + String title + static mapping = { + datasource 'secondary' + } +} +''') + + then: + service.isInterface() + + when:"the impl is obtained" + Class impl = service.classLoader.loadClass("\$FooServiceImplementation") + + then:"The impl is valid" + impl.getAnnotation(Transactional) != null + impl.getAnnotation(Transactional).connection() == 'secondary' + impl.getMethod("findByTitle", String).getAnnotation(Implemented).by() == FindOneWhereImplementer + } + + void "test @Where method on abstract class with @Transactional(connection)"() { + when:"The service transform is applied to an abstract class" + Class service = new GroovyClassLoader().parseClass(''' +import grails.gorm.services.Service +import grails.gorm.services.Where +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Transactional + +@Service(Foo) +@Transactional(connection = 'secondary') +abstract class FooService { + + @Where({ title ==~ pattern }) + abstract List searchByTitle(String pattern) +} +@Entity +class Foo { + String title + static mapping = { + datasource 'secondary' + } +} +''') + + then: + !service.isInterface() + + when:"the impl is obtained" + Class impl = service.classLoader.loadClass("\$FooServiceImplementation") + + then:"The impl is valid" + impl.getAnnotation(Transactional) != null + impl.getAnnotation(Transactional).connection() == 'secondary' + impl.getMethod("searchByTitle", String).getAnnotation(Implemented).by() == FindAllWhereImplementer + } + + void "test count method uses connection-aware implementer"() { + when:"The service transform is applied to an interface with count()" + Class service = new GroovyClassLoader().parseClass(''' +import grails.gorm.services.Service +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Transactional + +@Service(Foo) +@Transactional(connection = 'secondary') +interface FooService { + Number count() +} +@Entity +class Foo { + String title + static mapping = { + datasource 'secondary' + } +} +''') + + then: + service.isInterface() + + when:"the impl is obtained" + Class impl = service.classLoader.loadClass("\$FooServiceImplementation") + + then:"The impl is valid" + impl.getAnnotation(Transactional) != null + impl.getAnnotation(Transactional).connection() == 'secondary' + impl.getMethod("count").getAnnotation(Implemented).by() == CountImplementer + } + + void "test list, findAll, and findBy methods use connection-aware implementer"() { + when:"The service transform is applied to an interface with list, findAll, and findBy methods" + Class service = new GroovyClassLoader().parseClass(''' +import grails.gorm.services.Service +import grails.gorm.annotation.Entity +import grails.gorm.transactions.Transactional + +@Service(Foo) +@Transactional(connection = 'secondary') +interface FooService { + List list() + List findAllByTitle(String title) + Foo findByName(String name) + Foo find(Serializable id) +} +@Entity +class Foo { + String title + String name + static mapping = { + datasource 'secondary' + } +} +''') + + then: + service.isInterface() + + when:"the impl is obtained" + Class impl = service.classLoader.loadClass("\$FooServiceImplementation") + + then:"The impl is valid" + impl.getAnnotation(Transactional) != null + impl.getAnnotation(Transactional).connection() == 'secondary' + impl.getMethod("list").getAnnotation(Implemented).by() == FindAllImplementer + impl.getMethod("findAllByTitle", String).getAnnotation(Implemented).by() == FindAllByImplementer + impl.getMethod("findByName", String).getAnnotation(Implemented).by() == FindOneByImplementer + impl.getMethod("find", Serializable).getAnnotation(Implemented).by() == FindOneImplementer + } + + void "test @Where method without @Transactional(connection)"() { + when:"The service transform is applied to an interface with @Where" + Class service = new GroovyClassLoader().parseClass(''' +import grails.gorm.services.Service +import grails.gorm.services.Where +import grails.gorm.annotation.Entity + +@Service(Foo) +interface FooService { + + @Where({ title ==~ pattern }) + Number countByTitle(String pattern) +} +@Entity +class Foo { + String title + static mapping = { + datasource 'secondary' + } +} +''') + + then: + service.isInterface() + + when:"the impl is obtained" + Class impl = service.classLoader.loadClass("\$FooServiceImplementation") + + then:"The impl is valid" + impl.getMethod("countByTitle", String).getAnnotation(Implemented).by() == CountWhereImplementer + } +}