0

I have tables:

users (id, name, email, password)
user_statuses (user_id, is_premium, is_advanced, user_rank_id)
user_ranks (id, name, ordinal)

So the relation between User and UserStatus is 1-1, and I have following entity clasess:

@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String name;
    private String email;
    private String password;

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private UserStatus status;
}

@Entity
@Table(name = "user_statuses")
@Getter
@Setter
@NoArgsConstructor
public class UserStatus {
    @Id
    private long id;

    @MapsId
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    private boolean isPremium;
    private boolean isAdvanced;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_rank_id")
    private UserRank rank;
}

@Entity
@Table(name = "user_ranks")
@Getter
@Setter
@NoArgsConstructor
public class UserRank {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String name;
    private int ordinal;
}

Then i created endpoint "/users/{id}" which should return user's email address as a string:

@GetMapping("/users/{id}")
public String getUserEmail(@PathVariable("id") long userId) {
    User user = service.getUser(userId);
    return user.getEmail();
}

When I call above endpoint I get user's email address as a response, however looking at the console log I see that hibernate executed 2 queries but noone asked him to do so:

First one for fetching the user:

SELECT
    user0_.id AS id1_2_0_,
    user0_.email AS email2_2_0_,
    user0_.name AS name3_2_0_,
    user0_.password AS password4_2_0_
FROM
    users user0_
WHERE
    user0_.id = 1;

And second one for fetching User Status that is associated with this User object:

SELECT
    userstatus0_.user_id AS user_id1_1_0_,
    userstatus0_.is_advanced AS is_advan2_1_0_,
    userstatus0_.is_premium AS is_premi3_1_0_,
    userstatus0_.user_rank_id AS user_ran4_1_0_
FROM
    user_statuses userstatus0_
WHERE
    userstatus0_.user_id = 1;

So I am confused: Why is hibernate running second query when I set fetch = FetchType.LAZY on each relation... It looks like that LAZY is ignored for @OneToOne annotation?

I do not use EntityGraph.

How to stop hibernate for running second query?

EDIT

So, it turns out Hibernate ignores my Lazy hint because it needs to decide should it initialize property with NULL or ProxyObject which makes sense. This link explains it well:

https://thorben-janssen.com/hibernate-tip-lazy-loading-one-to-one/

However this link also suggests that the best way to model this is Unidirectional One to One and it says that I can always fetch UserStatus based on User's ID (because both tables "shares" primary key)

However this confuses me a little bit, because I can fetch both rows using single query (SELECT * FROM users LEFT JOIN user_statuses ON users.id = user_statuses.user_id), but with approach described in the link I need 2 queries, and as far as I know (which I might be wrong) is 1 query is better than executing 2 queries, also if I want to fetch 25 users and their User Statuses, then I would also need 2 queries, one for fetching users and then fetching corespoinding user statuses and finally write nested for each loops to join these objects. I could have just executed one single query to fetch everything...

clzola
  • 1,925
  • 3
  • 30
  • 49
  • Lazy seems to be working on the UserStatus relationships or additional queries would be seen. Try running your test and removing/disabling or otherwise making the user_statuses table query throw an exception, as the stack might indicate where/why the query is occurring. – Chris May 26 '20 at 14:59
  • @Chris if I load UserStatus entity then I do not see query for User entity, only if I try to load User entity it fetches UserStatus even if it is all lazy – clzola May 26 '20 at 15:44
  • I dont know why hibernate is executing two select queries instead of one with join, but you might wan't to know that it is possible to create bi-directional lazy one to one association using @LazyToOne if you enable bytecode enhancement. MapsIds is still the best solution for your case though. [read about bytecode enhancement](https://vladmihalcea.com/how-to-enable-bytecode-enhancement-dirty-checking-in-hibernate/) – Kamil Bęben May 26 '20 at 23:26
  • @KamilBęben If I use `@MapsId` and unidirectional one-to-one, why is than running two select queries better than one query with left join? – clzola May 27 '20 at 09:01
  • You've said that you wanted this to be lazily loaded, and using `MapsId` is the way to achieve this. Hovewer, yesterday i haven't noticed that the parent-side has mappedBy specified. You could make it bi-directional association as it has been described [here](https://stackoverflow.com/a/62033179/6700091), and use EntityGraph to fetch both parent and child in a single query when needed. – Kamil Bęben May 27 '20 at 11:24
  • MapsId is correct in the code above, and even if it is wrong, it should affect userStatus not the User class. I don't think anyone can tell you what is going wrong. For other providers, I'd usually look for toString method implementations or anything else that might indicate what it accessing that user.status relationship forcing it to be fetched - and is why I suggested you remove/disable the status table, as the stack trace from the failed query will tell you exactly where it is and what is causing it - your code or some Hibernate setting. – Chris May 27 '20 at 14:57
  • it will give you two queries. to avoid queries you have two options 1. use entity graph. 2. use @JsonIgnore UserStatus in User – SSK May 28 '20 at 12:59

2 Answers2

0

For one to one relation in hibernate it is always loading reference object whether you keep Fetch type Lazy or Eager. So alternate solution is select only those columns which are needed, it should not contain that reference column. So in this case hibernate will not fire another query.

Query for below class will be :

@Query("select new Example(id,field1) from Example")

@Entity
@Table(name = "example")
class Example implements Serializable {

    private static final long serialVersionUID = 1L;

    public Example(Long id, String field1) {
        this.id = id;
        this.field1 = field1;
    }

    @Id
    @Column(name = "id", nullable = false, updatable = false)
    private Long id;

    @OneToOne(mappedBy = "example", fetch = LAZY, cascade = ALL)
    private CustomerDetails customerDetails;

    @Column(name = "field1", nullable = false, updatable = false)
    private String field1;
}
0

It is possible to make OTO lazy even if it's not the owning side. You just need to mark it as optional = false. This way Hibernate will know that it can safely a create proxy (and null is not possible) as the association always exists. Note, though it really must be non-optional - the 2nd entity must always exist. Otherwise you'll get an exception once Hibernate tries to load it lazily.

As for the number of queries, with native Hibernate (not JPA!) you can select org.hibernate.annotations.FetchMode. Which gives options to:

  • Use a separate select
  • Or use a join to load association

Alternatively, you can stay with JPA and write a JPQL query and use fetch join to keep it as a single query.

PS: before doing additional select Hibernate will check if the element already exists within the Session. If it is, then no select is going to be issued. But with fetch join or FetchMode.JOIN you won't have this luxury - join will always happen.

Stanislav Bashkyrtsev
  • 14,470
  • 7
  • 42
  • 45