From 7a28b3caedac32f6944b99d10094a068c3c9d5ef Mon Sep 17 00:00:00 2001 From: Gavin Date: Mon, 8 May 2023 11:23:03 +0200 Subject: [PATCH] finish section on ids --- .../main/asciidoc/introduction/Entities.adoc | 249 ++++++++++++++++-- 1 file changed, 226 insertions(+), 23 deletions(-) diff --git a/documentation/src/main/asciidoc/introduction/Entities.adoc b/documentation/src/main/asciidoc/introduction/Entities.adoc index 2b6a3586fa..8db4b56b1e 100644 --- a/documentation/src/main/asciidoc/introduction/Entities.adoc +++ b/documentation/src/main/asciidoc/introduction/Entities.adoc @@ -29,7 +29,7 @@ An entity must: - be a non-`final` class, - with a non-`private` constructor with no parameters. -On the other hand, the entity class may be concrete or `abstract`, and it may have any number of additional constructors. +On the other hand, the entity class may be either concrete or `abstract`, and it may have any number of additional constructors. Every entity class must be annotated `@Entity`. @@ -42,14 +42,28 @@ class Book { } ---- -Alternatively, when XML-based mappings are used, the `` element is used to declare an entity class: +Alternatively, the class may be identified as an entity type by providing an XML-based mapping for the class. + +[TIP] +.Mapping entities using XML +==== +When XML-based mappings are used, the `` element is used to declare an entity class: [source,xml] ---- -org.hibernate.example + + org.hibernate.example - ... + + ... + + + ... + ---- +We won't have much more to say about XML-based mappings in this Introduction, since it's not our preferred way to do things. +But since the `orm.xml` mapping file format defined by the JPA specification was modelled closely on the annotation-based mappings, it's usually easy to go back and forth between the two options. +==== Each entity class has a default _access type_, either: @@ -62,17 +76,30 @@ Concretely: - if a field is annotated `@Id`, field access is used, or - if a getter method is annotated `@Id`, property access is used. +Back when Hibernate was just a baby, property access was quite popular in the Hibernate community. +Today, however, field access is _much_ more common. + [NOTE] .Explicit access type ==== -The access type may be specified explicitly using the `@Access` annotation, but we strongly discourage this, since it's ugly and never necessary. +The default access type may be specified explicitly using the `@Access` annotation, but we strongly discourage this, since it's ugly and never necessary. ==== +[IMPORTANT] +.Mapping annotations should be placed consistently +==== +If the `@Id` annotation occurs on a field, the other mapping annotations should also be applied to field; or, if the `@Id` annotation occurs on a getter, the other mapping annotations should be applied to getters. + +It is in principle possible to mix field and property access using explicit `@Access` annotations at the attribute level. +We don't recommend doing this. +==== + +Every entity must have an identifier attribute. + [identifier-attributes] === Identifier attributes -Every entity must have an identifier attribute. -The attribute is usually a field: +An identifier attribute is usually a field: [source,java] ---- @@ -105,21 +132,32 @@ class Book { } ---- +An identifier attribute must be annotated `@Id` or `@EmbeddedId`. + +Identifier values may be: + +- assigned by the application, that is, by your Java code, or +- generated and assigned by Hibernate. + +[generated-identifiers] +=== Generated identifiers + An identifier is often system-generated, in which case it should be annotated `@GeneratedValue`: [source,java] ---- -@Entity -class Book { - Book() {} - - @Id @GeneratedValue - Long id; - - ... -} +@Id @GeneratedValue +Long id; ---- +[TIP] +.Using surrogate keys +==== +System-generated identifiers, or _surrogate keys_ make it easier to evolve or refactor the relational data model. +If you have the freedom to define the relational schema, we recommend the use of surrogate keys. +On the other hand, if, as is more common, you're working with a pre-existing database schema, you might not have the option. +==== + JPA defines the following strategies for generating ids, which are enumerated by `GenerationType`: |=== @@ -132,20 +170,178 @@ JPA defines the following strategies for generating ids, which are enumerated by | `GenerationType.AUTO` | `Long` or `Integer` | Selects `SEQUENCE` `TABLE`, or `UUID` based on the identifier type and capabilities of the database. |=== +For example, the following id maps to a SQL `identity`, `auto_increment`, or `bigserial` column: + +[source,java] +---- +@Id @GeneratedValue(strategy=IDENTITY) +Long id; +---- + +The `@SequenceGenerator` and `@TableGenerator` annotations allow further control over `SEQUENCE` and `TABLE` generation respectively. + +Consider this sequence generator: + +[source,java] +---- +@SequenceGenerator(name="bookSeq", sequenceName="seq_book", + initialValue = 5, allocationSize=10) +---- + +Values are generated using a database sequence defined as follows: + +[source,sql] +---- +create sequence seq_book start with 5 increment by 10 +---- + +[IMPORTANT] +.Check the `initialValue` and `allocationSize` +==== +If you let Hibernate export your database schema, the sequence definition will have the right `start with` and `increment` values. +But if you're working with a database schema managed outside of Hibernate, makes sure the `initialValue` and `allocationSize` members of `@SequenceGenerator` match the `start with` and `increment` specified in the DDL. +==== + +Any identifier attribute may now make use of the generator named `bookSeq`: + +[source,java] +---- +@Id @GeneratedValue(strategy=SEQUENCE, generator="bookSeq") +Long id; +---- + +Actually, it's extremely common to place the `@SequenceGenerator` annotation on the `@Id` attribute that makes use of it: + +[source,java] +---- +@Id @GeneratedValue(strategy=SEQUENCE, generator="bookSeq") +@SequenceGenerator(name="bookSeq", sequenceName="seq_book", + initialValue = 5, allocationSize=10) +Long id; +---- + +[NOTE] +.JPA id generators may be shared between entities +==== +A `@SequenceGenerator` or `@TableGenerator` must have a name, and may be shared between multiple id attributes. +This fits somewhat uncomfortably with the common practice of annotating the `@Id` attribute which makes use of the generator! +==== + +As you can see, JPA provides quite adequate support for the most common strategies for system-generated ids. +However, the annotations themselves are a little more intrusive than they should be, and there's no well-defined way to extend this framework to support custom strategies for id generation. +Nor may `@GeneratedValue` be used on a property not annotated `@Id`. +Since custom id generation is a rather common requirement, Hibernate provides a very carefully-designed framework for user-defined ``Generator``s. + +[TIP] +.Defining your own id generators +==== +JPA doesn't define a standard way to extend the set of id generation strategies, but Hibernate does: + +- the `Generator` hierarchy of interfaces in the package `org.hibernate.generator` lets you define new generators, and +- the `@IdGeneratorType` meta-annotation from the package `org.hibernate.annotations` lets you write an annotation which associates a `Generator` type with identifier attributes. + +Furthermore, the `@ValueGenerationType` meta-annotation lets you write an annotation which associates a `Generator` type with a non-`@Id` attribute. + +These APIs are new in Hibernate 6, and supersede the classic `IdentifierGenerator` interface from older versions of Hibernate. +You can find out more from the Javadoc for `@IdGeneratorType` and for `org.hibernate.generator`. +==== + +Not every id maps to a (system-generated) surrogate key. +Primary keys which are meaningful to the user of the system are called _natural keys_. +Of particular interest are natural keys which comprise more than one database column, and such natural keys are called _composite keys_. + +[composite-identifiers] +=== Composite identifiers + +If your database uses composite keys, you'll need more than one identifier attribute. +There are two ways to map composite keys in JPA: + +- using an `@IdClass`, or +- using an `@EmbeddedId`. + +Perhaps the most immediately-natural way to represent this in an entity class is with multiple fields annotated `@Id`, for example: + [source,java] ---- @Entity +@IdClass(BookId.class) class Book { Book() {} - @Id @GeneratedValue(strategy=IDENTITY) - Long id; + @Id + String isbn; + + @Id + int printing; ... } ---- -The `@SequenceGenerator` and `@TableGenerator` annotations allow further control over id generation: +But this approach comes with a problem: what object can we use to identify a `Book` and pass to methods like `find()` which accept an identifier? + +The solution is to write a separate class with fields that match the identifier attributes of the entity. +The `@IdClass` annotation of the `Book` entity identifies the id class to use for that entity: + +[source,java] +---- +class BookId { + + String isbn; + int printing; + + BookId() {} + + BookId(String isbn, int printing) { + this.isbn = isbn; + this.printing = printing; + } + + @Override + public boolean equals(Object other) { + if (other instanceof BookId) { + BookId bookId = (BookId) other; + return bookId.isbn.equals(isbn) + && bookId.printing == printing; + } + else { + return false; + } + } + + @Override + public int hashCode() { + return isbn.hashCode(); + } +} +---- + +IMPORTANT: Every id class should override `equals()` and `hashCode()`. + +This is not our preferred approach. +Instead, we recommend that the `BookId` class be declared as an `@Embeddable` type: + +[source,java] +---- +@Embeddable +class BookId { + + String isbn; + + int printing; + + BookId() {} + + BookId(String isbn, int printing) { + this.isbn = isbn; + this.printing = printing; + } + + ... +} +---- + +And now the entity class may reuse this definition using `@EmbeddedId`: [source,java] ---- @@ -153,11 +349,18 @@ The `@SequenceGenerator` and `@TableGenerator` annotations allow further control class Book { Book() {} - @Id @GeneratedValue(strategy=SEQUENCE, generator="bookSeq") - @SequenceGenerator(name="bookSeq", sequenceName="seq_book", - allocationSize=10) - Long id; + @EmbeddedId + BookId bookId; ... } +---- + +This second approach eliminates some duplicated code. + +Either way, we may now use `BookId` to obtain instances of `Book`: + +[source,java] +---- +Book book = session.find(Book.class, new BookId(isbn, printing)); ---- \ No newline at end of file