This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 16f87ae438b4b60acc1842dab48d134f7ae2edbc Author: Walter Duque de Estrada <[email protected]> AuthorDate: Tue Mar 3 17:50:25 2026 -0600 updated docs using Copilot --- .../src/docs/asciidoc/advancedGORMFeatures.adoc | 36 ++++++ .../advancedGORMFeatures/defaultSortOrder.adoc | 43 +++++++ .../docs/asciidoc/advancedGORMFeatures/ormdsl.adoc | 19 ++++ .../advancedGORMFeatures/ormdsl/caching.adoc | 95 ++++++++++++++++ .../ormdsl/compositePrimaryKeys.adoc | 49 ++++++++ .../ormdsl/customCascadeBehaviour.adoc | 50 +++++++++ .../ormdsl/customHibernateTypes.adoc | 67 +++++++++++ .../ormdsl/customNamingStrategy.adoc | 52 +++++++++ .../ormdsl/databaseIndices.adoc | 55 +++++++++ .../ormdsl/derivedProperties.adoc | 57 ++++++++++ .../advancedGORMFeatures/ormdsl/fetchingDSL.adoc | 74 +++++++++++++ .../advancedGORMFeatures/ormdsl/identity.adoc | 65 +++++++++++ .../ormdsl/inheritanceStrategies.adoc | 95 ++++++++++++++++ .../ormdsl/optimisticLockingAndVersioning.adoc | 65 +++++++++++ .../ormdsl/tableAndColumnNames.adoc | 123 +++++++++++++++++++++ .../docs/src/docs/asciidoc/domainClasses.adoc | 66 +++++++++++ .../asciidoc/domainClasses/gormAssociation.adoc | 28 +++++ .../gormAssociation/basicCollectionTypes.adoc | 88 +++++++++++++++ .../domainClasses/gormAssociation/manyToMany.adoc | 68 ++++++++++++ .../gormAssociation/manyToOneAndOneToOne.adoc | 78 +++++++++++++ .../domainClasses/gormAssociation/oneToMany.adoc | 71 ++++++++++++ .../asciidoc/domainClasses/gormComposition.adoc | 61 ++++++++++ .../asciidoc/domainClasses/inheritanceInGORM.adoc | 54 +++++++++ .../asciidoc/domainClasses/sets,ListsAndMaps.adoc | 69 ++++++++++++ .../docs/src/docs/asciidoc/introduction.adoc | 53 +++++++++ .../docs/src/docs/asciidoc/persistenceBasics.adoc | 4 + .../docs/asciidoc/persistenceBasics/cascades.adoc | 44 ++++++++ .../persistenceBasics/deletingObjects.adoc | 53 +++++++++ .../docs/asciidoc/persistenceBasics/fetching.adoc | 55 +++++++++ .../docs/asciidoc/persistenceBasics/locking.adoc | 40 +++++++ .../persistenceBasics/modificationChecking.adoc | 45 ++++++++ .../persistenceBasics/savingAndUpdating.adoc | 76 +++++++++++++ .../docs/asciidoc/programmaticTransactions.adoc | 73 ++++++++++++ .../docs/src/docs/asciidoc/querying.adoc | 39 +++++++ .../docs/src/docs/asciidoc/quickStartGuide.adoc | 7 ++ .../docs/asciidoc/quickStartGuide/basicCRUD.adoc | 89 +++++++++++++++ 36 files changed, 2106 insertions(+) diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc index e69de29bb2..c7462361d0 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures.adoc @@ -0,0 +1,36 @@ +[[advancedGORMFeatures]] +== Advanced GORM Features + +This section covers advanced GORM and Hibernate mapping capabilities available through the `static mapping {}` DSL and other configuration mechanisms. + +The ORM DSL `mapping` block is available on every domain class and allows you to customise every aspect of the Hibernate mapping: + +[source,groovy] +---- +class Book { + String title + Date dateCreated + + static mapping = { + table 'books' + title column: 'book_title', index: true + batchSize 20 + cache usage: 'read-write' + } +} +---- + +The following topics are covered in this section: + +* xref:ormdsl-tableAndColumnNames[Table and Column Names] +* xref:ormdsl-identity[Identity] +* xref:ormdsl-compositePrimaryKeys[Composite Primary Keys] +* xref:ormdsl-optimisticLockingAndVersioning[Optimistic Locking and Versioning] +* xref:ormdsl-inheritanceStrategies[Inheritance Strategies] +* xref:ormdsl-fetchingDSL[Fetching Strategies] +* xref:ormdsl-caching[Caching] +* xref:ormdsl-customCascadeBehaviour[Custom Cascade Behaviour] +* xref:ormdsl-customNamingStrategy[Custom Naming Strategy] +* xref:ormdsl-customHibernateTypes[Custom Hibernate Types] +* xref:ormdsl-derivedProperties[Derived Properties] +* xref:ormdsl-databaseIndices[Database Indices] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc index e69de29bb2..f225b32f17 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/defaultSortOrder.adoc @@ -0,0 +1,43 @@ +[[advancedGORMFeatures-defaultSortOrder]] +== Default Sort Order + +You can configure a default sort order for `list()` and `findAll*` queries at the domain class level using the `mapping` block: + +[source,groovy] +---- +class Book { + String title + Date dateCreated + + static mapping = { + sort 'title' // <1> + } +} +---- +<1> All `Book.list()` calls will return results sorted by `title` in ascending order by default. + +=== Sort Direction + +[source,groovy] +---- +static mapping = { + sort title: 'desc' // <1> +} +---- +<1> Sort by `title` descending. + +=== Default Sort on Associations + +You can also define a default sort order on a `hasMany` collection: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books sort: 'title', order: 'asc' + } +} +---- + +NOTE: The default sort order in `mapping` applies to queries that do not specify their own `order`. Any query that specifies `sort` or `order` explicitly will override the default. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc index e69de29bb2..5248242564 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl.adoc @@ -0,0 +1,19 @@ +[[advancedGORMFeatures-ormdsl]] +== ORM DSL + +The ORM DSL is a `static mapping` closure on every domain class that gives you fine-grained control over how GORM maps your domain model to the database schema. + +See the subsections below for details on each feature: + +* xref:ormdsl-tableAndColumnNames[Table and Column Names] +* xref:ormdsl-identity[Identity] +* xref:ormdsl-compositePrimaryKeys[Composite Primary Keys] +* xref:ormdsl-optimisticLockingAndVersioning[Optimistic Locking and Versioning] +* xref:ormdsl-inheritanceStrategies[Inheritance Strategies] +* xref:ormdsl-fetchingDSL[Fetching Strategies] +* xref:ormdsl-caching[Caching] +* xref:ormdsl-customCascadeBehaviour[Custom Cascade Behaviour] +* xref:ormdsl-customNamingStrategy[Custom Naming Strategy] +* xref:ormdsl-customHibernateTypes[Custom Hibernate Types] +* xref:ormdsl-derivedProperties[Derived Properties] +* xref:ormdsl-databaseIndices[Database Indices] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc index e69de29bb2..49b95dc216 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/caching.adoc @@ -0,0 +1,95 @@ +[[ormdsl-caching]] +== Caching + +GORM supports Hibernate's second-level cache and query cache. Caching is configured per domain class and optionally per association. + +=== Enabling Second-Level Cache + +Second-level cache is enabled globally via configuration: + +[source,yaml] +---- +hibernate: + use_second_level_cache: true + cache: + region: + factory_class: 'org.hibernate.cache.jcache.internal.JCacheRegionFactory' +---- + +Then enable caching for individual domain classes using the `cache` directive in the `mapping` block: + +[source,groovy] +---- +class Book { + String title + static mapping = { + cache true // <1> + } +} +---- +<1> Enables the second-level cache for `Book` with the default `read-write` usage. + +=== Cache Usage + +You can control the cache usage strategy: + +[source,groovy] +---- +static mapping = { + cache usage: 'read-only' // <1> +} +---- + +[format="csv", options="header"] +|=== +usage,description +`read-write`,Cached data can be read and written — default +`read-only`,Cached data is never modified (best performance for immutable data) +`nonstrict-read-write`,No strict locking; possible stale reads between updates +`transactional`,Full transaction support (requires a JTA transaction manager) +|=== + +=== Cache Include + +The `include` option controls what data to cache: + +[source,groovy] +---- +static mapping = { + cache usage: 'read-write', include: 'non-lazy' // <1> +} +---- +<1> Only non-lazy properties are cached. Use `all` (default) to include lazy properties too. + +=== Caching Associations + +You can cache collection associations independently: + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] + static mapping = { + books cache: true + } +} +---- + +=== Query Cache + +To cache the results of individual queries, pass `cache: true` in the query options and enable the query cache globally: + +[source,yaml] +---- +hibernate: + cache: + use_query_cache: true +---- + +[source,groovy] +---- +List<Book> books = Book.findAllByGenre('Fiction', [cache: true]) +---- + +TIP: Only use the query cache for queries whose results change infrequently. Cached queries are invalidated whenever any entity in the queried table is updated. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc index e69de29bb2..198379629d 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/compositePrimaryKeys.adoc @@ -0,0 +1,49 @@ +[[ormdsl-compositePrimaryKeys]] +== Composite Primary Keys + +GORM allows you to map domain classes to tables that use a composite primary key (a key composed of two or more columns). This is commonly needed when mapping to legacy database schemas. + +=== Defining a Composite Identity + +Use `id composite: [...]` in the `mapping` block, listing the property names that together form the primary key: + +[source,groovy] +---- +class OrderItem { + Long orderId + Long productId + Integer quantity + + static mapping = { + id composite: ['orderId', 'productId'] // <1> + } +} +---- +<1> The composite key is made up of `orderId` and `productId`. + +NOTE: Domain classes with composite keys do **not** have the auto-generated `id` and `version` properties. You are responsible for setting the key properties before calling `save()`. + +=== Using Composite Keys + +[source,groovy] +---- +def item = new OrderItem(orderId: 1L, productId: 42L, quantity: 3) +item.save() + +// Load by composite key — pass a map +def found = OrderItem.get(orderId: 1L, productId: 42L) +---- + +=== Associations with Composite Keys + +When another domain class references a domain class with a composite key, GORM creates multiple foreign-key columns automatically: + +[source,groovy] +---- +class OrderLine { + Integer lineNumber + OrderItem item // foreign key will use both orderId and productId columns +} +---- + +TIP: Composite primary keys add complexity to all queries and associations. Prefer surrogate (auto-generated) single-column keys wherever possible and use composite unique constraints instead of composite keys for business-key uniqueness requirements. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc index e69de29bb2..d5ca3cc55a 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customCascadeBehaviour.adoc @@ -0,0 +1,50 @@ +[[ormdsl-customCascadeBehaviour]] +== Custom Cascade Behaviour + +Hibernate cascades control which persistence operations (save, update, delete, etc.) are automatically propagated from a parent entity to its associated children. + +=== Default Cascade Behaviour + +By default GORM applies `save-update` cascading on associations — when you save or update an entity, changes to its associated objects are also persisted. Deletions are **not** cascaded by default. + +=== Configuring Cascade + +Use the `cascade` option in the `mapping` block to override the default: + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] + static mapping = { + books cascade: 'all' // <1> + } +} +---- +<1> `all` cascades all operations including delete to `books`. + +[format="csv", options="header"] +|=== +value,description +`all`,Propagates all operations (save/update/delete/merge/refresh) +`save-update`,Propagates save and update — default +`delete`,Propagates delete only +`all-delete-orphan`,Like `all` but also deletes child rows not present in the collection +`none`,No cascade +|=== + +=== Cascade Delete Example + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] + static mapping = { + books cascade: 'all-delete-orphan' // <1> + } +} +---- +<1> When you remove a `Book` from `author.books` and call `author.save()`, the removed `Book` row is also deleted from the database. + +TIP: Use `all-delete-orphan` when the child entity has no meaning outside the parent (composition). Use `save-update` (default) when the child may belong to multiple parents (aggregation). diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc index e69de29bb2..79f709ba32 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customHibernateTypes.adoc @@ -0,0 +1,67 @@ +[[ormdsl-customHibernateTypes]] +== Custom Hibernate Types + +GORM allows you to map properties to custom Hibernate `UserType` implementations. This is useful for persisting non-standard Java/Groovy types — for example, storing a `List` as a comma-separated string, or encrypting values at the persistence layer. + +=== Per-Property Type + +Use the `type` option in the `mapping` block to assign a custom Hibernate type to a specific property: + +[source,groovy] +---- +class Setting { + String name + Serializable value + + static mapping = { + value type: 'serializable' // <1> + } +} +---- +<1> The type name can be a Hibernate built-in type alias, a fully qualified class name, or a `Class` object. + +With a custom `UserType` class: + +[source,groovy] +---- +class Product { + String name + List<String> tags + + static mapping = { + tags type: CsvStringListType // <1> + } +} +---- +<1> `CsvStringListType` implements `org.hibernate.usertype.UserType` and handles conversion between a comma-separated column value and a `List<String>`. + +=== Type Parameters + +If your custom type implements `org.hibernate.usertype.ParameterizedType`, pass parameters using `typeParams`: + +[source,groovy] +---- +class Measurement { + BigDecimal value + + static mapping = { + value type: FixedScaleDecimalType, typeParams: [scale: '4'] + } +} +---- + +=== Global User Type Mapping + +Register a user type for a given Java class globally so it applies to all properties of that type without explicit per-property configuration: + +[source,groovy] +---- +class MyDomainClass { + static mapping = { + userTypes Money: MoneyUserType // <1> + } +} +---- +<1> Any property of type `Money` in this class will use `MoneyUserType`. + +TIP: Implementing `org.hibernate.usertype.UserType` requires handling `sqlTypes()`, `nullSafeGet()`, `nullSafeSet()`, `deepCopy()`, and `equals()`. Prefer Hibernate's `CompositeUserType` for multi-column mappings. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc index e69de29bb2..0e0da62454 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc @@ -0,0 +1,52 @@ +[[ormdsl-customNamingStrategy]] +== Custom Naming Strategy + +By default GORM uses Hibernate's snake_case physical naming strategy (`PhysicalNamingStrategySnakeCaseImpl`), which converts camelCase class and property names to `snake_case` table and column names (e.g., `BookAuthor` → `book_author`, `firstName` → `first_name`). + +=== Configuring a Custom Strategy + +You can replace this with any Hibernate `PhysicalNamingStrategy` implementation via application configuration: + +[source,yaml] +---- +hibernate: + physicalNamingStrategy: com.example.MyCustomNamingStrategy +---- + +Or set it per datasource: + +[source,yaml] +---- +dataSources: + reporting: + hibernate: + physicalNamingStrategy: com.example.LegacyNamingStrategy +---- + +=== Implementing a Custom Strategy + +Implement Hibernate's `org.hibernate.boot.model.naming.PhysicalNamingStrategy` interface: + +[source,groovy] +---- +import org.hibernate.boot.model.naming.Identifier +import org.hibernate.boot.model.naming.PhysicalNamingStrategy +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment + +class UpperCaseNamingStrategy implements PhysicalNamingStrategy { + + @Override + Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) { + return Identifier.toIdentifier(name.text.toUpperCase()) + } + + @Override + Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) { + return Identifier.toIdentifier(name.text.toUpperCase()) + } + + // ... other required method overrides ... +} +---- + +TIP: Individual column or table names set explicitly in the `mapping` block always take precedence over what the naming strategy would produce. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc index e69de29bb2..9772224c6e 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/databaseIndices.adoc @@ -0,0 +1,55 @@ +[[ormdsl-databaseIndices]] +== Database Indices + +GORM lets you define database indices on domain class columns directly in the `mapping` block, so they are created automatically when `hbm2ddl` generates the schema. + +=== Single-Column Index + +Set `index: true` on a column to create an auto-named index, or provide a string name to name it explicitly: + +[source,groovy] +---- +class Book { + String title + String isbn + static mapping = { + title index: true // <1> + isbn index: 'isbn_idx' // <2> + } +} +---- +<1> Creates an unnamed (auto-named) index on `title`. +<2> Creates a named index `isbn_idx` on `isbn`. + +=== Composite Index + +To create a composite index across multiple columns, use the same index name on each column: + +[source,groovy] +---- +class OrderItem { + Long orderId + Long productId + static mapping = { + orderId index: 'order_product_idx' // <1> + productId index: 'order_product_idx' // <1> + } +} +---- +<1> Both columns share the same index name, so Hibernate creates a single composite index. + +=== Unique Index + +You can combine `index` with `unique` to create a unique index: + +[source,groovy] +---- +class Book { + String isbn + static mapping = { + isbn unique: true, index: 'isbn_unique_idx' + } +} +---- + +NOTE: Indices are only created automatically when `hibernate.hbm2ddl.auto` is set to `create`, `create-drop`, or `update`. For production schemas, prefer explicit DDL migration scripts. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc index e69de29bb2..1b826050ba 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/derivedProperties.adoc @@ -0,0 +1,57 @@ +[[ormdsl-derivedProperties]] +== Derived Properties + +A derived property is a read-only property whose value is computed by a SQL formula rather than stored in a dedicated column. + +=== Defining a Derived Property + +Use the `formula` option in the `mapping` block: + +[source,groovy] +---- +class Order { + BigDecimal subtotal + BigDecimal taxRate + + BigDecimal tax // <1> + BigDecimal total // <1> + + static mapping = { + tax formula: 'subtotal * tax_rate' // <2> + total formula: 'subtotal + (subtotal * tax_rate)' + } +} +---- +<1> `tax` and `total` have no corresponding columns in the table. +<2> The formula is a raw SQL expression evaluated by the database. + +=== Reading Derived Values + +Derived properties are populated when an entity is loaded: + +[source,groovy] +---- +def order = Order.get(1) +println order.tax // value computed by the database formula +---- + +WARNING: Derived properties are read-only. Setting them in Groovy code does not affect the database value — the formula always takes precedence when reloading. + +=== Column-Level Formulas (Read/Write Expressions) + +For finer control over individual column values, use `read` and `write` expressions on a regular property: + +[source,groovy] +---- +class CreditCard { + String cardNumber + static mapping = { + cardNumber { + read "decrypt(card_number)" // <1> + write "encrypt(?)" // <2> + } + } +} +---- +<1> SQL expression used when reading the column value. +<2> SQL expression wrapping the bound parameter when writing. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc index e69de29bb2..aafa4b9662 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/fetchingDSL.adoc @@ -0,0 +1,74 @@ +[[ormdsl-fetchingDSL]] +== Fetching Strategies + +GORM supports both lazy (default) and eager fetching for associations. You can control this per-property via the ORM DSL `mapping` block. + +=== Lazy Fetching (Default) + +By default, associations are loaded lazily — Hibernate issues a secondary query only when you first access the association: + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] + // books are loaded lazily by default +} +---- + +=== Eager Fetching + +To eagerly load an association in the same query as the owning entity, use `fetch: 'join'`: + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] + static mapping = { + books fetch: 'join' // <1> + } +} +---- +<1> Hibernate uses a SQL `JOIN` to load `books` alongside the `Author`. + +You can also use `fetch: 'select'` to trigger a secondary `SELECT` eagerly (as opposed to lazy, which defers the select until access): + +[source,groovy] +---- +static mapping = { + books fetch: 'select' // loads eagerly via a secondary SELECT +} +---- + +=== Batch Fetching + +Batch fetching is a performance optimisation that allows Hibernate to initialise multiple lazy proxies or collections in a single `SELECT`. Configure it with `batchSize`: + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] + static mapping = { + books batchSize: 10 // <1> + } +} +---- +<1> When accessing `books` on an uninitialized proxy, Hibernate will fetch up to 10 collections in one query. + +Batch size can also be set at the class level, which affects all lazy-loaded instances of that class: + +[source,groovy] +---- +class Book { + String title + static mapping = { + batchSize 10 + } +} +---- + +=== Lazy vs. Eager — Recommendations + +TIP: Eager fetching avoids N+1 query problems but can return large result sets. Prefer lazy loading with explicit eager overrides (via named queries or `where` clauses with `.join()`) for fine-grained control. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc index e69de29bb2..3c53237d67 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/identity.adoc @@ -0,0 +1,65 @@ +[[ormdsl-identity]] +== Identity + +GORM automatically adds an `id` property and a `version` property to every domain class. The `mapping` block lets you customise both. + +=== Generator Strategy + +The default generator is `native`, which delegates to the database for id generation (auto-increment, sequences, etc.). You can change this globally or per-class: + +[source,groovy] +---- +class Book { + String title + static mapping = { + id generator: 'sequence', params: [sequence_name: 'book_seq'] // <1> + } +} +---- +<1> Uses a named database sequence for the `id` column. + +Common generator values: + +[format="csv", options="header"] +|=== +value,description +`native`,Delegates to the database (auto-increment / sequence) — default +`assigned`,Application assigns the id before saving +`uuid`,Generates a UUID string id +`sequence`,Uses a named database sequence (configure via `params`) +`increment`,GORM-managed incrementing long — not suitable for clusters +`identity`,Database `IDENTITY` / auto-increment column +|=== + +=== Column Name + +[source,groovy] +---- +static mapping = { + id column: 'book_id' +} +---- + +=== Composite Identifiers + +See xref:ormdsl-compositePrimaryKeys[Composite Primary Keys]. + +=== Disabling Auto-generated Version + +The `version` column enables optimistic locking. To disable it: + +[source,groovy] +---- +static mapping = { + version false +} +---- + +You can also map it to a different column: + +[source,groovy] +---- +static mapping = { + version column: 'revision' +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc index e69de29bb2..9cb7d3ec84 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/inheritanceStrategies.adoc @@ -0,0 +1,95 @@ +[[ormdsl-inheritanceStrategies]] +== Inheritance Strategies + +GORM supports three Hibernate inheritance mapping strategies. + +=== Table-per-Hierarchy (Default) + +All classes in the hierarchy are stored in a single table. A discriminator column distinguishes rows for each subclass. This is the default: + +[source,groovy] +---- +class Content { + String title + // tablePerHierarchy is true by default +} + +class BlogPost extends Content { + String body +} + +class Page extends Content { + String html +} +---- + +The single table will contain columns for all properties of all subclasses, with nullable columns for subclass-specific fields. + +==== Customising the Discriminator + +[source,groovy] +---- +class Content { + String title + static mapping = { + discriminator column: 'content_type', value: 'content' + } +} + +class BlogPost extends Content { + static mapping = { + discriminator 'blog' // <1> + } +} +---- +<1> The discriminator value stored for `BlogPost` rows. + +You can also configure the discriminator column type and whether it is insertable: + +[source,groovy] +---- +static mapping = { + discriminator { + column name: 'dtype', sqlType: 'varchar(30)' + value 'blog' + insert false + } +} +---- + +=== Table-per-Subclass + +Each subclass has its own table containing only the subclass-specific columns, joined to the parent table via a foreign key: + +[source,groovy] +---- +class Content { + String title + static mapping = { + tablePerHierarchy false // <1> + } +} + +class BlogPost extends Content { + String body + // implicitly uses joined-subclass mapping +} +---- +<1> Disables single-table strategy; Hibernate will use joined subclass tables. + +=== Table-per-Concrete-Class + +Each concrete class has its own standalone table with all columns (inherited + its own). There is no shared parent table: + +[source,groovy] +---- +class Content { + String title + static mapping = { + tablePerConcreteClass true // <1> + } +} +---- +<1> Each concrete subclass gets its own fully self-contained table. + +TIP: Table-per-hierarchy is the most performant strategy because it requires no joins. Table-per-subclass is useful when you need to query on a subclass without nulls in the parent table. Table-per-concrete-class is least commonly used and makes polymorphic queries expensive. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc index e69de29bb2..a6b238d06a 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/optimisticLockingAndVersioning.adoc @@ -0,0 +1,65 @@ +[[ormdsl-optimisticLockingAndVersioning]] +== Optimistic Locking and Versioning + +GORM enables optimistic locking by default via a `version` column added to every domain class table. + +=== How Optimistic Locking Works + +When you call `save()`, Hibernate checks that the `version` in the database matches the version loaded by the current session. If another transaction modified the row in between, the versions will differ and Hibernate throws `StaleObjectStateException`: + +[source,groovy] +---- +def book = Book.get(1) +// ... another thread or transaction updates the same book row ... +book.title = "New Title" +book.save() // throws StaleObjectStateException if version was incremented elsewhere +---- + +Handle this with a try/catch in your service or controller: + +[source,groovy] +---- +try { + book.save(failOnError: true) +} catch (org.hibernate.StaleObjectStateException e) { + // handle conflict: reload and retry, or inform the user +} +---- + +=== Disabling Optimistic Locking + +[source,groovy] +---- +class Book { + String title + static mapping = { + version false // <1> + } +} +---- +<1> No `version` column is created; concurrent modifications are not detected. + +WARNING: Disabling versioning removes all optimistic locking protection. Concurrent updates to the same row will silently overwrite each other. + +=== Customising the Version Column + +[source,groovy] +---- +static mapping = { + version column: 'revision' +} +---- + +=== Locking Pessimistically + +For cases where you need a database-level lock, use GORM's `lock()` method: + +[source,groovy] +---- +Book.withTransaction { + def book = Book.lock(1) // <1> + book.title = "Locked Update" + book.save() +} +---- +<1> Issues a `SELECT ... FOR UPDATE`, preventing other transactions from reading or modifying the row until the transaction commits. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc index e69de29bb2..cecff6cd41 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/tableAndColumnNames.adoc @@ -0,0 +1,123 @@ +[[ormdsl-tableAndColumnNames]] +== Table and Column Names + +By default GORM derives table and column names from your domain class and property names using an underscore-based naming strategy. You can override these defaults with the ORM DSL `mapping` block. + +=== Changing the Table Name + +[source,groovy] +---- +class Book { + String title + static mapping = { + table 'books' // <1> + } +} +---- +<1> Maps the `Book` domain class to a table named `books`. + +You can also specify a catalog and/or schema: + +[source,groovy] +---- +class Book { + String title + static mapping = { + table name: 'books', catalog: 'inventory', schema: 'dbo' + } +} +---- + +=== Changing Column Names + +Use the property name followed by `column` to override the column name for any property: + +[source,groovy] +---- +class Book { + String title + static mapping = { + title column: 'book_title' + } +} +---- + +For multi-column user types, call `column` multiple times: + +[source,groovy] +---- +class Payment { + Money amount + static mapping = { + amount { + column name: 'amount_value' + column name: 'amount_currency' + } + } +} +---- + +=== Column Properties + +The `column` block supports the following attributes: + +[format="csv", options="header"] +|=== +attribute,description,default +`name`,The column name,derived from property name +`sqlType`,The SQL type override,derived from Hibernate type +`unique`,Whether the column has a unique constraint,`false` +`index`,Index name (or `true` for an auto-named index),none +`defaultValue`,The DDL default value for the column,none +`comment`,A DDL comment for the column,none +`read`,A SQL expression to use when reading the value,none +`write`,A SQL expression to use when writing the value,none +|=== + +=== Join Table Configuration for Collections + +When a domain class has a `hasMany` relationship without a `belongsTo` on the other side (unidirectional), or for `hasMany` of basic/enum types, GORM uses a join table. You can customise the join table name and its columns: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books joinTable: 'author_books' // <1> + } +} +---- +<1> Override the join table name only. + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books joinTable: [ + name: 'author_books', // <1> + key: 'author_fk', // <2> + column: 'book_fk' // <3> + ] + } +} +---- +<1> The join table name. +<2> The foreign-key column that points back to `Author`. +<3> The foreign-key column that points to `Book` (or holds the element value for basic types). + +You can also use the closure form: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books joinTable { + name 'author_books' + key 'author_fk' + column 'book_fk' + } + } +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses.adoc index e69de29bb2..6d2b13ab03 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses.adoc @@ -0,0 +1,66 @@ +[[domainClasses]] +== Domain Classes + +Domain classes are the heart of a GORM application. They represent the data model and are mapped to database tables automatically by convention. + +=== Anatomy of a Domain Class + +[source,groovy] +---- +class Book { + String title + String author + Integer pages + Date dateCreated // <1> + Date lastUpdated // <1> + + static constraints = { // <2> + title blank: false, maxSize: 255 + author blank: false + pages min: 1, nullable: true + } + + static mapping = { // <3> + table 'books' + title index: true + } +} +---- +<1> `dateCreated` and `lastUpdated` are automatically timestamped by GORM. +<2> The `constraints` block defines validation rules and column constraints. +<3> The `mapping` block customises the Hibernate ORM mapping. + +=== Automatic Properties + +Every domain class automatically gets: + +[cols="1,2"] +|=== +|Property | Description + +|`id` +|Auto-generated primary key (`Long` by default) + +|`version` +|Optimistic locking version column (`Long`) +|=== + +And auto-timestamped properties if declared: + +[cols="1,2"] +|=== +|Property | Description + +|`dateCreated` +|Set to the current timestamp on first save + +|`lastUpdated` +|Updated to the current timestamp on every save +|=== + +Refer to the following sections for details on domain class features: + +* xref:domainClasses-setsListsAndMaps[Sets, Lists and Maps] +* xref:domainClasses-gormAssociation[GORM Associations] +* xref:domainClasses-gormComposition[GORM Composition] +* xref:domainClasses-inheritanceInGORM[Inheritance in GORM] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation.adoc index e69de29bb2..7b54fcda8b 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation.adoc @@ -0,0 +1,28 @@ +[[domainClasses-gormAssociation]] +== GORM Associations + +GORM supports all standard relationship types between domain classes. Each association type maps to a specific Hibernate/database pattern. + +[cols="1,2"] +|=== +|Association type | Declared with + +|Many-to-one / One-to-one +|A property of the target type + +|One-to-many (bidirectional) +|`hasMany` + `belongsTo` + +|One-to-many (unidirectional) +|`hasMany` only (join table) + +|Many-to-many +|`hasMany` on both sides + `belongsTo` on one +|=== + +Refer to the following subsections for details on each association type: + +* xref:gormAssociation-manyToOneAndOneToOne[Many-to-One and One-to-One] +* xref:gormAssociation-oneToMany[One-to-Many] +* xref:gormAssociation-manyToMany[Many-to-Many] +* xref:gormAssociation-basicCollectionTypes[Basic Collection Types] diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/basicCollectionTypes.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/basicCollectionTypes.adoc index e69de29bb2..862489a65e 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/basicCollectionTypes.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/basicCollectionTypes.adoc @@ -0,0 +1,88 @@ +[[gormAssociation-basicCollectionTypes]] +== Basic Collection Types + +In addition to collections of domain objects, GORM supports collections of basic types — strings, numbers, enums, and other persistable values. These are stored in a separate join table. + +=== String Collections + +[source,groovy] +---- +class Author { + String name + static hasMany = [nicknames: String] // <1> +} +---- +<1> A join table `author_nicknames` is created with a `nicknames` column holding the string values. + +=== Numeric Collections + +[source,groovy] +---- +class Survey { + String question + static hasMany = [scores: Integer] +} +---- + +=== Enum Collections + +Collections of `enum` types are supported: + +[source,groovy] +---- +enum Status { ACTIVE, INACTIVE, SUSPENDED } + +class Account { + String name + static hasMany = [allowedStatuses: Status] // <1> +} +---- +<1> A join table `account_allowed_statuses` is created. Enum values are stored using their ordinal position by default. + +To store enum values as their string names instead of ordinals, configure the `enumType` on the column: + +[source,groovy] +---- +class Account { + static hasMany = [allowedStatuses: Status] + static mapping = { + allowedStatuses { + column enumType: 'string' // <1> + } + } +} +---- +<1> Stores `'ACTIVE'`, `'INACTIVE'`, etc. instead of `0`, `1`, `2`. + +=== Customising the Join Table + +You can override the join table name and the value column name: + +[source,groovy] +---- +class Author { + static hasMany = [nicknames: String] + static mapping = { + nicknames joinTable: [ + name: 'author_alias', // <1> + column: 'alias_value' // <2> + ] + } +} +---- +<1> The join table name. +<2> The column that holds the basic value. + +=== Accessing and Modifying + +Basic collections behave like any other GORM `hasMany` — use `addTo*` and `removeFrom*`: + +[source,groovy] +---- +def author = Author.get(1) +author.addToNicknames('Graeme') +author.save() + +author.removeFromNicknames('Graeme') +author.save() +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToMany.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToMany.adoc index e69de29bb2..02518a8d5c 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToMany.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToMany.adoc @@ -0,0 +1,68 @@ +[[gormAssociation-manyToMany]] +== Many-to-Many Associations + +A many-to-many association is created when both domain classes declare `hasMany` pointing at each other, and one side also declares `belongsTo`. + +=== Declaring the Relationship + +[source,groovy] +---- +class Book { + String title + static hasMany = [authors: Author] + static belongsTo = Author // <1> +} + +class Author { + String name + static hasMany = [books: Book] +} +---- +<1> `belongsTo` without a property name makes `Book` the owned side. The owning side (`Author`) controls cascade save/delete. + +GORM creates a join table `author_books` with foreign-key columns for both sides. + +=== Saving a Many-to-Many + +Always save from the owning side (the side that does **not** have `belongsTo`): + +[source,groovy] +---- +def author = new Author(name: 'Graeme Rocher') +def book = new Book(title: 'Grails in Action') + +author.addToBooks(book) +author.save() // <1> +---- +<1> `book` is cascade-saved because `Author` is the owning side. + +=== Join Table Customisation + +Override the join table name and columns in the `mapping` block: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books joinTable: [ + name: 'author_to_book', + key: 'auth_id', + column: 'book_id' + ] + } +} +---- + +=== Bidirectional Access + +Both sides of the relationship can be navigated: + +[source,groovy] +---- +Author author = Author.get(1) +author.books.each { println it.title } + +Book book = Book.get(1) +book.authors.each { println it.name } +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToOneAndOneToOne.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToOneAndOneToOne.adoc index e69de29bb2..cc459d4c35 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToOneAndOneToOne.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/manyToOneAndOneToOne.adoc @@ -0,0 +1,78 @@ +[[gormAssociation-manyToOneAndOneToOne]] +== Many-to-One and One-to-One Associations + +=== Many-to-One + +Declare a many-to-one association by adding a property of the target domain class type: + +[source,groovy] +---- +class Book { + String title + Author author // <1> +} + +class Author { + String name +} +---- +<1> A foreign key column `author_id` is added to the `book` table. + +When combined with `belongsTo`, the association participates in cascade save/delete: + +[source,groovy] +---- +class Book { + String title + static belongsTo = [author: Author] // <1> +} +---- +<1> The `author` property is added implicitly; deleting `Author` cascades to `Book`. + +=== One-to-One + +A one-to-one association is also declared as a simple property, but each `Author` can only have one `Biography`: + +[source,groovy] +---- +class Author { + String name + Biography biography // <1> +} + +class Biography { + String summary + static belongsTo = [author: Author] +} +---- +<1> A unique foreign key `biography_id` is added to `author`. + +=== Configuring the Foreign Key Column + +Override the foreign key column name in the `mapping` block: + +[source,groovy] +---- +class Book { + String title + Author author + static mapping = { + author column: 'fk_author' + } +} +---- + +=== Nullable Associations + +By default, GORM-managed foreign key columns are non-nullable. To allow null: + +[source,groovy] +---- +class Book { + Author author + + static constraints = { + author nullable: true + } +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/oneToMany.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/oneToMany.adoc index e69de29bb2..ed9266f17e 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/oneToMany.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormAssociation/oneToMany.adoc @@ -0,0 +1,71 @@ +[[gormAssociation-oneToMany]] +== One-to-Many Associations + +A one-to-many association is declared using `hasMany`. It represents a collection of associated domain objects. + +=== Bidirectional One-to-Many + +When both sides declare the relationship, GORM manages the foreign key on the many side's table: + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] // <1> +} + +class Book { + String title + Author author // <2> + static belongsTo = [author: Author] +} +---- +<1> `Author` owns a collection of `Book` objects. +<2> `Book` declares the back-reference. `belongsTo` also enables cascade delete. + +With `belongsTo`, deleting an `Author` will cascade-delete all of its `books`. + +=== Unidirectional One-to-Many + +Without `belongsTo` on the other side, GORM uses a join table to maintain the relationship: + +[source,groovy] +---- +class Author { + String name + static hasMany = [books: Book] +} + +class Book { + String title + // no belongsTo or author property +} +---- + +The join table name defaults to `author_books` and can be customised — see xref:ormdsl-tableAndColumnNames[Table and Column Names]. + +=== Sorting + +You can define a default sort order for the collection: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books sort: 'title', order: 'asc' + } +} +---- + +=== Adding and Removing Items + +[source,groovy] +---- +def author = Author.get(1) +author.addToBooks(new Book(title: 'Groovy in Action')) +author.save() + +author.removeFromBooks(author.books.first()) +author.save() +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormComposition.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormComposition.adoc index e69de29bb2..2e77b9cd76 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormComposition.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/gormComposition.adoc @@ -0,0 +1,61 @@ +[[domainClasses-gormComposition]] +== GORM Composition (Embedded Objects) + +GORM supports composition via `embedded`, which maps a non-domain class as a set of columns on the owning table rather than a separate table. + +=== Declaring an Embedded Component + +[source,groovy] +---- +class Address { + String street + String city + String postalCode + String country +} + +class Person { + String name + Address address // <1> + + static embedded = ['address'] // <2> +} +---- +<1> `Address` is a plain Groovy class (not a domain class). +<2> Declaring it in `embedded` maps its properties as columns on the `person` table. + +The `person` table will contain columns: `name`, `address_street`, `address_city`, `address_postal_code`, `address_country`. + +=== Overriding Column Names + +Use the `mapping` block to rename the embedded columns: + +[source,groovy] +---- +class Person { + static embedded = ['address'] + static mapping = { + address { + street column: 'addr_street' + city column: 'addr_city' + } + } +} +---- + +=== Nullable Embedded Objects + +If the embedded object can be absent, mark it as nullable in constraints: + +[source,groovy] +---- +class Person { + Address address + static embedded = ['address'] + static constraints = { + address nullable: true + } +} +---- + +NOTE: Embedded components are always persisted as part of the owning entity. There is no separate table, no `id`, and no lifecycle management for the embedded object. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/inheritanceInGORM.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/inheritanceInGORM.adoc index e69de29bb2..66a7d3bf83 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/inheritanceInGORM.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/inheritanceInGORM.adoc @@ -0,0 +1,54 @@ +[[domainClasses-inheritanceInGORM]] +== Inheritance in GORM + +GORM supports standard Groovy/Java class inheritance. Domain classes in a hierarchy all benefit from GORM persistence. + +See xref:ormdsl-inheritanceStrategies[Inheritance Strategies] for the available mapping strategies (table-per-hierarchy, table-per-subclass, table-per-concrete-class) and how to configure them. + +=== Basic Inheritance + +[source,groovy] +---- +class Content { + String title + Date dateCreated +} + +class BlogPost extends Content { + String body + String author +} + +class Page extends Content { + String html + String slug +} +---- + +By default all three classes are stored in a single `content` table (table-per-hierarchy). GORM uses a `class` discriminator column to distinguish rows. + +=== Querying the Hierarchy + +GORM queries are polymorphic by default — querying the parent class returns instances of all subclasses: + +[source,groovy] +---- +List<Content> all = Content.list() // returns BlogPost and Page instances + +List<BlogPost> posts = BlogPost.list() // returns only BlogPost instances +---- + +=== Abstract Base Classes + +You can use abstract domain classes as the root of a hierarchy. Abstract classes have no corresponding rows and cannot be instantiated directly: + +[source,groovy] +---- +abstract class Content { + String title +} + +class BlogPost extends Content { ... } +---- + +TIP: Prefer table-per-hierarchy (the default) for most use cases. It requires no `JOIN` for polymorphic queries and is the most performant strategy. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/sets,ListsAndMaps.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/sets,ListsAndMaps.adoc index e69de29bb2..c9183bd601 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/sets,ListsAndMaps.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/domainClasses/sets,ListsAndMaps.adoc @@ -0,0 +1,69 @@ +[[domainClasses-setsListsAndMaps]] +== Sets, Lists and Maps + +By default `hasMany` creates a `java.util.Set` collection (unordered, no duplicates). GORM also supports `List` and `Map` collection types. + +=== Sets (Default) + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] // Set<Book> by default +} +---- + +=== Lists (Ordered) + +Declare the collection property as a `List` to use a positional, ordered collection. GORM adds an `index` column to the join table: + +[source,groovy] +---- +class Author { + List books // <1> + static hasMany = [books: Book] +} +---- +<1> Declaring the field type as `List` tells GORM to use a list mapping with a position index. + +[source,groovy] +---- +def author = Author.get(1) +author.books[0] // <1> +---- +<1> Access by position — Hibernate maintains the order using an `idx` column in the join table. + +=== Maps (Key-Value) + +Declare the collection property as a `Map` to store key-value pairs. The key is typically a `String`: + +[source,groovy] +---- +class Author { + Map books // <1> + static hasMany = [books: Book] +} +---- +<1> Keys and values are stored in the join table. + +[source,groovy] +---- +def author = Author.get(1) +author.books['grailsInAction'] // <1> +---- +<1> Access by key. + +=== Sorting Sets + +For `Set`-based collections, define a default sort order in the `mapping` block: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books sort: 'title', order: 'asc' + } +} +---- + +TIP: `List` mappings incur the cost of maintaining a positional index column on every insert/reorder. Use them only when ordering matters. For most associations, the default `Set` is the better choice. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/introduction.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/introduction.adoc index e69de29bb2..ae4b738219 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/introduction.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/introduction.adoc @@ -0,0 +1,53 @@ +[[introduction]] +== Introduction + +GORM for Hibernate 7 (grails-data-hibernate7) is the Hibernate 7 persistence layer for GORM, the GRAILS Object Relational Mapping framework. It provides a high-level, convention-over-configuration API for mapping Groovy domain classes to a relational database via Hibernate ORM 7 and Jakarta EE 10. + +=== Features + +* Convention-based ORM mapping — minimal configuration for common patterns +* Full Hibernate 7 support with Jakarta EE 10 (`jakarta.*` packages) +* Spring Boot 3.5 integration +* Dynamic finders, named queries, `where` query DSL, HQL, and native SQL +* Optimistic locking, second-level caching, and batch fetching +* Comprehensive association mapping: one-to-one, one-to-many, many-to-many, basic collections +* Multiple inheritance strategies: table-per-hierarchy, table-per-subclass, table-per-concrete-class +* Multi-tenancy support +* Groovy `static mapping {}` DSL for full control over table/column names, types, and strategies + +=== Requirements + +[format="csv", options="header"] +|=== +Component,Version +JDK,17+ +Groovy,4.0.x +Spring Boot,3.5.x +Hibernate ORM,7.x +Jakarta EE,10 +|=== + +=== Quick Start + +Add the dependency to your Grails application and define a domain class: + +[source,groovy] +---- +class Book { + String title + String author + Date dateCreated + Date lastUpdated + + static constraints = { + title blank: false + author blank: false + } +} +---- + +GORM automatically: + +* Creates a `book` table with `id`, `version`, `title`, `author`, `date_created`, and `last_updated` columns +* Adds dynamic finders like `Book.findByTitle('...')`, `Book.findAllByAuthor('...')` +* Injects `save()`, `delete()`, `get()`, `list()`, and other persistence methods diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics.adoc index e69de29bb2..60841544e4 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics.adoc @@ -0,0 +1,4 @@ +[[persistenceBasics]] +== Persistence Basics + +This section covers the core persistence operations available on every GORM domain class: saving, updating, deleting, querying for changes, and transaction management. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/cascades.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/cascades.adoc index e69de29bb2..afae3b3bef 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/cascades.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/cascades.adoc @@ -0,0 +1,44 @@ +[[persistenceBasics-cascades]] +== Cascades + +Hibernate cascades propagate persistence operations from a parent entity to its associated children automatically. + +See xref:ormdsl-customCascadeBehaviour[Custom Cascade Behaviour] for the full reference on configuring cascade behaviour via the `mapping` block. + +=== Default Cascade Behaviour + +GORM applies `save-update` cascading by default on associations managed by `hasMany`. This means: + +- Saving an `Author` also saves any new or modified `Book` objects in its `books` collection. +- **Deleting an `Author` does NOT automatically delete its `books`** unless `belongsTo` is declared or `cascade: 'all'` is configured. + +=== Cascade with `belongsTo` + +Declaring `belongsTo` on the owned side automatically adds cascade-delete from the owning side: + +[source,groovy] +---- +class Book { + static belongsTo = [author: Author] // <1> +} +---- +<1> Deleting an `Author` cascade-deletes all associated `Book` rows. + +=== Cascade with `all-delete-orphan` + +Use `all-delete-orphan` to delete child rows that are removed from the collection: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] + static mapping = { + books cascade: 'all-delete-orphan' + } +} + +def author = Author.get(1) +author.books.remove(author.books.first()) // <1> +author.save() +---- +<1> The removed `Book` will be deleted from the database. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/deletingObjects.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/deletingObjects.adoc index e69de29bb2..51bc5d887c 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/deletingObjects.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/deletingObjects.adoc @@ -0,0 +1,53 @@ +[[persistenceBasics-deletingObjects]] +== Deleting Objects + +Call `delete()` on a loaded instance to remove it from the database: + +[source,groovy] +---- +def book = Book.get(1) +book.delete() +---- + +=== Flush on Delete + +[source,groovy] +---- +book.delete(flush: true) // issues DELETE immediately +---- + +=== Cascade Delete + +When a domain class declares `belongsTo`, deleting the parent also deletes its children: + +[source,groovy] +---- +class Author { + static hasMany = [books: Book] +} + +class Book { + static belongsTo = [author: Author] +} + +// Deletes the author AND all associated books +Author.get(1).delete() +---- + +To delete without cascading, remove the `belongsTo` and configure cascade behaviour explicitly — see xref:ormdsl-customCascadeBehaviour[Custom Cascade Behaviour]. + +=== Bulk Delete + +Use `deleteAll()` to delete all instances matching a criteria: + +[source,groovy] +---- +Book.where { genre == 'Horror' }.deleteAll() +---- + +Or with HQL: + +[source,groovy] +---- +Book.executeUpdate("delete Book where genre = :genre", [genre: 'Horror']) +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/fetching.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/fetching.adoc index e69de29bb2..eba41bb1ca 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/fetching.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/fetching.adoc @@ -0,0 +1,55 @@ +[[persistenceBasics-fetching]] +== Fetching + +Controlling how and when associated data is loaded is critical for application performance. + +See xref:ormdsl-fetchingDSL[Fetching Strategies] for full configuration details. + +=== Default: Lazy Loading + +Associations are loaded lazily by default — Hibernate does not query associated data until you access it: + +[source,groovy] +---- +def author = Author.get(1) // SELECT * FROM author WHERE id=1 +author.books.size() // SELECT * FROM book WHERE author_id=1 (triggered now) +---- + +=== N+1 Problem + +Loading a list of authors and accessing their books triggers one query per author: + +[source,groovy] +---- +Author.list().each { author -> + println author.books.size() // <1> +} +---- +<1> N additional queries for N authors — the N+1 problem. + +=== Solution: Eager Fetching with Join + +[source,groovy] +---- +// Option 1: query-time join +def authors = Author.findAll { + join 'books' +} + +// Option 2: mapping-level eager fetch (always eager — use with care) +static mapping = { + books fetch: 'join' +} +---- + +=== Batch Fetching + +A lighter alternative to `join` — fetch collections in batches to reduce query count without a cartesian product: + +[source,groovy] +---- +static mapping = { + books batchSize: 10 // <1> +} +---- +<1> Hibernate will initialise up to 10 book collections with a single `IN` query. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/locking.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/locking.adoc index e69de29bb2..9f152a8c45 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/locking.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/locking.adoc @@ -0,0 +1,40 @@ +[[persistenceBasics-locking]] +== Locking + +=== Optimistic Locking + +GORM enables optimistic locking by default via a `version` column. See xref:ormdsl-optimisticLockingAndVersioning[Optimistic Locking and Versioning] for full details. + +=== Pessimistic Locking + +To acquire a database-level lock on a row, use `lock()`: + +[source,groovy] +---- +Book.withTransaction { + def book = Book.lock(1) // <1> + book.title = 'Updated Safely' + book.save() +} // <2> +---- +<1> Issues `SELECT ... FOR UPDATE`, preventing concurrent reads/writes to this row. +<2> Lock is released when the transaction commits. + +You can also lock an already-loaded instance: + +[source,groovy] +---- +def book = Book.get(1) +book.lock() // upgrades to a pessimistic lock +---- + +=== Refresh + +To reload the current state from the database (discarding in-memory changes): + +[source,groovy] +---- +def book = Book.get(1) +// ... another thread modifies the book ... +book.refresh() // re-reads from the database +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/modificationChecking.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/modificationChecking.adoc index e69de29bb2..8e4b97d243 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/modificationChecking.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/modificationChecking.adoc @@ -0,0 +1,45 @@ +[[persistenceBasics-modificationChecking]] +== Modification Checking + +Hibernate tracks which properties have been modified since the entity was loaded. GORM exposes this via `isDirty()` and related methods. + +=== Checking if an Instance is Dirty + +[source,groovy] +---- +def book = Book.get(1) +book.isDirty() // false — just loaded + +book.title = 'New Title' +book.isDirty() // true — title has changed +---- + +=== Checking a Specific Property + +[source,groovy] +---- +book.isDirty('title') // true +book.isDirty('genre') // false — genre unchanged +---- + +=== Getting the Original Value + +[source,groovy] +---- +def book = Book.get(1) +println book.title // 'Original Title' +book.title = 'New Title' +println book.getPersistentValue('title') // 'Original Title' +---- + +=== Dirty Properties + +Get a list of all property names that have changed: + +[source,groovy] +---- +def book = Book.get(1) +book.title = 'New Title' +book.genre = 'Fiction' +println book.dirtyPropertyNames // ['title', 'genre'] +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/savingAndUpdating.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/savingAndUpdating.adoc index e69de29bb2..056658d272 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/savingAndUpdating.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/persistenceBasics/savingAndUpdating.adoc @@ -0,0 +1,76 @@ +[[persistenceBasics-savingAndUpdating]] +== Saving and Updating + +=== Saving + +Call `save()` on a domain instance to persist it. GORM delegates to Hibernate's `Session.saveOrUpdate()`: + +[source,groovy] +---- +def book = new Book(title: 'Groovy in Action', author: 'Dierk König') +book.save() +---- + +If validation fails, `save()` returns `null` and the errors are available on the instance: + +[source,groovy] +---- +def book = new Book(title: '') // violates blank constraint +if (!book.save()) { + book.errors.allErrors.each { println it } +} +---- + +=== Fail on Error + +Use `failOnError: true` to throw a `ValidationException` instead of returning `null`: + +[source,groovy] +---- +book.save(failOnError: true) // throws ValidationException on constraint violation +---- + +=== Flush + +By default Hibernate delays SQL writes until the session is flushed. Force an immediate flush: + +[source,groovy] +---- +book.save(flush: true) // <1> +---- +<1> Issues the `INSERT` or `UPDATE` immediately. + +=== Updating + +Modify properties on a loaded instance and call `save()`: + +[source,groovy] +---- +def book = Book.get(1) +book.title = 'Updated Title' +book.save() +---- + +=== Dynamic Update + +To generate `UPDATE` statements that only include changed columns (useful for wide tables), enable `dynamicUpdate`: + +[source,groovy] +---- +class Book { + static mapping = { + dynamicUpdate true + } +} +---- + +=== Dynamic Insert + +Similarly, `dynamicInsert` generates `INSERT` statements that omit null properties: + +[source,groovy] +---- +static mapping = { + dynamicInsert true +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/programmaticTransactions.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/programmaticTransactions.adoc index e69de29bb2..32a60e62c6 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/programmaticTransactions.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/programmaticTransactions.adoc @@ -0,0 +1,73 @@ +[[programmaticTransactions]] +== Programmatic Transactions + +GORM integrates with Spring's transaction management. All persistence operations should run within a transaction. + +=== `withTransaction` + +Use `withTransaction` on any domain class to run a block within a transaction: + +[source,groovy] +---- +Book.withTransaction { + new Book(title: 'Grails in Action', author: 'Glen Smith').save() + new Book(title: 'Groovy in Action', author: 'Dierk König').save() + // both are committed together; any exception rolls back both +} +---- + +The closure receives a `TransactionStatus` parameter if needed: + +[source,groovy] +---- +Book.withTransaction { TransactionStatus status -> + def book = new Book(title: 'Test') + book.save() + if (someCondition) { + status.setRollbackOnly() // <1> + } +} +---- +<1> Marks the transaction for rollback without throwing an exception. + +=== `withNewTransaction` + +Start a new, independent transaction (suspending the current one if any): + +[source,groovy] +---- +Book.withNewTransaction { + // runs in a brand-new transaction regardless of any outer transaction +} +---- + +=== `withSession` + +Access the underlying Hibernate `Session` directly: + +[source,groovy] +---- +Book.withSession { session -> + session.flush() + session.clear() // <1> +} +---- +<1> Evicts all entities from the first-level cache. + +=== Service-Layer Transactions + +In a Grails application, services are transactional by default. Annotate individual methods or the entire service class with Spring's `@Transactional` for fine-grained control: + +[source,groovy] +---- +import org.springframework.transaction.annotation.Transactional + +@Transactional +class BookService { + def transferOwnership(Long bookId, Long newAuthorId) { + def book = Book.get(bookId) + book.author = Author.get(newAuthorId) + book.save(failOnError: true) + } +} +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/querying.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/querying.adoc index e69de29bb2..8e9c9f846d 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/querying.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/querying.adoc @@ -0,0 +1,39 @@ +[[querying]] +== Querying + +GORM provides multiple querying mechanisms, ranging from simple dynamic finders to full SQL queries. + +* xref:querying-whereQueries[Where Queries (Criteria DSL)] — type-safe Groovy criteria queries +* xref:querying-hql[HQL Queries] — Hibernate Query Language +* xref:querying-nativeSql[Native SQL Queries] — raw database SQL via Hibernate + +=== Dynamic Finders + +The simplest form of querying uses auto-generated finder methods based on property names: + +[source,groovy] +---- +Book.findByTitle('Groovy in Action') +Book.findAllByAuthorAndGenre('Dierk König', 'Tech') +Book.countByGenre('Fiction') +Book.findByTitleLike('%Groovy%') +Book.findAllByPagesGreaterThan(300) +Book.findAllByTitleIlike('%groovy%') // case-insensitive +---- + +Finder methods support pagination: + +[source,groovy] +---- +Book.findAllByGenre('Fiction', [max: 10, offset: 0, sort: 'title', order: 'asc']) +---- + +=== `get`, `list`, `count` + +[source,groovy] +---- +Book.get(1) // by id +Book.list() // all +Book.list(max: 10, offset: 20) // paginated +Book.count() // total count +---- diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide.adoc index e69de29bb2..8c9250307e 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide.adoc @@ -0,0 +1,7 @@ +[[quickStartGuide]] +== Quick Start Guide + +This section covers getting up and running with GORM for Hibernate 7 quickly. + +For full configuration options, see xref:configuration[Configuration]. +For persistence basics, see xref:persistenceBasics[Persistence Basics]. diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc index e69de29bb2..d235c1bc18 100644 --- a/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc +++ b/grails-data-hibernate7/docs/src/docs/asciidoc/quickStartGuide/basicCRUD.adoc @@ -0,0 +1,89 @@ +[[quickStartGuide-basicCRUD]] +== Basic CRUD + +Every GORM domain class automatically gets Create, Read, Update, and Delete (CRUD) operations. + +=== Create + +[source,groovy] +---- +// Constructor with named parameters +def book = new Book(title: 'Groovy in Action', author: 'Dierk König') +book.save() + +// Or using the create() factory method +def book = Book.create(title: 'Grails in Action', author: 'Glen Smith') +---- + +=== Read + +[source,groovy] +---- +// By primary key +def book = Book.get(1) + +// Returns null if not found +def book = Book.get(999) // null + +// Get multiple by IDs +def books = Book.getAll(1, 2, 3) + +// Load a proxy (no immediate SELECT) +def book = Book.load(1) + +// List all +def books = Book.list() + +// With pagination +def books = Book.list(max: 10, offset: 0, sort: 'title', order: 'asc') + +// Dynamic finders +def book = Book.findByTitle('Groovy in Action') +def books = Book.findAllByAuthor('Dierk König') +def count = Book.countByAuthor('Dierk König') +---- + +=== Update + +[source,groovy] +---- +def book = Book.get(1) +book.title = 'Updated Title' +book.save() +---- + +=== Delete + +[source,groovy] +---- +def book = Book.get(1) +book.delete() +---- + +=== Validation + +`save()` runs constraints before persisting and returns `null` if validation fails: + +[source,groovy] +---- +def book = new Book(title: '') // blank title violates constraint +if (!book.save()) { + println book.errors.allErrors // print validation errors +} + +// Throw on failure instead +book.save(failOnError: true) // throws ValidationException +---- + +=== Transactions + +GORM operations run inside Hibernate sessions. Use `withTransaction` for explicit transaction control: + +[source,groovy] +---- +Book.withTransaction { + def book = new Book(title: 'Tx Book', author: 'Author') + book.save() + // any exception here rolls back the transaction +} +----
