Entity types

Usage of the word entity

The entity type describes the mapping between the actual persistable domain model object and a database table row. To avoid any confusion with the annotation that marks a given entity type, the annotation will be further referred as @Entity.

Throughout this chapter and thereafter, entity types will be simply referred as entity.

POJO Models

Section 2.1 The Entity Class of the JPA 2.1 specification defines its requirements for an entity class. Applications that wish to remain portable across JPA providers should adhere to these requirements.

  • The entity class must be annotated with the javax.persistence.Entity annotation (or be denoted as such in XML mapping)

  • The entity class must have a public or protected no-argument constructor. It may define additional constructors as well.

  • The entity class must be a top-level class.

  • An enum or interface may not be designated as an entity.

  • The entity class must not be final. No methods or persistent instance variables of the entity class may be final.

  • If an entity instance is to be used remotely as a detached object, the entity class must implement the Serializable interface.

  • Both abstract and concrete classes can be entities. Entities may extend non-entity classes as well as entity classes, and non-entity classes may extend entity classes.

  • The persistent state of an entity is represented by instance variables, which may correspond to JavaBean-style properties. An instance variable must be directly accessed only from within the methods of the entity by the entity instance itself. The state of the entity is available to clients only through the entity’s accessor methods (getter/setter methods) or other business methods.

Hibernate, however, is not as strict in its requirements. The differences from the list above include:

  • The entity class must have a no-argument constructor, which may be public, protected or package visibility. It may define additional constructors as well.

  • The entity class need not be a top-level class.

  • Technically Hibernate can persist final classes or classes with final persistent state accessor (getter/setter) methods. However, it is generally not a good idea as doing so will stop Hibernate from being able to generate proxies for lazy-loading the entity.

  • Hibernate does not restrict the application developer from exposing instance variables and reference them from outside the entity class itself. The validity of such a paradigm, however, is debatable at best.

Let’s look at each requirement in detail.

Prefer non-final classes

A central feature of Hibernate is the ability to load lazily certain entity instance variables (attributes) via runtime proxies. This feature depends upon the entity class being non-final or else implementing an interface that declares all the attribute getters/setters. You can still persist final classes that do not implement such an interface with Hibernate, but you will not be able to use proxies for fetching lazy associations, therefore limiting your options for performance tuning. For the very same reason, you should also avoid declaring persistent attribute getters and setters as final.

Starting in 5.0 Hibernate offers a more robust version of bytecode enhancement as another means for handling lazy loading. Hibernate had some bytecode re-writing capabilities prior to 5.0 but they were very rudimentary. See the BytecodeEnhancement for additional information on fetching and on bytecode enhancement.

Implement a no-argument constructor

The entity class should have a no-argument constructor. Both Hibernate and JPA require this.

JPA requires that this constructor be defined as public or protected. Hibernate, for the most part, does not care about the constructor visibility, as long as the system SecurityManager allows overriding the visibility setting. That said, the constructor should be defined with at least package visibility if you wish to leverage runtime proxy generation.

Declare getters and setters for persistent attributes

The JPA specification requires this, otherwise the model would prevent accessing the entity persistent state fields directly from outside the entity itself.

Although Hibernate does not require it, it is recommended to follow the JavaBean conventions and define getters and setters for entity persistent attributes. Nevertheless, you can still tell Hibernate to directly access the entity fields.

Attributes (whether fields or getters/setters) need not be declared public. Hibernate can deal with attributes declared with public, protected, package or private visibility. Again, if wanting to use runtime proxy generation for lazy loading, the getter/setter should grant access to at least package visibility.

Provide identifier attribute(s)

Historically this was considered optional. However, not defining identifier attribute(s) on the entity should be considered a deprecated feature that will be removed in an upcoming release.

The identifier attribute does not necessarily need to be mapped to the column(s) that physically define the primary key. However, it should map to column(s) that can uniquely identify each row.

We recommend that you declare consistently-named identifier attributes on persistent classes and that you use a nullable (i.e., non-primitive) type.

The placement of the @Id annotation marks the persistence state access strategy.

Example 1. Identifier mapping
@Id
private Long id;

Hibernate offers multiple identifier generation strategies, see the Identifier Generators chapter for more about this topic.

Mapping the entity

The main piece in mapping the entity is the javax.persistence.Entity annotation. The @Entity annotation defines just one attribute name which is used to give a specific entity name for use in JPQL queries. By default, the entity name represents the unqualified name of the entity class itself.

Example 2. Simple @Entity mapping
@Entity(name = "Book")
public static class Book {

    @Id
    private Long id;

    private String title;

    private String author;

    //Getters and setters are omitted for brevity
}

An entity models a database table. The identifier uniquely identifies each row in that table. By default, the name of the table is assumed to be the same as the name of the entity. To explicitly give the name of the table or to specify other information about the table, we would use the javax.persistence.Table annotation.

Example 3. Simple @Entity with @Table
@Entity(name = "Book")
@Table(
    catalog = "public",
    schema = "store",
    name = "book"
)
public static class Book {

    @Id
    private Long id;

    private String title;

    private String author;

    //Getters and setters are omitted for brevity
}

Implementing equals() and hashCode()

Much of the discussion in this section deals with the relation of an entity to a Hibernate Session, whether the entity is managed, transient or detached. If you are unfamiliar with these topics, they are explained in the Persistence Context chapter.

Whether to implement equals() and hashCode() methods in your domain model, let alone how to implement them, is a surprisingly tricky discussion when it comes to ORM.

There is really just one absolute case: a class that acts as an identifier must implement equals/hashCode based on the id value(s). Generally, this is pertinent for user-defined classes used as composite identifiers. Beyond this one very specific use case and few others we will discuss below, you may want to consider not implementing equals/hashCode altogether.

So what’s all the fuss? Normally, most Java objects provide a built-in equals() and hashCode() based on the object’s identity, so each new object will be different from all others. This is generally what you want in ordinary Java programming. Conceptually however this starts to break down when you start to think about the possibility of multiple instances of a class representing the same data.

This is, in fact, exactly the case when dealing with data coming from a database. Every time we load a specific Person from the database we would naturally get a unique instance. Hibernate, however, works hard to make sure that does not happen within a given Session. In fact, Hibernate guarantees equivalence of persistent identity (database row) and Java identity inside a particular session scope. So if we ask a Hibernate Session to load that specific Person multiple times we will actually get back the same instance:

Example 4. Scope of identity
Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );

assertTrue( book1 == book2 );

Consider we have a Library parent entity which contains a java.util.Set of Book entities:

Library entity mapping

@Entity(name = "Library")
public static class Library {

    @Id
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "book_id")
    private Set<Book> books = new HashSet<>();

    //Getters and setters are omitted for brevity
}
Example 5. Set usage with Session-scoped identity
Library library = entityManager.find( Library.class, 1L );

Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );

library.getBooks().add( book1 );
library.getBooks().add( book2 );

assertEquals( 1, library.getBooks().size() );

However, the semantic changes when we mix instances loaded from different Sessions:

Example 6. Mixed Sessions
Book book1 = doInJPA( this::entityManagerFactory, entityManager -> {
    return entityManager.find( Book.class, 1L );
} );

Book book2 = doInJPA( this::entityManagerFactory, entityManager -> {
    return entityManager.find( Book.class, 1L );
} );

assertFalse( book1 == book2 );
doInJPA( this::entityManagerFactory, entityManager -> {
    Set<Book> books = new HashSet<>();

    books.add( book1 );
    books.add( book2 );

    assertEquals( 2, books.size() );
} );

Specifically the outcome in this last example will depend on whether the Book class implemented equals/hashCode, and, if so, how.

If the Book class did not override the default equals/hashCode, then the two Book object reference are not going to be equal since their references are different.

Consider yet another case:

Example 7. Sets with transient entities
Library library = entityManager.find( Library.class, 1L );

Book book1 = new Book();
book1.setId( 100L );
book1.setTitle( "High-Performance Java Persistence" );

Book book2 = new Book();
book2.setId( 101L );
book2.setTitle( "Java Persistence with Hibernate" );

library.getBooks().add( book1 );
library.getBooks().add( book2 );

assertEquals( 2, library.getBooks().size() );

In cases where you will be dealing with entities outside of a Session (whether they be transient or detached), especially in cases where you will be using them in Java collections, you should consider implementing equals/hashCode.

A common initial approach is to use the entity’s identifier attribute as the basis for equals/hashCode calculations:

Example 8. Naive equals/hashCode implementation
@Entity(name = "Library")
public static class Library {

    @Id
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "book_id")
    private Set<Book> books = new HashSet<>();

    //Getters and setters are omitted for brevity
}

@Entity(name = "Book")
public static class Book {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String author;

    //Getters and setters are omitted for brevity

    @Override
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }
        Book book = (Book) o;
        return Objects.equals( id, book.id );
    }

    @Override
    public int hashCode() {
        return Objects.hash( id );
    }
}

It turns out that this still breaks when adding transient instance of Book to a set as we saw in the last example:

Example 9. Auto-generated identifiers with Sets and naive equals/hashCode
Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );

Book book2 = new Book();
book2.setTitle( "Java Persistence with Hibernate" );

Library library = doInJPA( this::entityManagerFactory, entityManager -> {
    Library _library = entityManager.find( Library.class, 1L );

    _library.getBooks().add( book1 );
    _library.getBooks().add( book2 );

    return _library;
} );

assertFalse( library.getBooks().contains( book1 ) );
assertFalse( library.getBooks().contains( book2 ) );

The issue here is a conflict between the use of generated identifier, the contract of Set and the equals/hashCode implementations. Set says that the equals/hashCode value for an object should not change while the object is part of the Set. But that is exactly what happened here because the equals/hasCode are based on the (generated) id, which was not set until the JPA transaction is committed.

Note that this is just a concern when using generated identifiers. If you are using assigned identifiers this will not be a problem, assuming the identifier value is assigned prior to adding to the Set.

Another option is to force the identifier to be generated and set prior to adding to the Set:

Example 10. Forcing the flush before adding to the Set
Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );

Book book2 = new Book();
book2.setTitle( "Java Persistence with Hibernate" );

Library library = doInJPA( this::entityManagerFactory, entityManager -> {
    Library _library = entityManager.find( Library.class, 1L );

    entityManager.persist( book1 );
    entityManager.persist( book2 );
    entityManager.flush();

    _library.getBooks().add( book1 );
    _library.getBooks().add( book2 );

    return _library;
} );

assertTrue( library.getBooks().contains( book1 ) );
assertTrue( library.getBooks().contains( book2 ) );

But this is often not feasible.

The final approach is to use a "better" equals/hashCode implementation, making use of a natural-id or business-key.

Example 11. Natural Id equals/hashCode
@Entity(name = "Library")
public static class Library {

    @Id
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "book_id")
    private Set<Book> books = new HashSet<>();

    //Getters and setters are omitted for brevity
}

@Entity(name = "Book")
public static class Book {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String author;

    @NaturalId
    private String isbn;

    //Getters and setters are omitted for brevity

    @Override
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }
        Book book = (Book) o;
        return Objects.equals( isbn, book.isbn );
    }

    @Override
    public int hashCode() {
        return Objects.hash( isbn );
    }
}

This time, when adding a Book to the Library Set, you can retrieve the Book even after it’s being persisted:

Example 12. Natural Id equals/hashCode persist example
Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );
book1.setIsbn( "978-9730228236" );

Library library = doInJPA( this::entityManagerFactory, entityManager -> {
    Library _library = entityManager.find( Library.class, 1L );

    _library.getBooks().add( book1 );

    return _library;
} );

assertTrue( library.getBooks().contains( book1 ) );

As you can see the question of equals/hashCode is not trivial, nor is there a one-size-fits-all solution.

Although using a natural-id is best for equals and hashCode, sometimes you only have the entity identifier that provides a unique constraint.

It’s possible to use the entity identifier for equality check, but it needs a workaround:

  • you need to provide a constant value for hashCode so that the hash code value does not change before and after the entity is flushed.

  • you need to compare the entity identifier equality only for non-transient entities.

For details on mapping the identifier, see the Identifiers chapter.

Mapping the entity to a SQL query

You can map an entity to a SQL query using the @Subselect annotation.

Example 13. @Subselect entity mapping
@Entity(name = "Client")
@Table(name = "client")
public static class Client {

    @Id
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    //Getters and setters omitted for brevity

}

@Entity(name = "Account")
@Table(name = "account")
public static class Account {

    @Id
    private Long id;

    @ManyToOne
    private Client client;

    private String description;

    //Getters and setters omitted for brevity

}

@Entity(name = "AccountTransaction")
@Table(name = "account_transaction")
public static class AccountTransaction {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private Account account;

    private Integer cents;

    private String description;

    //Getters and setters omitted for brevity

}

@Entity(name = "AccountSummary")
@Subselect(
    "select " +
    "    a.id as id, " +
    "    concat(concat(c.first_name, ' '), c.last_name) as clientName, " +
    "    sum(at.cents) as balance " +
    "from account a " +
    "join client c on c.id = a.client_id " +
    "join account_transaction at on a.id = at.account_id " +
    "group by a.id, concat(concat(c.first_name, ' '), c.last_name)"
)
@Synchronize( {"client", "account", "account_transaction"} )
public static class AccountSummary {

    @Id
    private Long id;

    private String clientName;

    private int balance;

    //Getters and setters omitted for brevity

}

In the example above, the Account entity does not retain any balance since every account operation is registered as an AccountTransaction. To find the Account balance, we need to query the AccountSummary which shares the same identifier with the Account entity.

However, the AccountSummary is not mapped to a physical table, but to an SQL query.

So, if we have the following AccountTransaction record, the AccountSummary balance will mach the proper amount of money in this Account.

Example 14. Finding a @Subselect entity
doInJPA( this::entityManagerFactory, entityManager -> {
    Client client = new Client();
    client.setId( 1L );
    client.setFirstName( "John" );
    client.setLastName( "Doe" );
    entityManager.persist( client );

    Account account = new Account();
    account.setId( 1L );
    account.setClient( client );
    account.setDescription( "Checking account" );
    entityManager.persist( account );

    AccountTransaction transaction = new AccountTransaction();
    transaction.setAccount( account );
    transaction.setDescription( "Salary" );
    transaction.setCents( 100 * 7000 );
    entityManager.persist( transaction );

    AccountSummary summary = entityManager.createQuery(
        "select s " +
        "from AccountSummary s " +
        "where s.id = :id", AccountSummary.class)
    .setParameter( "id", account.getId() )
    .getSingleResult();

    assertEquals( "John Doe", summary.getClientName() );
    assertEquals( 100 * 7000, summary.getBalance() );
} );

If we add a new AccountTransaction entity and refresh the AccountSummary entity, the balance is updated accordingly:

Example 15. Refreshing a @Subselect entity
doInJPA( this::entityManagerFactory, entityManager -> {
    AccountSummary summary = entityManager.find( AccountSummary.class, 1L );
    assertEquals( "John Doe", summary.getClientName() );
    assertEquals( 100 * 7000, summary.getBalance() );

    AccountTransaction transaction = new AccountTransaction();
    transaction.setAccount( entityManager.getReference( Account.class, 1L ) );
    transaction.setDescription( "Shopping" );
    transaction.setCents( -100 * 2200 );
    entityManager.persist( transaction );
    entityManager.flush();

    entityManager.refresh( summary );
    assertEquals( 100 * 4800, summary.getBalance() );
} );

The goal of the @Synchronize annotation in the AccountSummary entity mapping is to instruct Hibernate which database tables are needed by the underlying @Subselect SQL query. This is because, unlike JPQL and HQL queries, Hibernate cannot parse the underlying native SQL query.

With the @Synchronize annotation in place, when executing a HQL or JPQL which selects from the AccountSummary entity, Hibernate will trigger a Persistence Context flush if there are pending Account, Client or AccountTransaction entity state transitions.

Define a custom entity proxy

By default, when it needs to use a proxy instead of the actual Pojo, Hibernate is going to use a Bytecode manipulation library like Javassist or Byte Buddy.

However, if the entity class is final, Javassist will not create a proxy and you will get a Pojo even when you only need a proxy reference. In this case, you could proxy an interface that this particular entity implements, as illustrated by the following example.

Example 16. Final entity class implementing the Identifiable interface
public interface Identifiable {

    Long getId();

    void setId(Long id);
}

@Entity( name = "Book" )
@Proxy(proxyClass = Identifiable.class)
public static final class Book implements Identifiable {

    @Id
    private Long id;

    private String title;

    private String author;

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public void setId(Long id) {
        this.id = id;
    }

    //Other getters and setters omitted for brevity
}

The @Proxy annotation is used to specify a custom proxy implementation for the current annotated entity.

When loading the Book entity proxy, Hibernate is going to proxy the Identifiable interface instead as illustrated by the following example:

Example 17. Proxying the final entity class implementing the Identifiable interface
doInHibernate( this::sessionFactory, session -> {
    Book book = new Book();
    book.setId( 1L );
    book.setTitle( "High-Performance Java Persistence" );
    book.setAuthor( "Vlad Mihalcea" );

    session.persist( book );
} );

doInHibernate( this::sessionFactory, session -> {
    Identifiable book = session.getReference( Book.class, 1L );

    assertTrue(
        "Loaded entity is not an instance of the proxy interface",
        book instanceof Identifiable
    );
    assertFalse(
        "Proxy class was not created",
        book instanceof Book
    );
} );
insert
into
    Book
    (author, title, id)
values
    (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Vlad Mihalcea]
-- binding parameter [2] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [3] as [BIGINT]  - [1]

As you can see in the associated SQL snippet, Hibernate issues no SQL SELECT query since the proxy can be constructed without needing to fetch the actual entity Pojo.

Dynamic entity proxies using the @Tuplizer annotation

It is possible to map your entities as dynamic proxies using the @Tuplizer annotation.

In the following entity mapping, both the embeddable and the entity are mapped as interfaces, not Pojos.

Example 18. Dynamic entity proxy mapping
@Entity
@Tuplizer(impl = DynamicEntityTuplizer.class)
public interface Cuisine {

    @Id
    @GeneratedValue
    Long getId();
    void setId(Long id);

    String getName();
    void setName(String name);

    @Tuplizer(impl = DynamicEmbeddableTuplizer.class)
    Country getCountry();
    void setCountry(Country country);
}
@Embeddable
public interface Country {

    @Column(name = "CountryName")
    String getName();

    void setName(String name);
}

The @Tuplizer instructs Hibernate to use the DynamicEntityTuplizer and DynamicEmbeddableTuplizer to handle the associated entity and embeddable object types.

Both the Cuisine entity and the Country embeddable types are going to be instantiated as Java dynamic proxies, as you can see in the following DynamicInstantiator example:

Example 19. Instantiating entities and embeddables as dynamic proxies
public class DynamicEntityTuplizer extends PojoEntityTuplizer {

    public DynamicEntityTuplizer(
            EntityMetamodel entityMetamodel,
            PersistentClass mappedEntity) {
        super( entityMetamodel, mappedEntity );
    }

    @Override
    protected Instantiator buildInstantiator(
            EntityMetamodel entityMetamodel,
            PersistentClass persistentClass) {
        return new DynamicInstantiator(
            persistentClass.getClassName()
        );
    }

    @Override
    protected ProxyFactory buildProxyFactory(
            PersistentClass persistentClass,
            Getter idGetter,
            Setter idSetter) {
        return super.buildProxyFactory(
            persistentClass, idGetter,
            idSetter
        );
    }
}
public class DynamicEmbeddableTuplizer
        extends PojoComponentTuplizer {

    public DynamicEmbeddableTuplizer(Component embeddable) {
        super( embeddable );
    }

    protected Instantiator buildInstantiator(Component embeddable) {
        return new DynamicInstantiator(
            embeddable.getComponentClassName()
        );
    }
}
public class DynamicInstantiator
        implements Instantiator {

    private final Class targetClass;

    public DynamicInstantiator(String targetClassName) {
        try {
            this.targetClass = Class.forName( targetClassName );
        }
        catch (ClassNotFoundException e) {
            throw new HibernateException( e );
        }
    }

    public Object instantiate(Serializable id) {
        return ProxyHelper.newProxy( targetClass, id );
    }

    public Object instantiate() {
        return instantiate( null );
    }

    public boolean isInstance(Object object) {
        try {
            return targetClass.isInstance( object );
        }
        catch( Throwable t ) {
            throw new HibernateException(
                "could not get handle to entity as interface : " + t
            );
        }
    }
}
public class ProxyHelper {

    public static <T> T newProxy(Class<T> targetClass, Serializable id) {
        return ( T ) Proxy.newProxyInstance(
            targetClass.getClassLoader(),
            new Class[] {
                targetClass
            },
            new DataProxyHandler(
                targetClass.getName(),
                id
            )
        );
    }

    public static String extractEntityName(Object object) {
        if ( Proxy.isProxyClass( object.getClass() ) ) {
            InvocationHandler handler = Proxy.getInvocationHandler(
                object
            );
            if ( DataProxyHandler.class.isAssignableFrom( handler.getClass() ) ) {
                DataProxyHandler myHandler = (DataProxyHandler) handler;
                return myHandler.getEntityName();
            }
        }
        return null;
    }
}
public final class DataProxyHandler implements InvocationHandler {

    private String entityName;

    private Map<String, Object> data = new HashMap<>();

    public DataProxyHandler(String entityName, Serializable id) {
        this.entityName = entityName;
        data.put( "Id", id );
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if ( methodName.startsWith( "set" ) ) {
            String propertyName = methodName.substring( 3 );
            data.put( propertyName, args[0] );
        }
        else if ( methodName.startsWith( "get" ) ) {
            String propertyName = methodName.substring( 3 );
            return data.get( propertyName );
        }
        else if ( "toString".equals( methodName ) ) {
            return entityName + "#" + data.get( "Id" );
        }
        else if ( "hashCode".equals( methodName ) ) {
            return this.hashCode();
        }
        return null;
    }

    public String getEntityName() {
        return entityName;
    }
}

With the DynamicInstantiator in place, we can work with the dynamic proxy entities just like with Pojo entities.

Example 20. Persisting entities and embeddables as dynamic proxies
Cuisine _cuisine = doInHibernateSessionBuilder(
        () -> sessionFactory()
                .withOptions()
                .interceptor( new EntityNameInterceptor() ),
        session -> {
    Cuisine cuisine = ProxyHelper.newProxy( Cuisine.class, null );
    cuisine.setName( "Française" );

    Country country = ProxyHelper.newProxy( Country.class, null );
    country.setName( "France" );

    cuisine.setCountry( country );
    session.persist( cuisine );

    return cuisine;
} );

doInHibernateSessionBuilder(
        () -> sessionFactory()
                .withOptions()
                .interceptor( new EntityNameInterceptor() ),
        session -> {
    Cuisine cuisine = session.get( Cuisine.class, _cuisine.getId() );

    assertEquals( "Française", cuisine.getName() );
    assertEquals( "France", cuisine.getCountry().getName() );
} );

Define a custom entity persister

The @Persister annotation is used to specify a custom entity or collection persister.

For entities, the custom persister must implement the EntityPersister interface.

For collections, the custom persister must implement the CollectionPersister interface.

Example 21. Entity persister mapping
@Entity
@Persister( impl = EntityPersister.class )
public class Author {

    @Id
    public Integer id;

    @OneToMany( mappedBy = "author" )
    @Persister( impl = CollectionPersister.class )
    public Set<Book> books = new HashSet<>();

    //Getters and setters omitted for brevity
    public void addBook(Book book) {
        this.books.add( book );
        book.setAuthor( this );
    }
}
@Entity
@Persister( impl = EntityPersister.class )
public class Book {

    @Id
    public Integer id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    public Author author;

    //Getters and setters omitted for brevity
}

By providing your own EntityPersister and CollectionPersister implementations, you can control how entities and collections are persisted in to the database.