Глава 6. Маппинг коллекций

6.1. Персистентные коллекции

Этот раздел не содержит примеры Java-кода. Мы предполагаем, что вы уже знаете как использовать библиотеку коллекций Java.

Hibernate может сохранять коллекции следующих типов: java.util.Map, java.util.Set, java.util.SortedMap, java.util.SortedSet, java.util.List, любой масив персистентных сущностей и значений. Свойства типов: java.util.Collection или java.util.List могут так же сохранаться с семантикой "bag".

Предостережение: персистентные коллекции не созраняют дополнительной семантики добавляемой конкретной реализацией класса (например "упорядоченность" LinkedHashSet). Персистентные коллекцие ведут себя как следующие реализации: HashMap, HashSet, TreeMap, TreeSet и ArrayList соотвественно. Более того, Java-типы свойств содержащих коллекции должны быть интерфейсам (такие как Map, Set или List; не HashMap, TreeSet или ArrayList). Это ограничение связано с тем, что пока вы не видите, Hibernate незаметно заменяет ваши экземляры своими персистентными реализациями Map, Set or List (Так же будте аккуратны при использовании == на ваши коллекции)

Cat cat = new DomesticCat();
Cat kitten = new DomesticCat();
....
Set kittens = new HashSet();
kittens.add(kitten);
cat.setKittens(kittens);
session.save(cat);
kittens = cat.getKittens(); //Коллекция kittens - множество (Set)
(HashSet) cat.getKittens(); //Ошибка!

Коллекции подчиняются общим правилам типов-значений: не распределяется доступ, создаются и удаляются вместе с содержащей их сущностью. Вследствие того как они храняться в реляционной модели, коллекции не поддерживают null-значений; Hibernate не делает различий между пустой коллекцией и коллекцией со значением null.

Коллекции автоматически сохраняются когда на них ссылается персистентный объект и автоматически удаляются если никто на них не ссылается. Если коллекция передается от одного персистентного объекта другому, ее элеметны могут быть перемещены из одной таблицы в другую. Вам не нужно беспокоится об этом. Просто используйте коллекции Hiberante так же как вы используете коллекции Java, при этом убедитесь что вы понимаете семантику двухсторонних ассоциаций (описаны ниже).

Экземпляры коллекций на уровне базы данных харрактеризуются внешним ключом на содержащую сущность. Такой внешний ключ рассматривается как ключ коллекции и определяется в документе маппинга при помощи элемента <key>.

Collections may contain almost any other Hibernate type, including all basic types, custom types, entity types and components. This is an important definition: An object in a collection can either be handled with "pass by value" semantics (it therefore fully depends on the collection owner) or it can be a reference to another entity with an own lifecycle. Collections may not contain other collections. The contained type is referred to as the collection element type. Collection elements are mapped by <element>, <composite-element>, <one-to-many>, <many-to-many> or <many-to-any>. The first two map elements with value semantics, the other three are used to map entity associations.

All collection types except Set and bag have an index column - a column that maps to an array or List index or Map key. The index of a Map may be of any basic type, an entity type or even a composite type (it may not be a collection). The index of an array or list is always of type integer. Indexes are mapped using <index>, <index-many-to-many>, <composite-index> or <index-many-to-any>.

There are quite a range of mappings that can be generated for collections, covering many common relational models. We suggest you experiment with the schema generation tool to get a feeling for how various mapping declarations translate to database tables.

6.2. Mapping a Collection

Collections are declared by the <set>, <list>, <map>, <bag>, <array> and <primitive-array> elements. <map> is representative:

<map
name="propertyName" (1)
table="table_name"(2)
schema="schema_name"(3)
lazy="true|false" (4)
inverse="true|false"(5)
cascade="all|none|save-update|delete|all-delete-orphan" (6)
sort="unsorted|natural|comparatorClass" (7)
order-by="column_name asc|desc" (8)
where="arbitrary sql where condition" (9)
outer-join="true|false|auto"(10)
batch-size="N"(11)
access="field|property|ClassName" (12)
>

<key .... />
<index .... />
<element .... />
</map>
(1)

name the collection property name

(2)

table (optional - defaults to property name) the name of the collection table (not used for one-to-many associations)

(3)

schema (optional) the name of a table schema to override the schema declared on the root element

(4)

lazy (optional - defaults to false) enable lazy initialization (not used for arrays)

(5)

inverse (optional - defaults to false) mark this collection as the "inverse" end of a bidirectional association

(6)

cascade (optional - defaults to none) enable operations to cascade to child entities

(7)

sort (optional) specify a sorted collection with natural sort order, or a given comparator class

(8)

order-by (optional, JDK1.4 only) specify a table column (or columns) that define the iteration order of the Map, Set or bag, together with an optional asc or desc

(9)

where (optional) specify an arbitrary SQL WHERE condition to be used when retrieving or removing the collection (useful if the collection should contain only a subset of the available data)

(10)

outer-join (optional) specify that the collection should be fetched by outer join, whenever possible. Only one collection may be fetched by outer join per SQL SELECT.

(11)

batch-size (optional, defaults to 1) specify a "batch size" for lazily fetching instances of this collection.

(12)

access (optional - defaults to property): The strategy Hibernate should use for accessing the property value.

The mapping of a List or array requires a seperate table column holding the array or list index (the i in foo[i]). If your relational model doesn't have an index column, e.g. if you're working with legacy data, use an unordered Set instead. This seems to put people off who assume that List should just be a more convenient way of accessing an unordered collection. Hibernate collections strictly obey the actual semantics attached to the Set, List and Map interfaces. List elements don't just spontaneously rearrange themselves!

On the other hand, people who planned to use the List to emulate bag semantics have a legitimate grievance here. A bag is an unordered, unindexed collection which may contain the same element multiple times. The Java collections framework lacks a Bag interface, hence you have to emulate it with a List. Hibernate lets you map properties of type List or Collection with the <bag> element. Note that bag semantics are not really part of the Collection contract and they actually conflict with the semantics of the List contract (however, you can sort the bag arbitrarily, discussed later in this chapter).

Note: Large Hibernate bags mapped with inverse="false" are inefficient and should be avoided; Hibernate can't create, delete or update rows individually, because there is no key that may be used to identify an individual row.

6.3. Collections of Values and Many-To-Many Associations

A collection table is required for any collection of values and any collection of references to other entities mapped as a many-to-many association (the natural semantics for a Java collection). The table requires (foreign) key column(s), element column(s) and possibly index column(s).

The foreign key from the collection table to the table of the owning class is declared using a <key> element.

<key column="column_name"/>
(1)

column (required): The name of the foreign key column.

For indexed collections like maps and lists, we require an <index> element. For lists, this column contains sequential integers numbered from zero. Make sure that your index really starts from zero if you have to deal with legacy data. For maps, the column may contain any values of any Hibernate type.

<index
column="column_name"(1)
type="typename" (2)
/>
(1)

column (required): The name of the column holding the collection index values.

(2)

type (optional, defaults to integer): The type of the collection index.

Alternatively, a map may be indexed by objects of entity type. We use the <index-many-to-many> element.

<index-many-to-many
column="column_name"(1)
class="ClassName" (2)
/>
(1)

column (required): The name of the foreign key column for the collection index values.

(2)

class (required): The entity class used as the collection index.

For a collection of values, we use the <element> tag.

<element
column="column_name"(1)
type="typename" (2)
/>
(1)

column (required): The name of the column holding the collection element values.

(2)

type (required): The type of the collection element.

A collection of entities with its own table corresponds to the relational notion of many-to-many association. A many to many association is the most natural mapping of a Java collection but is not usually the best relational model.

<many-to-many
column="column_name" (1)
class="ClassName"(2)
outer-join="true|false|auto" (3)
/>
(1)

column (required): The name of the element foreign key column.

(2)

class (required): The name of the associated class.

(3)

outer-join (optional - defaults to auto): enables outer-join fetching for this association when hibernate.use_outer_join is set.

Some examples, first, a set of strings:

<set name="names" table="NAMES">
<key column="GROUPID"/>
<element column="NAME" type="string"/>
</set>

A bag containing integers (with an iteration order determined by the order-by attribute):

<bag name="sizes" table="SIZES" order-by="SIZE ASC">
<key column="OWNER"/>
<element column="SIZE" type="integer"/>
</bag>

An array of entities - in this case, a many to many association (note that the entities are lifecycle objects, cascade="all"):

<array name="foos" table="BAR_FOOS" cascade="all">
<key column="BAR_ID"/>
<index column="I"/>
<many-to-many column="FOO_ID" class="org.hibernate.Foo"/>
</array>

A map from string indices to dates:

<map name="holidays" table="holidays" schema="dbo" order-by="hol_name asc">
<key column="id"/>
<index column="hol_name" type="string"/>
<element column="hol_date" type="date"/>
</map>

A list of components (discussed in the next chapter):

<list name="carComponents" table="car_components">
<key column="car_id"/>
<index column="posn"/>
<composite-element class="org.hibernate.car.CarComponent">
<property name="price" type="float"/>
<property name="type" type="org.hibernate.car.ComponentType"/>
<property name="serialNumber" column="serial_no" type="string"/>
</composite-element>
</list>

6.4. One-To-Many Associations

A one to many association links the tables of two classes directly, with no intervening collection table. (This implements a one-to-many relational model.) This relational model loses some of the semantics of Java collections:

  • No null values may be contained in a map, set or list

  • An instance of the contained entity class may not belong to more than one instance of the collection

  • An instance of the contained entity class may not appear at more than one value of the collection index

An association from Foo to Bar requires the addition of a key column and possibly an index column to the table of the contained entity class, Bar. These columns are mapped using the <key> and <index> elements described above.

The <one-to-many> tag indicates a one to many association.

<one-to-many class="ClassName"/>
(1)

class (required): The name of the associated class.

Example:

<set name="bars">
<key column="foo_id"/>
<one-to-many class="org.hibernate.Bar"/>
</set>

Notice that the <one-to-many> element does not need to declare any columns. Nor is it necessary to specify the table name anywhere.

Very Important Note: If the <key> column of a <one-to-many> association is declared NOT NULL, Hibernate may cause constraint violations when it creates or updates the association. To prevent this problem, you must use a bidirectional association with the many valued end (the set or bag) marked as inverse="true". See the discussion of bidirectional associations later in this chapter.

6.5. Lazy Initialization

Collections (other than arrays) may be lazily initialized, meaning they load their state from the database only when the application needs to access it. Initialization happens transparently to the user so the application would not normally need to worry about this (in fact, transparent lazy initialization is the main reason why Hibernate needs its own collection implementations). However, if the application tries something like this:

s = sessions.openSession();
User u = (User) s.find("from User u where u.name=?", userName, Hibernate.STRING).get(0);
Map permissions = u.getPermissions();
s.connection().commit();
s.close();

Integer accessLevel = (Integer) permissions.get("accounts");// Error!

It could be in for a nasty surprise. Since the permissions collection was not initialized when the Session was committed, the collection will never be able to load its state. The fix is to move the line that reads from the collection to just before the commit. (There are other more advanced ways to solve this problem, however.)

Alternatively, use a non-lazy collection. Since lazy initialization can lead to bugs like that above, non-laziness is the default. However, it is intended that lazy initialization be used for almost all collections, especially for collections of entities (for reasons of efficiency).

Exceptions that occur while lazily initializing a collection are wrapped in a LazyInitializationException.

Declare a lazy collection using the optional lazy attribute:

<set name="names" table="NAMES" lazy="true">
<key column="group_id"/>
<element column="NAME" type="string"/>
</set>

In some application architectures, particularly where the code that accesses data using Hibernate, and the code that uses it are in different application layers, it can be a problem to ensure that the Session is open when a collection is initialized. They are two basic ways to deal with this issue:

  • In a web-based application, a servlet filter can be used to close the Session only at the very end of a user request, once the rendering of the view is complete. Of course, this places heavy demands upon the correctness of the exception handling of your application infrastructure. It is vitally important that the Session is closed and the transaction ended before returning to the user, even when an exception occurs during rendering of the view. The servlet filter has to be able to access the Session for this approach. We recommend that a ThreadLocal variable be used to hold the current Session (see chapter 1, Раздел 1.4, «Поиграем с котами», for an example implementation).

  • In an application with a seperate business tier, the business logic must "prepare" all collections that will be needed by the web tier before returning. This means that the business tier should load all the data and return all the data already initialized to the presentation/web tier that is required for a particular use case. Usually, the application calls Hibernate.initialize() for each collection that will be needed in the web tier (this call must occur before the session is closed) or retrieves the collection eagerly using a Hibernate query with a FETCH clause.

  • You may also attach a previously loaded object to a new Session with update() or lock() before accessing unitialized collections (or other proxies). Hibernate can not do this automatically, as it would introduce ad hoc transaction semantics!

You can use the filter() method of the Hibernate Session API to get the size of a collection without initializing it:

( (Integer) s.filter( collection, "select count(*)" ).get(0) ).intValue()

filter() or createFilter() are also used to efficiently retrieve subsets of a collection without needing to initialize the whole collection.

6.6. Sorted Collections

Hibernate supports collections implementing java.util.SortedMap and java.util.SortedSet. You must specify a comparator in the mapping file:

<set name="aliases" table="person_aliases" sort="natural">
<key column="person"/>
<element column="name" type="string"/>
</set>

<map name="holidays" sort="my.custom.HolidayComparator" lazy="true">
<key column="year_id"/>
<index column="hol_name" type="string"/>
<element column="hol_date" type="date"/>
</map>

Allowed values of the sort attribute are unsorted, natural and the name of a class implementing java.util.Comparator.

Sorted collections actually behave like java.util.TreeSet or java.util.TreeMap.

If you want the database itself to order the collection elements use the order-by attribute of set, bag or map mappings. This solution is only available under JDK 1.4 or higher (it is implemented using LinkedHashSet or LinkedHashMap). This performs the ordering in the SQL query, not in memory.

<set name="aliases" table="person_aliases" order-by="name asc">
<key column="person"/>
<element column="name" type="string"/>
</set>

<map name="holidays" order-by="hol_date, hol_name" lazy="true">
<key column="year_id"/>
<index column="hol_name" type="string"/>
<element column="hol_date type="date"/>
</map>

Note that the value of the order-by attribute is an SQL ordering, not a HQL ordering!

Associations may even be sorted by some arbitrary criteria at runtime using a filter().

sortedUsers = s.filter( group.getUsers(), "order by this.name" );

6.7. Using an <idbag>

If you've fully embraced our view that composite keys are a bad thing and that entities should have synthetic identifiers (surrogate keys), then you might find it a bit odd that the many to many associations and collections of values that we've shown so far all map to tables with composite keys! Now, this point is quite arguable; a pure association table doesn't seem to benefit much from a surrogate key (though a collection of composite values might). Nevertheless, Hibernate provides a (slightly experimental) feature that allows you to map many to many associations and collections of values to a table with a surrogate key.

The <idbag> element lets you map a List (or Collection) with bag semantics.

<idbag name="lovers" table="LOVERS" lazy="true">
<collection-id column="ID" type="long">
<generator class="hilo"/>
</collection-id>
<key column="PERSON1"/>
<many-to-many column="PERSON2" class="eg.Person" outer-join="true"/>
</idbag>

As you can see, an <idbag> has a synthetic id generator, just like an entity class! A different surrogate key is assigned to each collection row. Hibernate does not provide any mechanism to discover the surrogate key value of a particular row, however.

Note that the update performance of an <idbag> is much better than a regular <bag>! Hibernate can locate individual rows efficiently and update or delete them individually, just like a list, map or set.

In the current implementation, the identity identifier generation strategy is not supported for <idbag> collection identifiers.

6.8. Bidirectional Associations

A bidirectional association allows navigation from both "ends" of the association. Two kinds of bidirectional association are supported:

one-to-many

set or bag valued at one end, single-valued at the other

many-to-many

set or bag valued at both ends

Please note that Hibernate does not support bidirectional one-to-many associations with an indexed collection (list, map or array) as the "many" end, you have to use a set or bag mapping.

You may specify a bidirectional many-to-many association simply by mapping two many-to-many associations to the same database table and declaring one end as inverse (which one is your choice). Here's an example of a bidirectional many-to-many association from a class back to itself (each category can have many items and each item can be in many categories):

<class name="org.hibernate.auction.Category">
<id name="id" column="ID"/>
...
<bag name="items" table="CATEGORY_ITEM" lazy="true">
<key column="CATEGORY_ID"/>
<many-to-many class="org.hibernate.auction.Item" column="ITEM_ID"/>
</bag>
</class>

<class name="org.hibernate.auction.Item">
<id name="id" column="ID"/>
...

<!-- inverse end -->
<bag name="categories" table="CATEGORY_ITEM" inverse="true" lazy="true">
<key column="ITEM_ID"/>
<many-to-many class="org.hibernate.auction.Category" column="CATEGORY_ID"/>
</bag>
</class>

Changes made only to the inverse end of the association are not persisted. This means that Hibernate has two representations in memory for every bidirectional association, one link from A to B and another link from B to A. This is easier to understand if you think about the Java object model and how we create a many-to-many relationship in Java:

category.getItems().add(item);// The category now "knows" about the relationship
item.getCategories().add(category); // The item now "knows" about the relationship

session.update(item); // No effect, nothing will be saved!
session.update(category); // The relationship will be saved

The non-inverse side is used to save the in-memory representation to the database. We would get an unneccessary INSERT/UPDATE and probably even a foreign key violation if both would trigger changes! The same is of course also true for bidirectional one-to-many associations.

You may map a bidirectional one-to-many association by mapping a one-to-many association to the same table column(s) as a many-to-one association and declaring the many-valued end inverse="true".

<class name="eg.Parent">
<id name="id" column="id"/>
....
<set name="children" inverse="true" lazy="true">
<key column="parent_id"/>
<one-to-many class="eg.Child"/>
</set>
</class>

<class name="eg.Child">
<id name="id" column="id"/>
....
<many-to-one name="parent" class="eg.Parent" column="parent_id"/>
</class>

Mapping one end of an association with inverse="true" doesn't affect the operation of cascades, both are different concepts!

6.9. Ternary Associations

There are two possible approaches to mapping a ternary association. One approach is to use composite elements (discussed below). Another is to use a Map with an association as its index:

<map name="contracts" lazy="true">
<key column="employer_id"/>
<index-many-to-many column="employee_id" class="Employee"/>
<one-to-many class="Contract"/>
</map>
<map name="connections" lazy="true">
<key column="node1_id"/>
<index-many-to-many column="node2_id" class="Node"/>
<many-to-many column="connection_id" class="Connection"/>
</map>

6.10. Heterogeneous Associations

The <many-to-any> and <index-many-to-any> elements provide for true heterogeneous associations. These mapping elements work in the same way as the <any> element - and should also be used rarely, if ever.

6.11. Collection examples

The previous sections are pretty confusing. So lets look at an example. This class:

package eg;
import java.util.Set;

public class Parent {
private long id;
private Set children;

public long getId() { return id; }
private void setId(long id) { this.id=id; }

private Set getChildren() { return children; }
private void setChildren(Set children) { this.children=children; }

....
....
}

has a collection of eg.Child instances. If each child has at most one parent, the most natural mapping is a one-to-many association:

<hibernate-mapping>

<class name="eg.Parent">
<id name="id">
<generator class="sequence"/>
</id>
<set name="children" lazy="true">
<key column="parent_id"/>
<one-to-many class="eg.Child"/>
</set>
</class>

<class name="eg.Child">
<id name="id">
<generator class="sequence"/>
</id>
<property name="name"/>
</class>

</hibernate-mapping>

This maps to the following table definitions:

create table parent ( id bigint not null primary key )
create table child ( id bigint not null primary key, name varchar(255), parent_id bigint )
alter table child add constraint childfk0 (parent_id) references parent

If the parent is required, use a bidirectional one-to-many association:

<hibernate-mapping>

<class name="eg.Parent">
<id name="id">
<generator class="sequence"/>
</id>
<set name="children" inverse="true" lazy="true">
<key column="parent_id"/>
<one-to-many class="eg.Child"/>
</set>
</class>

<class name="eg.Child">
<id name="id">
<generator class="sequence"/>
</id>
<property name="name"/>
<many-to-one name="parent" class="eg.Parent" column="parent_id" not-null="true"/>
</class>

</hibernate-mapping>

Notice the NOT NULL constraint:

create table parent ( id bigint not null primary key )
create table child ( id bigint not null
 primary key,
 name varchar(255),
 parent_id bigint not null )
alter table child add constraint childfk0 (parent_id) references parent

On the other hand, if a child might have multiple parents, a many-to-many association is appropriate:

<hibernate-mapping>

<class name="eg.Parent">
<id name="id">
<generator class="sequence"/>
</id>
<set name="children" lazy="true" table="childset">
<key column="parent_id"/>
<many-to-many class="eg.Child" column="child_id"/>
</set>
</class>

<class name="eg.Child">
<id name="id">
<generator class="sequence"/>
</id>
<property name="name"/>
</class>

</hibernate-mapping>

Table definitions:

create table parent ( id bigint not null primary key )
create table child ( id bigint not null primary key, name varchar(255) )
create table childset ( parent_id bigint not null,
child_id bigint not null,
primary key ( parent_id, child_id ) )
alter table childset add constraint childsetfk0 (parent_id) references parent
alter table childset add constraint childsetfk1 (child_id) references child