Hibernate: findById vs getbyId

Viewed 14801

having this classes:

User.java:

@Entity
@Setter
@Getter
@NoArgsConstructor
public class User {
    @Id
    private int id;
    private String username;
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Address address;

    public User(String username) {
        this.username = username;
    }
}

Address.java:

@Entity
@Data
public class Address {
    @Id
    private int id;
    private String country;
    @OneToOne
    private User user;
}

UserRepository.java:

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}

DemoApplcation.java:

@Bean
    public CommandLineRunner loadData(UserRepository userRepo){
        return new CommandLineRunner() {
            @Override
            public void run(String... args) throws Exception {
                User u = new User("motherfucker");
                Address a = new Address();
                a.setCountry("CZ");
                u.setAddress(a);
                a.setUser(u);
                userRepo.save(u);

                //User newUser = userRepo.getById(0);
                User newUser = userRepo.findById(0).orElse(null);
                System.out.println(newUser.getUsername());
            }
        };
    }

Now the findById(int: id) works without problem (defined in CrudRepository from which extends JpaRepository). However the getById(int :id) (defined in JpaRepository) gives LazyInitializationException even with fetch = Fetch.EAGER attribute specified in mapping. In documentation it says

Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is implemented this is very likely to always return an instance and throw an EntityNotFoundException on first access. Some of them will reject invalid identifiers immediately.

  1. I didn't get EntityNotFoundException but LazyInitializationException
  2. Why is there a method which declares in its documentation that it throws exception without any reason? -> always return an instance and throw an EntityNotFoundException

from this description it seems for me this method be useless if it always throws exception. Why does this method exists?

What is the right way (method) for fetching data in hibernate?

4 Answers
  1. You got LazyInitializationException exactly because you set fetch = Fetch.EAGER. getById() returns a lazily fetched entity and thus the exception.
  2. Just guessing here, but maybe because of the laziness behaviour of getById(). But to be honest I don't understand that either.

It is also important to highlight another detail: findById() method uses EntityManager find(Class<T> entityClass, Object primaryKey, LockModeType lockMode, Map<String,Object> properties)) method internally and getById() uses EntityManager getReference(Class<T> entityClass, Object primaryKey) method. As a consequence, findById() returns the actual object and getById returns a reference of the entity.

Reference documentation:

Usually we can use either findById or getById. There's no issue in using either of it.

But if we want to understand more specific differences the those differences are as follows:

- getById: This is used only if we are sure about getting an entity we requested from the database. If we don't get any entity, it gives an exception. This is similar to a child who get's whatever it wants but starts screaming if it doesn't get what he requires.

- findById: This is used if we are not sure that whether the requested entity in the database is present or not. So even if the entity is not present in the database it returns null and doesn't throw any exception.

getById -> returns a reference proxy to for the actual Entity. Where only the id set (since you already passed it). This object's getters and setters may be called in the same @Transaction. But, once you get out of the transaction, then the proxy will return LazyInitializationException.

getById implementation

if you look at the getReference java doc. I believe this makes all clear. So, hibernate does not hit to the database if you call getById.

Get an instance, whose state may be lazily fetched. If the requested instance does not exist in the database, the EntityNotFoundException is thrown when the instance state is first accessed. (The persistence provider runtime is permitted to throw the EntityNotFoundException when getReference is called.) The application should not expect that the instance state will be available upon detachment, unless it was accessed by the application while the entity manager was open.

on the other hand, findById directly hit to DB and performs the select query.

I will provide examples for this simple mapping

@Entity
public class User {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String userName;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn // mappedBy can be used here for a bidirectional relation
    private Address address;

}

@Entity
public class Address {

    @Id
    private Long id;

    public Address(Long id) {
        this.id = id;
    }
    
}

Using transient Address

Lets say we have Address table with tabular data — many users can have the same address. We want to create a new user and let's say that we know a user's Address id = 100L from somewhere.

User user = new User();
Address address = new Address(100L);
user.setAddress(address);
userRepo.save(user);

It will work, but Hibernate generates an additionally SQL query to load the address with id=100L from a database.

Using Address proxy with getById()

Let's change our example

User user = new User();
Address address = addressRepo.getById(100L);
user.setAddress(address);
userRepo.save(user);

We get an address using addressRepo.getById(100L). This call doesn't hit the database at all. Hibernate just returns a proxy for the address id = 100L and doesn't load fields of the address from a database. And when we do userRepo.save(user) Hibernate just does one SQL insert. It doesn't load the address from a database like in the first example does.

Using attached Address with findById()

@Transactional
public void save() {
    User user = new User();
    Address address = addressRepo.findById(100L);
    user.setAddress(address);
    userRepo.save(user);
}

Using deattached Address with findById()

public void save() {
    User user = new User();
    Address address = addressRepo.findById(100L);
    user.setAddress(address);
    userRepo.save(user);
}
Related