I have two tables with 'one to many' relationship. I use Jpa + Spring JpaRepository. Sometimes I have to get object from Database with internal object. Sometimes I dont't have to. Repositories always return object with internal objects. I try to get 'Owner' from Database and I always get Set books; It's OK. But when I read fields of this internal Book , I get LazyInitializationException. How to get null instead of Exception?
@Entity
@Table(name = "owners")
@NamedEntityGraph(name = "Owner.books",
attributeNodes = @NamedAttributeNode("books"))
public class Owner implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "owner_id", nullable = false, unique = true)
private Long id;
@Column(name = "owner_name", nullable = false)
private String name;
@OneToMany(fetch = FetchType.LAZY,mappedBy = "owner")
private Set<Book> books= new HashSet<>(0);
public Worker() {
}
}
@Entity
@Table(name = "books")
@NamedEntityGraph(name = "Book.owner",
attributeNodes = @NamedAttributeNode("owner"))
public class Book implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "book_id", unique = true, nullable = false)
private Long id;
@Column(name = "book_name", nullable = false, unique = true)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id")
private Owner owner;
public Task() {
}
}
public interface BookRepository extends JpaRepository<Book,Long>{
@Query("select t from Book t")
@EntityGraph(value = "Book.owner", type = EntityGraph.EntityGraphType.LOAD)
List<Book> findAllWithOwner();
@Query("select t from Book t where t.id = :aLong")
@EntityGraph(value = "Book.owner", type = EntityGraph.EntityGraphType.LOAD)
Book findOneWithOwner(Long aLong);
}
You are getting LazyInitializationException
because you are accessing the content of the books Set outside the context of a transaction, most likely because it's already closed. Example:
You get an Owner from the database with your DAO or Spring Data repository, in a method in your Service class:
public Owner getOwner(Integer id) {
Owner owner = ownerRepository.findOne(id);
// You try to access the Set here
return owner;
}
At this point you have an Owner object, with a books Set which is empty, and will only be populated when someone wants to access its contents. The books Set can only be populated if there is an open transaction. Unfortunately, the findOne
method has opened and already closed the transaction, so there's no open transaction and you will get the infamous LazyInitializationException
when you do something like owner.getBooks().size()
.
You have a couple of options:
As OndrejM said you need to wrap the code in a way that it all executes in the same transaction. And the easiest way to do it is using Spring's @Transactional annotation:
@Transactional
public Owner getOwner(Integer id) {
Owner owner = ownerRepository.findOne(id);
// You can access owner.getBooks() content here because the transaction is still open
return owner;
}
You have fetch = FecthType.LAZY
in you @Column
definition and that's why the Set is being loaded lazily (this is also the fetch type that JPA uses by default if none is specified). If you want the Set to be fully populated automatically right after you get the Owner object from the database you should define it like this:
@OneToMany(fetch = FetchType.EAGER, mappedBy = "owner")
private Set<Book> books= new HashSet<Book>();
If the Book
entity is not very heavy and every Owner
does not have a huge amount of books it's not a crime to bring all the books from that owner from the database. But you should also be aware that if you retrieve a list of Owner
you are retrieving all the books from all those owners too, and that the Book
entity might be loading other objects it depends on as well.
The purpose of LazyInitializationException
is to to raise an error when the loaded entity has lost connection to the database but not yet loaded data which is now requested. By default, all collections inside an entity are loaded lazily, i.e. at the point when requested, usually by calling an operation on them (e.g. size() or isEmpty()).
You should wrap the code that calls the repository and then works with the entity in a single transaction, so that the entity does not loose connection to DB until the transaction is finished. If you do not do that, the repository will create a transaction on its own to load the data, and close the transaction right after. Returned entity is then without transaction and it is not possible to tell, if ots collections have some elements or not. Instead, LazyInitializationException
is thrown.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With