diff --git a/grails-data-hibernate5/docs/src/docs/asciidoc/querying/whereQueries.adoc b/grails-data-hibernate5/docs/src/docs/asciidoc/querying/whereQueries.adoc index e42ab3e6dfa..01a728a6081 100644 --- a/grails-data-hibernate5/docs/src/docs/asciidoc/querying/whereQueries.adoc +++ b/grails-data-hibernate5/docs/src/docs/asciidoc/querying/whereQueries.adoc @@ -30,6 +30,7 @@ The link:../api/org/grails/datastore/gorm/GormEntity.html#where(groovy.lang.Clos def query = Person.where { firstName == "Bart" } + Person bart = query.find() ---- @@ -43,9 +44,10 @@ class Person { static DetachedCriteria simpsons = where { lastName == "Simpson" } - ... } + ... + Person.simpsons.each { Person p -> println p.firstname } @@ -58,9 +60,11 @@ Query execution is lazy and only happens upon usage of the < 9) } + def results = query.list(sort:"firstName") ---- @@ -129,9 +134,11 @@ Since the return value of the `where` method is a < query = Person.where { lastName == "Simpson" } + DetachedCriteria bartQuery = query.where { firstName == "Bart" } + Person p = bartQuery.find() ---- @@ -142,6 +149,8 @@ Note that you cannot pass a closure defined as a variable into the `where` metho def callable = { lastName == "Simpson" } + +// Fails: where() requires an inline closure for the AST transform def query = Person.where(callable) ---- @@ -154,6 +163,7 @@ import grails.gorm.DetachedCriteria def callable = { lastName == "Simpson" } as DetachedCriteria + def query = Person.where(callable) ---- @@ -262,42 +272,76 @@ Operator,Criteria Method,Description *<=*,sizeLe,The collection size is less than or equal to |=== -==== Query Aliases and Sorting -If you define a query for an association an alias is automatically generated for the query. For example the following query: +==== Sorting + +Sorting with `where` queries is performed by passing `sort` and (optionally) `order` arguments to the `list` method of a <>. + +===== Single-field Sorting + +To sort by a single property: [source,groovy] ---- -def query = Pet.where { - owner.firstName == "Fred" +def query = Person.where { + firstName == "Jack" } + +def results = query.list(sort: "firstName") +---- + +By default, sorting is ascending. You can specify the direction explicitly using the `order` argument: + +[source,groovy] +---- +def results = query.list(sort: "firstName", order: "desc") +---- + +===== Multi-field Sorting + +To sort by multiple fields, pass a `Map` to the `sort` argument: + +[source,groovy] +---- +def results = query.list(sort: [ + firstName: "asc", + lastName: "desc" +]) ---- -Will generate an alias for the `owner` association such as `owner_alias_0`. These generated aliases are fine for most cases, but are not useful if you want to later sort or use a projection on the results. For example the following query will fail: +This sorts results first by `firstName` ascending and then by `lastName` descending. + +===== Sorting with Associations + +When sorting by properties of associated entities, you must define an explicit alias within the `where` query. +Without an explicit alias, sorting on association properties may fail due to dynamically generated aliases. + +For example the following query will fail: [source,groovy] ---- // fails because a dynamic alias is used Pet.where { owner.firstName == "Fred" -}.list(sort:"owner.lastName") +}.list(sort: "owner.lastName") ---- -If you plan to sort the results then an explicit alias should be used and these can be defined by simply declaring a variable in the `where` query: +In this case GORM will generate an alias for the `owner` association such as `owner_alias_0`. These generated aliases are internal and are not suitable for later reference in sorting or projections. + +To avoid this problem, define an explicit alias: [source,groovy] ---- +// 1. Define alias def query = Pet.where { - def o1 = owner <1> - o1.firstName == "Fred" <2> -}.list(sort:'o1.lastName') <3> ----- + def o = owner + o.firstName == "Fred" +} -<1> Define an alias called `o1` -<2> Use the alias in the query itself -<3> Use the alias to sort the results +// 2. Execute query using alias for sorting +def results = query.list(sort: "o.lastName") +---- -By assigning the name of an association to a local variable it will automatically become an alias usable within the query itself and also for the purposes of sorting or projecting the results. ==== Subqueries @@ -307,7 +351,7 @@ It is possible to execute subqueries within where queries. For example to find a [source,groovy] ---- final query = Person.where { - age > avg(age) + age > avg(age) } ---- @@ -330,7 +374,7 @@ You can apply additional criteria to any subquery by using the `of` method and p [source,groovy] ---- def query = Person.where { - age > avg(age).of { lastName == "Simpson" } && firstName == "Homer" + age > avg(age).of { lastName == "Simpson" } && firstName == "Homer" } ---- @@ -363,7 +407,7 @@ Criteria and where queries can be seamlessly mixed: ---- def results = Person.withCriteria { notIn "firstName", Person.where { age < 18 }.firstName - } +} ---- Subqueries can be used with projections: @@ -380,10 +424,11 @@ Correlated queries that span two domain classes can be used: ---- def employees = Employee.where { region.continent in ['APAC', "EMEA"] - }.id() - def results = Sale.where { +}.id() + +def results = Sale.where { employee in employees && total > 100000 - }.employee.list() +}.employee.list() ---- And support for aliases (cross query references) using simple variable declarations has been added to where queries: @@ -455,6 +500,7 @@ Since each `where` method call returns a <> i DetachedCriteria query = Person.where { lastName == 'Simpson' } + int total = query.updateAll(lastName:"Bloggs") ---- @@ -467,5 +513,6 @@ To batch delete records you can use the `deleteAll` method: DetachedCriteria query = Person.where { lastName == 'Simpson' } + int total = query.deleteAll() ---- diff --git a/grails-doc/src/en/guide/GORM/quickStartGuide/basicCRUD.adoc b/grails-doc/src/en/guide/GORM/quickStartGuide/basicCRUD.adoc index bd548f3fdd4..bdcc7abd694 100644 --- a/grails-doc/src/en/guide/GORM/quickStartGuide/basicCRUD.adoc +++ b/grails-doc/src/en/guide/GORM/quickStartGuide/basicCRUD.adoc @@ -27,13 +27,48 @@ To create a domain class use Map constructor to set its properties and call link [source,groovy] ---- -def p = new Person(name: "Fred", age: 40, lastVisit: new Date()) +def p = new Person( + name: "Fred", + age: 40, + lastVisit: new Date(), +) + p.save() ---- The link:{domainClassesRef}save.html[save] method will persist your class to the database using the underlying Hibernate ORM layer. +=== List + +To retrieve multiple instances, use the `list` method: + +[source,groovy] +---- +def people = Person.list() +---- + +This returns all `Person` records from the database. + +You can also pass pagination and sorting parameters: + +[source,groovy] +---- +def fetchParams = [sort: 'name', order: 'asc', max: 10, offset: 0] +def people = Person.list(fetchParams) +---- + +The `list` method supports: + +* `max` – maximum number of results +* `offset` – starting position +* `sort` – property to sort by +* `order` – sort direction (`asc` or `desc`) + +In addition to these you can specify advanced parameters such as `cache`, `fetch`, `lock`, `readOnly`, `fetchSize`, `timeout`, `flushMode`, and `ignoreCase`. + +See link:{domainClassesRef}list.html[list] for the complete list of supported parameters. + === Read @@ -42,7 +77,7 @@ Grails transparently adds an implicit `id` property to your domain class which y [source,groovy] ---- def p = Person.get(1) -assert 1 == p.id +assert p.id == 1 ---- This uses the link:{domainClassesRef}get.html[get] method that expects a database identifier to read the `Person` object back from the database. @@ -79,6 +114,19 @@ p.name = "Bob" p.save() ---- +You can also update multiple properties at once using the `properties` assignment: + +[source,groovy] +---- +def p = Person.get(1) +p.properties = [name: "Bob", age: 45] +p.save() +---- + +This will bind the given map to the domain instance, updating all matching properties in a single step. + +Only properties defined in the domain class will be assigned, and any missing properties in the map will remain unchanged. + === Delete @@ -91,6 +139,141 @@ def p = Person.get(1) p.delete() ---- +If a delete operation fails (for example due to database constraints), an exception is thrown. + +You can handle this using a `try/catch` block: + +[source,groovy] +---- +def p = Person.get(1) +try { + p.delete(flush: true) +} catch (Exception e) { + println "Delete failed: ${e.message}" +} +---- + +Unlike the link:{domainClassesRef}save.html[save] method, the `delete` method does not support a `failOnError` parameter. Instead, errors are propagated as exceptions. + +Using `flush: true` ensures the delete is executed immediately, so any errors are raised at that point. + +=== Querying + +To dynamically build queries based on optional parameters a common pattern is to use `DetachedCriteria` and progressively compose filters depending on the provided inputs. + +==== Properties + +Consider the following example using the `Person` domain class: + +[source,groovy] +---- +import grails.gorm.DetachedCriteria + +private DetachedCriteria buildQuery(Map filterParams) { + def query = Person.where {} + + if (filterParams.containsKey('id')) { + query = query.where { id == filterParams.id } + } + + if (filterParams.containsKey('name')) { + query = query.where { name == filterParams.name } + } + + if (filterParams.containsKey('age')) { + query = query.where { age == filterParams.age } + } + + return query +} +---- + +==== Associations + +You can filter by associated properties using dot notation or nested criteria. For example, if `Person` has a self-referencing relationship `parent`, you can filter by properties of the parent. + +[source,groovy] +---- +class Person { + String name + Integer age + Date lastVisit + Person parent +} +---- + +Using dot notation: + +[source,groovy] +---- +if (filterParams.containsKey('parent.name')) { + query = query.where { parent.name == filterParams.'parent.name' } +} +---- + +Using a nested criteria block, which is useful when filtering multiple properties of the association: + +[source,groovy] +---- +if (filterParams.containsKey('parent.name')) { + query = query.where { + parent { + name == filterParams.'parent.name' + } + } +} +---- + +This approach allows you to: + +* Build queries incrementally +* Apply only the filters that are actually provided +* Keep query logic reusable and centralized + +You can then use this query in different ways. + +==== Find a Single Result + +[source,groovy] +---- +def filterParams = [id: 1] +def person = buildQuery(filterParams).get() +---- + +NOTE: Note: `DetachedCriteria.get()` returns a single result from the criteria query, while `Person.get(id)` is a static lookup by primary key on the domain class. + +==== List Results + +[source,groovy] +---- +def filterParams = [age: 40] +def fetchParams = [sort: 'name', order: 'asc'] +def people = buildQuery(filterParams).list(fetchParams) +---- + +Filters can be combined simply by passing multiple items: + +[source,groovy] +---- +def filterParams = [name: "Fred", age: 40] +def results = buildQuery(filterParams).list() +---- + +==== Count Results + +[source,groovy] +---- +def filterParams = [age: 40] +def total = buildQuery(filterParams).count() +---- + +Each condition is applied only if the corresponding parameter exists, making this pattern highly flexible for search forms and APIs. + +==== Notes +* Each call to `where {}` returns a new `DetachedCriteria`, allowing safe chaining. +* This pattern avoids large, hardcoded query methods. +* It works seamlessly with GORM and Hibernate in Grails. +This technique is especially useful in service layers where filtering logic must remain dynamic and maintainable.