This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch test/expand-integration-test-coverage in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 3c062589ddc9b1eb7729dff28c5e534c7fb26d3c Author: James Fredley <[email protected]> AuthorDate: Sun Jan 25 22:07:57 2026 -0500 Add GORM advanced query and relationship tests to app1 - Add GormAdvancedSpec with 53 tests for GORM queries - Tests criteria queries, HQL, dynamic finders - Tests projections, aggregations, and pagination - Tests domain relationships (one-to-many, many-to-many) - Includes cascade operations and orphan removal testing --- .../domain/functionaltests/gorm/Author.groovy | 62 ++ .../domain/functionaltests/gorm/GormBook.groovy | 70 ++ .../functionaltests/gorm/GormAdvancedSpec.groovy | 716 +++++++++++++++++++++ 3 files changed, 848 insertions(+) diff --git a/grails-test-examples/app1/grails-app/domain/functionaltests/gorm/Author.groovy b/grails-test-examples/app1/grails-app/domain/functionaltests/gorm/Author.groovy new file mode 100644 index 0000000000..0370b5be13 --- /dev/null +++ b/grails-test-examples/app1/grails-app/domain/functionaltests/gorm/Author.groovy @@ -0,0 +1,62 @@ +/* + * 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 functionaltests.gorm + +/** + * Domain class for advanced GORM query tests. + */ +class Author { + + String name + String country + Integer birthYear + boolean active = true + + Date dateCreated + Date lastUpdated + + static hasMany = [books: GormBook] + + static constraints = { + name blank: false, size: 1..100 + country nullable: true + birthYear nullable: true, min: 1800, max: 2100 + } + + static mapping = { + books cascade: 'all-delete-orphan' + } + + static namedQueries = { + activeAuthors { + eq 'active', true + } + fromCountry { String countryName -> + eq 'country', countryName + } + bornAfter { Integer year -> + gt 'birthYear', year + } + prolificAuthors { + // Authors with more than 2 books + sizeGt 'books', 2 + } + } +} diff --git a/grails-test-examples/app1/grails-app/domain/functionaltests/gorm/GormBook.groovy b/grails-test-examples/app1/grails-app/domain/functionaltests/gorm/GormBook.groovy new file mode 100644 index 0000000000..48fc078725 --- /dev/null +++ b/grails-test-examples/app1/grails-app/domain/functionaltests/gorm/GormBook.groovy @@ -0,0 +1,70 @@ +/* + * 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 functionaltests.gorm + +/** + * Domain class for advanced GORM query tests - Book entity. + */ +class GormBook { + + String title + String genre + BigDecimal price + Integer pageCount + Integer publicationYear + boolean inPrint = true + Double rating + + Date dateCreated + Date lastUpdated + + static belongsTo = [author: Author] + + static constraints = { + title blank: false, size: 1..200 + genre inList: ['Fiction', 'Non-Fiction', 'Science', 'History', 'Biography', 'Fantasy', 'Mystery', 'Romance'] + price min: 0.0 + pageCount nullable: true, min: 1 + publicationYear nullable: true, min: 1450, max: 2100 + rating nullable: true, min: 0.0d, max: 5.0d + } + + static mapping = { + table 'gorm_books' + } + + static namedQueries = { + inGenre { String genreName -> + eq 'genre', genreName + } + publishedAfter { Integer year -> + gt 'publicationYear', year + } + pricedBetween { BigDecimal min, BigDecimal max -> + between 'price', min, max + } + highlyRated { + ge 'rating', 4.0d + } + availableInPrint { + eq 'inPrint', true + } + } +} diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/gorm/GormAdvancedSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/gorm/GormAdvancedSpec.groovy new file mode 100644 index 0000000000..2a70fd3930 --- /dev/null +++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/gorm/GormAdvancedSpec.groovy @@ -0,0 +1,716 @@ +/* + * 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 functionaltests.gorm + +import functionaltests.Application +import grails.testing.mixin.integration.Integration +import grails.gorm.transactions.Rollback +import spock.lang.Specification +import spock.lang.Shared + +/** + * Integration tests for advanced GORM features: + * - Criteria queries + * - Where queries + * - Projections and aggregations + * - HQL queries + * - Named queries + * - Detached criteria + */ +@Integration(applicationClass = Application) +@Rollback +class GormAdvancedSpec extends Specification { + + def setup() { + // Create test data + createTestData() + } + + private void createTestData() { + // Author 1: American author with 3 books + def author1 = new Author(name: 'John Smith', country: 'USA', birthYear: 1970, active: true) + author1.addToBooks(new GormBook(title: 'The Great Adventure', genre: 'Fiction', price: 19.99, pageCount: 350, publicationYear: 2020, rating: 4.5)) + author1.addToBooks(new GormBook(title: 'Mystery Manor', genre: 'Mystery', price: 14.99, pageCount: 280, publicationYear: 2021, rating: 4.2)) + author1.addToBooks(new GormBook(title: 'Science Today', genre: 'Science', price: 29.99, pageCount: 450, publicationYear: 2022, rating: 4.8)) + author1.save(failOnError: true) + + // Author 2: British author with 2 books + def author2 = new Author(name: 'Jane Doe', country: 'UK', birthYear: 1985, active: true) + author2.addToBooks(new GormBook(title: 'British History', genre: 'History', price: 24.99, pageCount: 500, publicationYear: 2019, rating: 4.0)) + author2.addToBooks(new GormBook(title: 'Royal Biography', genre: 'Biography', price: 21.99, pageCount: 320, publicationYear: 2020, rating: 3.8)) + author2.save(failOnError: true) + + // Author 3: French author with 1 book, inactive + def author3 = new Author(name: 'Pierre Martin', country: 'France', birthYear: 1960, active: false) + author3.addToBooks(new GormBook(title: 'French Cuisine', genre: 'Non-Fiction', price: 34.99, pageCount: 400, publicationYear: 2018, inPrint: false, rating: 4.6)) + author3.save(failOnError: true) + + // Author 4: American author with 4 books (prolific) + def author4 = new Author(name: 'Sarah Johnson', country: 'USA', birthYear: 1975, active: true) + author4.addToBooks(new GormBook(title: 'Fantasy World', genre: 'Fantasy', price: 16.99, pageCount: 600, publicationYear: 2021, rating: 4.9)) + author4.addToBooks(new GormBook(title: 'Dragon Tales', genre: 'Fantasy', price: 17.99, pageCount: 550, publicationYear: 2022, rating: 4.7)) + author4.addToBooks(new GormBook(title: 'Magic Realm', genre: 'Fantasy', price: 18.99, pageCount: 580, publicationYear: 2023, rating: 4.8)) + author4.addToBooks(new GormBook(title: 'Love Story', genre: 'Romance', price: 12.99, pageCount: 250, publicationYear: 2020, rating: 3.5)) + author4.save(failOnError: true, flush: true) + } + + // ========== Criteria Query Tests ========== + + def "criteria query - basic equals"() { + when: "querying with equals criterion" + def results = Author.createCriteria().list { + eq('country', 'USA') + } + + then: "matching authors are returned" + results.size() == 2 + results*.name.containsAll(['John Smith', 'Sarah Johnson']) + } + + def "criteria query - like operator"() { + when: "querying with like criterion" + def results = GormBook.createCriteria().list { + like('title', '%History%') + } + + then: "matching books are returned" + results.size() >= 1 // At least our British History book + results.every { it.title.contains('History') } + results.find { it.title == 'British History' } != null + } + + def "criteria query - ilike (case insensitive)"() { + when: "querying with case-insensitive like" + def results = GormBook.createCriteria().list { + ilike('title', '%FANTASY%') + } + + then: "matching books regardless of case" + results.size() >= 1 // At least our Fantasy World book + results.every { it.title.toLowerCase().contains('fantasy') } + results.find { it.title == 'Fantasy World' } != null + } + + def "criteria query - between"() { + when: "querying with between criterion" + def results = GormBook.createCriteria().list { + between('price', 15.00, 25.00) + } + + then: "books in price range are returned" + results.size() >= 4 + results.every { it.price >= 15.00 && it.price <= 25.00 } + } + + def "criteria query - greater than and less than"() { + when: "querying with gt and lt" + def results = GormBook.createCriteria().list { + gt('pageCount', 400) + lt('price', 30.00) + } + + then: "matching books are returned" + results.every { it.pageCount > 400 && it.price < 30.00 } + } + + def "criteria query - in list"() { + when: "querying with inList" + def results = GormBook.createCriteria().list { + inList('genre', ['Fantasy', 'Mystery']) + } + + then: "books in specified genres are returned" + results.size() >= 4 + results.every { it.genre in ['Fantasy', 'Mystery'] } + } + + def "criteria query - is null and is not null"() { + when: "querying for non-null ratings" + def results = GormBook.createCriteria().list { + isNotNull('rating') + } + + then: "all books have ratings" + results.every { it.rating != null } + } + + def "criteria query - and/or logic"() { + when: "querying with or logic" + def results = GormBook.createCriteria().list { + or { + eq('genre', 'Fantasy') + ge('price', 30.00) + } + } + + then: "books matching either condition are returned" + results.every { it.genre == 'Fantasy' || it.price >= 30.00 } + } + + def "criteria query - nested and/or"() { + when: "querying with nested logic" + def results = GormBook.createCriteria().list { + and { + eq('inPrint', true) + or { + eq('genre', 'Fiction') + eq('genre', 'Science') + } + } + } + + then: "books matching complex condition" + results.every { it.inPrint && (it.genre == 'Fiction' || it.genre == 'Science') } + } + + def "criteria query - association query"() { + when: "querying through association" + def results = GormBook.createCriteria().list { + author { + eq('country', 'USA') + } + } + + then: "books by USA authors are returned" + results.size() >= 7 + } + + def "criteria query - order by"() { + when: "querying with ordering" + def results = GormBook.createCriteria().list { + order('price', 'desc') + } + + then: "results are ordered by price descending" + for (int i = 0; i < results.size() - 1; i++) { + results[i].price >= results[i + 1].price + } + } + + def "criteria query - multiple order by"() { + when: "querying with multiple orderings" + def results = GormBook.createCriteria().list { + order('genre', 'asc') + order('price', 'desc') + } + + then: "results are ordered" + results.size() > 0 + } + + def "criteria query - pagination"() { + when: "querying with pagination" + def results = GormBook.createCriteria().list(max: 3, offset: 0) { + order('title', 'asc') + } + + then: "paginated results are returned" + results.size() <= 3 + } + + def "criteria query - count"() { + when: "counting with criteria" + def count = Author.createCriteria().count { + eq('active', true) + } + + then: "count is returned" + count >= 3 // At least our 3 active authors + } + + def "criteria query - get single result"() { + when: "getting single result with unique constraint" + def result = Author.createCriteria().list { + eq('name', 'John Smith') + maxResults(1) + } + + then: "author is returned" + result.size() >= 1 + result[0].name == 'John Smith' + } + + // ========== Where Query Tests ========== + + def "where query - simple condition"() { + when: "using where query" + def results = Author.where { + country == 'UK' + }.list() + + then: "matching authors are returned" + results.size() >= 1 // At least our UK test author + results.every { it.country == 'UK' } + results.find { it.name == 'Jane Doe' } != null + } + + def "where query - multiple conditions"() { + when: "using where with multiple conditions" + def results = GormBook.where { + genre == 'Fantasy' && price < 18.00 + }.list() + + then: "matching books are returned" + results.every { it.genre == 'Fantasy' && it.price < 18.00 } + } + + def "where query - or conditions"() { + when: "using where with or" + def results = GormBook.where { + genre == 'History' || genre == 'Biography' + }.list() + + then: "books in either genre" + results.size() >= 2 // At least our 2 test books + results*.genre.every { it in ['History', 'Biography'] } + } + + def "where query - comparison operators"() { + when: "using comparison operators" + def results = GormBook.where { + rating >= 4.5 && pageCount > 300 + }.list() + + then: "highly rated long books" + results.every { it.rating >= 4.5 && it.pageCount > 300 } + } + + def "where query - like pattern"() { + when: "using like in where query" + def results = GormBook.where { + title =~ '%Tales%' + }.list() + + then: "matching books are returned" + results.size() >= 1 // At least our Dragon Tales book + results.every { it.title.contains('Tales') } + results.find { it.title == 'Dragon Tales' } != null + } + + def "where query - in list"() { + when: "using in operator" + def genres = ['Fantasy', 'Romance'] + def results = GormBook.where { + genre in genres + }.list() + + then: "books in specified genres" + results.every { it.genre in genres } + } + + def "where query - association traversal"() { + when: "traversing association in where" + def results = GormBook.where { + author.country == 'USA' + }.list() + + then: "books by USA authors" + results.size() >= 7 + } + + def "where query - detached for reuse"() { + given: "a detached where query" + def fantasyBooks = GormBook.where { + genre == 'Fantasy' + } + + when: "reusing the query with additional conditions" + def highlyRatedFantasy = fantasyBooks.where { + rating >= 4.7 + }.list() + + then: "combined conditions apply" + highlyRatedFantasy.every { it.genre == 'Fantasy' && it.rating >= 4.7 } + } + + def "where query - count"() { + when: "counting with where query" + def count = GormBook.where { + inPrint == true + }.count() + + then: "count is correct" + count >= 9 // 9 books in print + } + + // ========== Projection Tests ========== + + def "projection - single property"() { + when: "projecting single property" + def titles = GormBook.createCriteria().list { + projections { + property('title') + } + } + + then: "list of titles is returned" + titles.every { it instanceof String } + } + + def "projection - multiple properties"() { + when: "projecting multiple properties" + def results = GormBook.createCriteria().list { + projections { + property('title') + property('price') + } + } + + then: "list of arrays is returned" + results.every { it.size() == 2 } + } + + def "projection - distinct"() { + when: "getting distinct values" + def genres = GormBook.createCriteria().list { + projections { + distinct('genre') + } + } + + then: "distinct genres are returned" + genres.unique().size() == genres.size() + } + + def "projection - count"() { + when: "counting with projection" + def count = GormBook.createCriteria().get { + projections { + count('id') + } + } + + then: "count is returned" + count >= 10 + } + + def "projection - sum"() { + when: "summing with projection" + def totalPages = GormBook.createCriteria().get { + projections { + sum('pageCount') + } + } + + then: "sum is returned" + totalPages > 0 + } + + def "projection - average"() { + when: "averaging with projection" + def avgPrice = GormBook.createCriteria().get { + projections { + avg('price') + } + } + + then: "average is returned" + avgPrice > 0 + } + + def "projection - min and max"() { + when: "getting min and max" + def minMax = GormBook.createCriteria().get { + projections { + min('price') + max('price') + } + } + + then: "min and max are returned" + minMax[0] < minMax[1] + } + + def "projection - group by"() { + when: "grouping by genre with count" + def results = GormBook.createCriteria().list { + projections { + groupProperty('genre') + count('id') + } + order('genre', 'asc') + } + + then: "grouped counts are returned" + results.every { it.size() == 2 } + def fantasyCount = results.find { it[0] == 'Fantasy' } + fantasyCount != null + fantasyCount[1] >= 3 + } + + def "projection - group by with aggregations"() { + when: "grouping with multiple aggregations" + def results = GormBook.createCriteria().list { + projections { + groupProperty('genre') + count('id') + avg('price') + sum('pageCount') + } + } + + then: "aggregated results per genre" + results.every { it.size() == 4 } + } + + // ========== HQL Query Tests ========== + + def "HQL - simple select"() { + when: "executing simple HQL" + def results = GormBook.executeQuery("from GormBook where genre = :genre", [genre: 'Fantasy']) + + then: "matching books are returned" + results.every { it.genre == 'Fantasy' } + } + + def "HQL - select with projection"() { + when: "HQL with projection" + def titles = GormBook.executeQuery("select b.title from GormBook b where b.price > :price", [price: 20.00]) + + then: "titles are returned" + titles.every { it instanceof String } + } + + def "HQL - join query"() { + when: "HQL with join" + def results = GormBook.executeQuery( + "select b from GormBook b join b.author a where a.country = :country", + [country: 'UK'] + ) + + then: "books by UK authors" + results.size() >= 2 // At least our 2 UK books + results.every { it.author.country == 'UK' } + } + + def "HQL - aggregate functions"() { + when: "HQL with aggregates" + def result = GormBook.executeQuery( + "select count(b), avg(b.price), sum(b.pageCount) from GormBook b" + ) + + then: "aggregates are returned" + result[0][0] >= 10 // count + result[0][1] > 0 // avg price + result[0][2] > 0 // sum pages + } + + def "HQL - group by"() { + when: "HQL with group by" + def results = GormBook.executeQuery( + "select b.genre, count(b), avg(b.rating) from GormBook b group by b.genre order by b.genre" + ) + + then: "grouped results" + results.size() > 0 + } + + def "HQL - named parameters"() { + when: "HQL with named parameters" + def results = GormBook.executeQuery( + "from GormBook b where b.price between :minPrice and :maxPrice and b.rating >= :minRating", + [minPrice: 15.00, maxPrice: 25.00, minRating: 4.0d] // Use 'd' suffix for Double type + ) + + then: "matching books" + results.every { it.price >= 15.00 && it.price <= 25.00 && it.rating >= 4.0 } + } + + def "HQL - pagination"() { + when: "HQL with pagination" + def results = GormBook.executeQuery( + "from GormBook b order by b.title", + [max: 5, offset: 0] + ) + + then: "paginated results" + results.size() <= 5 + } + + def "HQL - update query"() { + given: "a book to update" + def book = GormBook.findByTitle('The Great Adventure') + def originalPrice = book.price + + when: "executing update HQL" + def updated = GormBook.executeUpdate( + "update GormBook b set b.price = b.price * 1.1 where b.genre = :genre", + [genre: 'Fiction'] + ) + + then: "books are updated" + updated >= 1 + + cleanup: + GormBook.executeUpdate("update GormBook b set b.price = :price where b.title = :title", + [price: originalPrice, title: 'The Great Adventure']) + } + + def "HQL - subquery"() { + when: "HQL with subquery" + def results = GormBook.executeQuery( + "from GormBook b where b.price > (select avg(b2.price) from GormBook b2)" + ) + + then: "books above average price" + results.size() > 0 + } + + // ========== Named Query Tests ========== + + def "named query - simple"() { + when: "using named query" + def results = Author.activeAuthors.list() + + then: "active authors are returned" + results.size() >= 3 // At least our 3 test authors + results.every { it.active } + } + + def "named query - with parameter"() { + when: "using named query with parameter" + def results = Author.fromCountry('USA').list() + + then: "USA authors are returned" + results.size() >= 2 // At least our 2 USA test authors + results.every { it.country == 'USA' } + } + + def "named query - chained"() { + when: "chaining named queries" + def results = Author.activeAuthors.fromCountry('USA').list() + + then: "active USA authors" + results.size() >= 2 // At least our 2 active USA test authors + results.every { it.active && it.country == 'USA' } + } + + def "named query - with criteria"() { + when: "combining named query with criteria" + def results = Author.activeAuthors.list { + gt('birthYear', 1970) + } + + then: "active authors born after 1970" + results.every { it.active && it.birthYear > 1970 } + } + + def "named query - book queries"() { + when: "using book named queries" + def fantasyBooks = GormBook.inGenre('Fantasy').list() + def highRated = GormBook.highlyRated.list() + def affordable = GormBook.pricedBetween(10.00, 20.00).list() + + then: "queries return correct results" + fantasyBooks.every { it.genre == 'Fantasy' } + highRated.every { it.rating >= 4.0 } + affordable.every { it.price >= 10.00 && it.price <= 20.00 } + } + + def "named query - count"() { + when: "counting with named query" + def count = GormBook.availableInPrint.count() + + then: "count is returned" + count >= 9 + } + + // ========== Detached Criteria Tests ========== + + def "detached criteria - basic"() { + given: "a detached criteria" + def criteria = new grails.gorm.DetachedCriteria(GormBook).build { + eq('genre', 'Fantasy') + } + + when: "executing detached criteria" + def results = criteria.list() + + then: "matching books are returned" + results.every { it.genre == 'Fantasy' } + } + + def "detached criteria - with projections"() { + given: "a detached criteria with projection" + def criteria = new grails.gorm.DetachedCriteria(GormBook).build { + projections { + property('title') + } + eq('inPrint', true) + } + + when: "executing" + def titles = criteria.list() + + then: "titles are returned" + titles.every { it instanceof String } + } + + def "detached criteria - reusable"() { + given: "a base criteria" + def baseCriteria = new grails.gorm.DetachedCriteria(GormBook).build { + gt('rating', 4.0d) + } + + when: "reusing with additional conditions" + def expensiveHighRated = baseCriteria.build { + gt('price', 20.00) + }.list() + + def cheapHighRated = baseCriteria.build { + lt('price', 20.00) + }.list() + + then: "both queries work independently" + expensiveHighRated.every { it.rating > 4.0 && it.price > 20.00 } + cheapHighRated.every { it.rating > 4.0 && it.price < 20.00 } + } + + // ========== Batch Processing Tests ========== + + def "batch processing with withSession"() { + when: "processing in batches" + def processedCount = 0 + GormBook.withSession { session -> + GormBook.list().each { book -> + // Simulate batch processing + processedCount++ + if (processedCount % 5 == 0) { + session.flush() + session.clear() + } + } + } + + then: "all books processed" + processedCount >= 10 + } + + def "read-only query optimization"() { + when: "executing read-only query" + def results = GormBook.createCriteria().list { + readOnly(true) + eq('inPrint', true) + } + + then: "results are returned (read-only mode)" + results.size() >= 9 + } +}
