7

Imagine 2 tables in a relational database, e.g. Person and Billing. There is a (non-mandatory) OneToOne association defined between these entities, and they share the Person primary key (i.e. PERSON_ID is defined in both Person and Billing, and it is a foreign key in the latter).

When doing a select on Person via a named query such as:

from Person p where p.id = :id

Hibernate/JPA generates two select queries, one on the Person table and another on the Billing table.

The example above is very simple and would not cause any performance issues, given the query returns only one result. Now, imagine that Person has n OneToOne relationships (all non-mandatory) with other entities (all sharing the Person primary key).

Correct me if I'm wrong, but running a select query on Person, returning r rows, would result in (n+1)*r selects being generated by Hibernate, even if the associations are lazy.

Is there a workaround for this potential performance disaster (other than not using a shared primary key at all)? Thank you for all your ideas.

octy
  • 6,445
  • 1
  • 26
  • 38
  • I think you're looking for "eager fetching", but I don't know hibernate. Hope that helps someone with a real answer. – Stefan Mai Jun 16 '09 at 16:27
  • With eager fetching, the data from those OneToOne associations would always be loaded (it's not what I want). What I'm looking for is a way to cap the number of native selects generated by Hibernate, independent of the number of rows returned. – octy Jun 16 '09 at 17:43
  • Eager fetching doesn't affect Hibernate queries. See http://stackoverflow.com/questions/463349 – Steve Kuo Jun 16 '09 at 18:39
  • This is a common problem.. giiglr for "hibernate n+1 selects" – skaffman Jun 16 '09 at 18:54
  • Thank you all for your responses/comments! I tried some of the suggestions posted here and also googled around a bit. My problem is not quite the same as the "Hibernate n+1 select"-issue. To put it simpler: if I execute a HQL query such as "from Person p where p.id = :id", I would expect Hibernate to only hit the Person table, even if it has an optional OneToOne association with the Billing table. Instead, it hits the Billing table too! I also found (https://www.hibernate.org/162.html) - a more detailed explanation. I guess I should stay away, as Pat suggested. Unless a workaround exists? – octy Jun 17 '09 at 14:03
  • One-to-One associations in Hibernate are, in my opinion, broken. Noone agrees with me, though. The usual workaround for his brokenness is to use a Many-To-One association with a unique=true restriction on the "many" end. Dumb, but it usually works. – skaffman Jun 17 '09 at 17:06

6 Answers6

8

Imagine 2 tables in a relational database, e.g. Person and Billing. There is a (non-mandatory) OneToOne association defined between these entities,

Lazy fetching is conceptually not possible for non-mandatory OneToOne by default, Hibernate has to hit the database to know if the association is null or not. More details from this old wiki page:

Some explanations on lazy loading (one-to-one)

[...]

Now consider our class B has one-to-one association to C

class B {
    private C cee;

    public C getCee() {
        return cee;
    }

    public void setCee(C cee) {
        this.cee = cee;
    }
}

class C {
    // Not important really
}

Right after loading B, you may call getCee() to obtain C. But look, getCee() is a method of YOUR class and Hibernate has no control over it. Hibernate does not know when someone is going to call getCee(). That means Hibernate must put an appropriate value into "cee" property at the moment it loads B from database. If proxy is enabled for C, Hibernate can put a C-proxy object which is not loaded yet, but will be loaded when someone uses it. This gives lazy loading for one-to-one.

But now imagine your B object may or may not have associated C (constrained="false"). What should getCee() return when specific B does not have C? Null. But remember, Hibernate must set correct value of "cee" at the moment it set B (because it does no know when someone will call getCee()). Proxy does not help here because proxy itself in already non-null object.

So the resume: if your B->C mapping is mandatory (constrained=true), Hibernate will use proxy for C resulting in lazy initialization. But if you allow B without C, Hibernate just HAS TO check presence of C at the moment it loads B. But a SELECT to check presence is just inefficient because the same SELECT may not just check presence, but load entire object. So lazy loading goes away.

So, not possible... by default.

Is there a workaround for this potential performance disaster (other than not using a shared primary key at all)? Thank you for all your ideas.

The problem is not the shared primary key, with or without shared primary key, you'll get it, the problem is the nullable OneToOne.

First option: use bytecode instrumentation (see references to the documentation below) and no-proxy fetching:

@OneToOne( fetch = FetchType.LAZY )
@org.hibernate.annotations.LazyToOne(org.hibernate.annotations.LazyToOneOption.NO_PROXY)

Second option: Use a fake ManyToOne(fetch=FetchType.LAZY). That's probably the most simple solution (and to my knowledge, the recommended one). But I didn't test this with a shared PK though.

Third option: Eager load the Billing using a join fetch.

Related question

References

Community
  • 1
  • 1
Pascal Thivent
  • 535,937
  • 127
  • 1,027
  • 1,106
  • Thanks for such a great answer! Unfortunately, I cannot verify all your suggestions directly, as I don't have access to that code anymore. From what I remember, we implemented option 3 you suggested (join fetch). – octy Aug 31 '10 at 15:49
1

This is a common performance issue with Hibernate (just search for "Hibernate n+1"). There are three options to avoiding n+1 queries:

  • Batch size
  • Subselect
  • Do a LEFT JOIN in your query

These are covered in the Hibernate FAQs here and here

Steve Kuo
  • 58,491
  • 75
  • 189
  • 247
1

Stay away from hibernate's OneToOne mapping

It is very broken and dangerous. You are one minor bug away from a database corruption problem.

http://opensource.atlassian.com/projects/hibernate/browse/HHH-2128

Pat
  • 5,601
  • 5
  • 31
  • 50
  • [HHH-2128](http://opensource.atlassian.com/projects/hibernate/browse/HHH-2128) is a user issue, not an Hibernate issue, the user is using an nonsensical mapping. Pure PEBKAC problem, not Hibernate related. – Pascal Thivent Aug 21 '10 at 15:22
  • really? so plan on telling that to the CEO, when 1-1 mapping results in a corrupted db? Tools are supposed to prevent PEBKAC issues. But guess you have never been coding at 3am trying to hit deadlines. – Pat Sep 03 '10 at 08:41
0

You could try "blind-guess optimization", which is good for "n+1 select problems". Annotate you field (or getter) like this:

@org.hibernate.annotations.BatchSize(size = 10)
java.util.Set<Billing> bills =  new HashSet<Billing>();
Schildmeijer
  • 19,921
  • 11
  • 59
  • 79
0

That "n+1" problem will only occur if you specify the relationship as as lazy or you explicitly indicate that you want hibernate to run a separate query.

Hibernate can fetch the relationship to Billing with an outer join on the select of Person, obviating the n+1 problem altogether. I think it is the fetch="XXX" indication in your hbm files.

Check out A Short Primer On Fetching Strategies

Michael Wiles
  • 19,590
  • 17
  • 65
  • 98
0

use optional =true with a one-to-one relationship like this to avoid the n+1 issue

@OneToOne(fetch = FetchType.LAZY, optional=true)
@PrimaryKeyJoinColumn
Shaam
  • 113
  • 10