The labs on this site need JavaScript to run. The same content is written out below.
Learn › JPA › Entity mapping
Value types and embeddable types
How is a class that groups an address stored in a table, and why make it immutable?
Values that always go together, such as a city and a street, are easier to handle when you group them in a class like Address. A class like this has no id, and the values it holds are all there is to it, so it is called a value type. Unlike an entity, a value type is not stored on its own. It is stored along with the entity that holds it: a single value goes into columns of the table of that entity, and a collection of values goes into a separate table whose foreign key is the id of that entity. Here you see three scenes: what shape the table takes, what happens when two entities share the same Address object, and what SQL is sent when you change a collection that holds several value types.
Checked against real execution · All 56 combinations were run on Hibernate 7.4.5 and H2, and the tables, the SQL that was sent, the values left, and the exception at startup were checked against the results
Concepts in this lab
- Value type
- This is a type whose meaning is decided only by the values it holds, without an identifier (id). It includes basic values such as numbers and strings, embeddable types that group several values like an address, and element collections that hold several of them. An entity is told apart by its id and is tracked as the same thing even when its values change, but a value type is stored as part of the entity that holds it, and it disappears together with that entity when the entity is deleted. If several entities point to the same value type object, a value changed through one of them is also seen through the others, so as a rule a value type should be made an immutable object.
- Embeddable type (@Embeddable)
- This is a value type that groups values used together, such as a city and a street, into one class and uses it as a field of an entity. You put @Embeddable on the class and @Embedded on the field that uses it. No separate table is created, and the fields inside it are spread out and stored as columns of the entity's table. So the table shape and the SQL are the same as when you keep the fields as they are, and only in the Java code are the related values and methods gathered in one place. If you use the same embeddable type twice in one entity, the column names collide, so you rename them with @AttributeOverride.
- Element collection (@ElementCollection)
- This is a List or Set that holds several value types, used as a field of an entity. You cannot put several values in the columns of one row, so each value is stored as one row in a separate table that has the id of the entity as a foreign key. A value type has no id, so it is hard to point to which row in the database changed. So when the collection changes, SQL that deletes all the rows of that entity and inserts them again may be sent. Use an element collection when there are only a few values and they rarely change. Otherwise, make them an entity and solve it with a one-to-many relationship.
- Immutable object
- This is an object whose state does not change after it is created. Because its value never changes, it is safe for several threads to use at the same time, you can pass it to other code without worrying that it will be changed, and it is well suited for caching or for use as a key in a hash collection. Declare the fields private final, provide no methods that change the state, and make the class final so that a subclass cannot change its behavior. If a field refers to a mutable object (such as a List or a Date), the constructor and the getters pass copies in and out (defensive copying) so that outside code cannot change the internals.
- Dirty checking
- The feature that automatically runs UPDATE statements for changed entities. When the values of an entity managed by the persistence context change, JPA compares the entity with the state it first read at flush time (usually just before the commit), and it runs an UPDATE statement for each entity whose values differ. By default, Hibernate puts all columns in the UPDATE statement, not only the changed columns. To include only the changed columns, use @DynamicUpdate. So if you only change the values of an entity that you loaded, the change is applied to the database without calling a separate save method.
Try these
- Default settings — The city was changed only for Alice, but Bob's row is UPDATEd as well.
- Putting in a copy — The objects are separate, so only Alice's row is UPDATEd.
- Making the value type immutable — There is no setter, so there is no way to change the shared object.
- Table of an embeddable type — No Address table is created. The fields become columns of the member table.
- Using the same value type twice — The column names collide, so an exception is thrown at startup.
- Changing only one value in an element collection — All the addresses of that member are deleted and inserted again.
- With a Set and all columns NOT NULL — Only the changed row is deleted and inserted.
Running with the default settings
- new Address("Boston", "Main St"): Creates one Address object. The variable addr points to this object.
- new Member("Alice", addr): The address field of Alice points to the same object as addr. The object was not copied. There is just one more reference to the same object.
- new Member("Bob", addr): The address field of Bob also points to the same object. Now two members share one Address object.
- em.persist(member1): An INSERT is sent and the row of Alice is stored. The values of the address go into the city and street columns of the member table.
- em.persist(member2): An INSERT is sent and the row of Bob is stored. In the table, the two rows each hold their own city and street values. The fact that the object is shared is not recorded in the table.
- member1.getAddress().setCity("Denver"): This is code that tries to change only Alice's address. But what it changes is the object that the two members share. member2.getAddress().getCity() is now "Denver" as well.
- Transaction commit, dirty checking: Just before commit, JPA compares the current values of the entities it manages with the values that were first stored. The city of Alice changed from Boston to Denver, so an UPDATE is sent for the row of Alice.
- It compares Bob too: The address of Bob points to the same object, so its city has changed to Denver as well. An UPDATE is sent for the row of Bob too. Only Alice's city was meant to change, but the address of Bob changed as well.
Result of this run
- When two entities point to the same value type object, a value changed through one of them is also seen through the other. JPA sees both entities as changed and sends 2 UPDATEs. There is no compile error and no exception, so it becomes a bug that is hard to find.
- In Java, when you put an object in a variable or pass it on, the object is not copied. Only the reference is copied. So simply "being careful not to share" is hard to rely on.
Code
@Embeddable
class Address {
String city;
String street;
void setCity(String city) { this.city = city; } // the value can be changed
}
@Entity
class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
@Embedded
Address address;
}
// Constructors and getters are omitted
// Inside a transaction
Address addr = new Address("Boston", "Main St");
Member member1 = new Member("Alice", addr);
Member member2 = new Member("Bob", addr);
em.persist(member1);
em.persist(member2);
member1.getAddress().setCity("Denver");
// Transaction commit
Assumptions of this model
All 56 combinations (5 table shapes, 3 ways of sharing, and for element collections 4 kinds x 3 operations x 2, 3, 4, or 5 addresses) were run on Hibernate 7.4.5 and an H2 database with code of the same structure. They were checked against the screen for the following: the tables and columns that were created, the kinds of SQL that were sent and the count per table, the SQL that was sent before commit, whether the DELETE is a statement that deletes all the rows of that member, the values left in the database, whether the address that was read back is null, and the exception at startup. The results for element collections are the behavior of Hibernate 7.4.5 and may differ by version. In particular, the fact that a Set whose columns allow null deletes everything and inserts it again, like a List, was confirmed by running it. Which row numbers are deleted and changed with @OrderColumn was confirmed from the conditions of the real SQL statements (member_id and idx) and the rows left after the run. The primary key uses the IDENTITY strategy, so the INSERT is sent as soon as persist is called. The sharing scene is what happens inside one transaction that saves two new members. Entities that are read from the database each get their own newly created value type objects, so this problem occurs only when the code itself puts in the same object. The SQL statements on the screen are shortened to be easy to read, so they are not letter for letter the same as the real statements. The order of the columns in the table may also differ from the real one.
Previous lab: Entity mapping and primary keys · Next lab: The owning side of a relationship