Hibernate

Troubleshooting

p6sy

Allows seeing all statements sent to DB via JDBC. It helps

  • Know how many queries that are generated (don't base on Hibernate query log, it's not precise when JDBC batch update is enabled).
  • Know what are parameters
  • Know execution time of each query
  • many more...

Steps to use:

  • Add the jar file to the classpath
  • Replace the vendor's driver class with p6sy's one
  • Add p6spy's prefix to the vendor's JDBC URL
  • Add a spy.properties file to the classpath

There are many other configuration scenarios.

Note:

  • By default, p6spy won't log batch statements. Must un-exclude it in the configuration file.
  • By default, p6spy will log one line for each time the java.sql.Statement#addBatch is called (with query and parameters). If we want to know how many queries that are sent by the application, this is not what we want. To only show one query per batch, override methods onAfterAddBatch and onAfterAddBatch of com.p6spy.engine.event.SimpleJdbcEventListener.

This is not the only tool. There are others, such as datasource-proxy

FLush Oracle caches

Needed when reproducing performance issue

alter system flush buffer_cache;
alter system flush shared_pool;

## flush disk cache, unneeded in most of the case
##echo 3 > /proc/sys/vm/drop_caches

Debug queries

Place breakpoints in

  • org.hibernate.loader.Loader#executeQueryStatement
  • org.hibernate.engine.jdbc.internal.StatementPreparerImpl#prepareQueryStatement

Check whether JDBC batch update is really working

  • Hibernate 3: org.hibernate.jdbc.AbstractBatcher
  • After version 3: org.hibernate.engine.jdbc.batch.internal.BatchingBatch

Check where is a setting used

  • hibernate 3: org.hibernate.cfg.Settings
  • hibernate 5: org.hibernate.boot.spi.SessionFactoryOptions
  • Log all settings: <logger name="org.hibernate.cfg.Settings" level="DEBUG" />

Inside out

Inserts twice, why?

  • There are 2 CascadeType.PERSIST in 2 different paths (probably in @OneToMany) pointing to the same entity. Just remove such cascading from one side.
  • Hitting a bug which was fixed in 5.0.8: adding a new entity to a @OneToMany(cascade = {MERGE, PERSIST}) mapped as list and using Hibernate 3.3.1, 3.5.6, 3.6.0, 3.6.10, 4.2.6, 4.3.11, or 5.0.2 (may be others).

Clob/blob

When mapped to java.sql.Clob/Blob, they make the entity always-dirty get the getter is called!

Putting annotations on fields or getters?

Doesn't matter, but only use one, don't mix. Hibernate will guess the access type from the position of @Id or @EmbeddedId


About mappedBy & @JoinColumn

mappedBy: To put in a non-owning side of a bi-directional relationship.

@JoinColumn: to put in the owning side, the side owning the foreign key.


About persistent bag

bag is one of Hibernate's implementation for mapping collections. When defining mapping for collections, java.util.List is used for both bag and list. From the mapping perspective, the only way to know whether we are using a bag or a list is looking at the existence of @OrderColumn (or the deprecated @IndexColumn).

@OneToMany(cascade = ALL)
@JoinTable(name = "post_comment",
    joinColumns = @JoinColumn(name = "post_id"),
    inverseJoinColumns = @JoinColumn(name = "comment_id")
)
private List<Comment> tags = new ArrayList<>(); // Bag will be used
@OrderColumn(name="comment_index")
@OneToMany(cascade = ALL)
@JoinTable(name = "post_comment",
    joinColumns = @JoinColumn(name = "post_id"),
    inverseJoinColumns = @JoinColumn(name = "comment_id")
)
private List<Comment> tags = new ArrayList<>(); // List will be used

What the heck is the bag? unordered collection, can contain duplicated elements!

  • Unordered collection: we save a collection of [A, B, C] but we are not guaranteed it will be in that order when retrieving. That's fine, we often don't care.
  • Can contain duplicated elements. Wait, what? like above, every Comment has one technical ID, how come it can be duplicated? Let's model the table comment differently: only has columns post_id and comment_text. Now, the mapping can be done like following
@ElementCollection
@CollectionTable(name="comment", joinColumns=@JoinColumn(name="post_id"))
@Column(name="comment_text")
private List<String> tags = new ArrayList<>(); // bag will be used

Now, elements can be duplicated!

MultipleBagFetchException

Happens when simultaneously fetch more than one bag (either via JOIN FETCH or FetchType.EAGER).

But why?

Let's assume the following simple query

select PARENT.parent_id,
       CHILDREN_1.children_1_id,
       CHILDREN_1.children_2_id
from PARENT
     left outer join CHILDREN_1 on PARENT.parent_id = CHILDREN_1.parent_id
     left outer join CHILDREN_2 on PARENT.parent_id = CHILDREN_2.parent_id

PARENT has one row; CHILDREN_1 has 2 rows; CHILDREN_2 has 1 row. The query result will look like:

parent_id children_1_id children_1_id
p 1 children_1 A children_2 X
p 1 children_1 B children_2 X

What do we expect?

  • One PARENT entity
  • with a collection of CHILDREN_1 having 2 items
  • and a collection of CHILDREN_2 with 1 item

Recall that a bag can contain duplicate items. So in this case, the collection of CHILDREN_2 will contain 2 identical items! That is not what the expectation of the query


About @ManyToMany

DON'T use CascadeType.REMOVE because it will not only apply to the link table but also the other side of the association!

DON'T use PersistentBag because it leads to "delete-then-insert" behavior.

DO use PersistentList or PersistentSet instead.

@...
public class Post {
    ...

    @ManyToMany(cascade = { PERSIST, MERGE })
    @JoinTable(name = "post_tag",
               joinColumns = @JoinColumn(name = "post_id"),
               inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    // DON'T: private List<Tag> tags = new ArrayList<>();
    // DO   :
    private Set<Tag> tags = new HashSet<>();

    ...
}

@...
public class Tag {
    ...

    @ManyToMany(mappedBy = "tags")
    // DON'T: private List<Post> posts = new ArrayList<>();
    // DO   :
    private Set<Post> posts = new HashSet<>();
}

About @ManyToOne

Can be cascading too!


About @OneToOne

Unique foreign key

This is not efficient in term of DB design: The parent_id should be used as PK of the Child

If mapping bidirectional:

@Table...
@Entity...
public class Parent {
    @OneToOne(mappedBy = "parent", cascade = CascadeType.ALL,
              fetch = FetchType.LAZY, optional = false)
    private Child child;
}

@Table
@Entity
public class Child {
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

When loading a Parent, the child will always be fetched. Reason: for a managed entity, it is required both type (class) and the ID. From the Parent entity, the only way to know the Child ID is fetching it!

Shared primary key

@MapsId - effective unidirectional

@Table
@Entity
public class Child {

    // child's ID must be also the the FK pointing to its Parent
    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Parent parent;
}

Can't use shared primary key and still want bidirectional mappings with lazy loading on the nonowning side

Using @OneToMany in the non-owning side

@Table...
@Entity...
public class Parent {
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL,
              fetch = FetchType.LAZY, orphanRemoval = true)
    private Set<Child> children;

    // Don't publish getter/setter for children property

    public Child getChild() {
        return children.isEmpty() ? null : children.iterator().next();
    }

    public void setChild(Child child) {
        children.clear();
        children.add(child);
        child.setParent(this);
    }
}

Batch update / insert

Enable it:

hibernate.jdbc.batch_size=100
hibernate.jdbc.batch_versioned_data=true
hibernate.order_updates=true
hibernate.order_inserts=true

batch_versioned_data: since 5.0, it's true by default. If it is set to false and the entities have the optimistic locking column, they won't be batched. Reason: some databases return wrong update count in case of error thus it's impossible to detect concurrent-update error for items within the batch (this is the case of Oracle until 11g. It was fixed since 12c)

Since 5.2, it's possible to override batch size per session

entityManager
    .unwrap(Session.class)
    .setJdbcBatchSize(10); // in this session, the batch size will be 10

Batch fetch size

Purpose: loading several uninitialized proxies if one proxy is accessed

How: by select * from ... where id in (?, ?, ...). See BatchingEntityLoader, BatchingCollectionInitializer

Configure:

  • Globally: hibernate.default_batch_fetch_size
  • @BatchSize on individual entity or collection

Behavior:

  • Prior to Hibernate 4.2.0: a set of queries are pre-generated depending on the configured batch size and use the next-smaller size. See Hibernate's ArrayHelper#getBatchSizes. Be aware that, extra heap memory is needed!

  • Since 4.2.0: hibernate.batch_fetch_style can be one of:
    • LEGACY (above)
    • PADDED, like legacy, but use next-bigger size, extra ? will be filled with the same ID (one of IDs in the batch)
    • DYNAMIC: the in (...) will be generated based on the actually number of IDs.

Connection release mode

hibernate.connection.release_mode (org.hibernate.cfg.AvailableSettings#RELEASE_CONNECTIONS),

  • In non-JTA environment, is AFTER_TRANSACTION.
  • In JTA, it will be default to AFTER_STATEMENT to circumvent the issue in some EE environments in which an EJB calling another will result a connection leak. If don't use EE, it's best to set it to AFTER_TRANSACTION for better performance.

Supports both XML mappings and Java annotation mappings

public class ..... implements BeanFactoryAware

    private ConfigurableListableBeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

....

    LocalContainerEntityManagerFactoryBean factoryBean = ...

    // JPA way of scanning entities.
    // Looks for packages defined in annotation @EntityScan
    String[] packageToScan = EntityScanPackages.get(this.beanFactory)
            .map(bean -> bean.getPackageNames().toArray(new String[0]))
            .orElse(new String[0]);
    factoryBean.setPackagesToScan(packageToScan);

    // Looks for Hibernate mapping files
    // This is equivalent to the pattern: classpath*:resources/hbm/*.hbm.xml
    ClasspathScanningPersistenceUnitPostProcessor postProcessor =
            new ClasspathScanningPersistenceUnitPostProcessor("resources.hbm");
    postProcessor.setMappingFileNamePattern("*.hbm.xml");
    factoryBean.setPersistenceUnitPostProcessors(postProcessor);

hashCode, equals

@MappedSuperclass
public abstract class AbstractEntity {

    @Transient
    private boolean hasHashCodeGeneratedWhileTransient = false;

    ....

    public boolean isPersisted() {
        return getId() != null;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if ((obj == null) || (getClass() != obj.getClass())) {
            return false;
        }

        if (obj instanceof AbstractEntity) {
            final AbstractEntity other = (AbstractEntity) obj;
            if (isPersisted() && other.isPersisted()) {
                return getId().equals(other.getId());
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        if (!isPersisted()) {
            hasHashCodeGeneratedWhileTransient = true;
            return super.hashCode();
        }

        if (hasHashCodeGeneratedWhileTransient) {
            return super.hashCode();
        }
        return getId().hashCode();
    }
}

Learning resources