Lazy loading doesn't work for OneToMany relationship on Spring Boot - spring

I've been working with spring boot data jpa + spring + mysql + thymeleaf and have encountered a problem.
Is a #OneToMany (fetch = FetchType.LAZY) relationship for a list, the list loads hibernate like #OneToMany (fetch = FetchType.EAGER) outside. But if I use the annotation #ManyToOne this does work perfectly.
Any idea why it happens this behavior?
By the way, I want to keep the spring.jpa.open-in-view = true property. I debug an application Author - Book (One to Many) developed in IntelliJ IDEA.
Here's the Book class.
#Entity
public class Book implements Serializable {
#Id
#GeneratedValue
private Integer id;
#NotNull
private String name;
#NotNull
#JoinColumn(name = "author_id")
#ManyToOne(fetch = FetchType.LAZY)
private Author author;
/* getters and setters */
}
Here the class Author
#Entity
public class Author implements Serializable {
#Id
#GeneratedValue
private Integer id;
#NotNull
private String name;
#OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> bookList;
/* getters and setteres */
}
The controller for debug.
#Controller
#RequestMapping("/")
public class HomeController {
#Autowired
private AuthorRepository authorRepository;
#Autowired
private BookRepository bookRepository;
#GetMapping
private ModelAndView index() {
List<Author> authorList = authorRepository.findAll();
return new ModelAndView("home"); // first breakpoint
}
#GetMapping("/books")
private ModelAndView viewBooks() {
List<Book> bookList = bookRepository.findAll();
return new ModelAndView("books"); // second breakpoint
}
}
Here's the result.
First breakpoint result
Second breakpoint result

Everything seems to be defined correctly, and after a research, I have done Spring data does use lazy loading plus you defined it on your associations.
I believe that when you use the debugging views in order to tell what inside, then you actually do the fetching.

Related

When does the hibernate session gets closed

I have created the following entities.
#Entity
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "student")
private List<Book> books;
}
#Entity
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToOne
#JoinColumn(name = "STUDENT_ID")
private Student student;
}
My controller looks like this
#RestController
public class Controller {
MyService myService;
public Controller(MyService myService) {
this.myService = myService;
}
#GetMapping("student")
public List<Book> getBooksForStudent(Long id) {
return myService.getBooks(id);
}
}
The service is as follows.
public class MyService {
#Autowired
private StudentRepo studentRepo;
public List<Book> getStudent(Long id) {
Optional<Student> studentOptional = studentRepo.findById(id);
return studentOptional.map(Student::getBooks).orElseThrow(IllegalArgumentException::new);
}
}
I am getting the list of books as expected. But as I'm having lazy loaded list for books I should be getting a LazyInitializationException. I have not added transnational to the method and I'm returning the list of books from the entity itself without mapping it to a DTO. Why is the hibernate session not getting closed after the end of the method?
#RestController is transactional by default. Spring boot automatically registers an OpenEntityManagerInViewInterceptor when you use a web application/you use JPA. Refer #RestController methods seem to be Transactional by default, Why?

How can I add a tenant condition to Spring Data JPA Default and Dervied Queries

I have a Springboot Application with Repositories having Spring Data JPA Queries like findOne, findAll and also derived ones like findByID or findByName etc.
What I want to achieve is multitenancy. All entities have an "account_id" column which holds the tenant.
How do I add a filter like "account_id" to all the queries metioned above without using derived queries that contains those name slike findIdAndAccountid (which would be findone)
#Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
Category findByName(String name);
}
Here's the corresponding entity
#Entity
#Table(name = "unit")
#Data
public class Unit {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
#Column(name = "account_id")
private Long account_id;
}
I know most people use schemas as tenant separation but that's impossible for me. Is there a way (I didn't find one) to add such a tenant filter condition on those queries without writing NamedQueries or using DerivedQueries. An elegeant solution like annotate the repository or entity or maybe the queries that all queries should add the additional filter "account_id"?
You can add Where clause on your Entity classes (Didnt had time to test )
#Entity
#Table(name = "unit")
#Data
#Where(clause = "account_id= :account_id")
public class Unit {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
#Column(name = "account_id")
private Long account_id;
}
Update and Solution
1. Create a Filter & FilterDef on the entity like so
#FilterDef(name="accountFilter", parameters=#ParamDef( name="accountId", type="long" ) )
#Filters( {
#Filter(name="accountFilter", condition=":accountId = account_id")
} )
public class Category {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
#Column(name = "account_id")
private Long account_id;
}
enable filtering in the controller by autowiring entitymanager, writing a method to enable the filter and activate the filter in #ModelAttribute for each request
#RestController
#RequestMapping(path = "/categories",produces = MediaType.APPLICATION_JSON_VALUE )
public class CategoryController {
private final CategoryRepository repository;
#Autowired
private EntityManager entityManager;
CategoryController(CategoryRepository repository) {
this.repository = repository;
}
private void activateFilter() {
Session session = entityManager.unwrap(Session.class);
Filter filter = session.enableFilter("accountFilter");
filter.setParameter("accountId", Long.valueOf(TenantContext.getCurrentTenant()));
}
#ModelAttribute
public void initFilter() {
activateFilter();
}
... your rest methods here
}

Using Entity with OneToMany and HATEOAS RessourceAssembler leads to infinite recursion

I'm using two JPA entities annotated with #OneToMany (parent) <-> #ManyToOne (child) and I also wrote a RessourceAssembler to turn the entities into resources in the controller of a Springboot application (see below for code samples).
Without the relationship #OneToMany in the parent entity, Ressource assembling and serialisation works just fine.
As soon as I add the OneToMany relation on the parent the serialisation breaks with this:
"Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.springframework.hateoas.Resource[\"content\"]->com.marcelser.app.entities.Storage[\"storageStock\"])"
As you can see the infinite loop comes from the hateoas Resource, not the entities themselves.
I already tried to add #JsonManagedReference and #JsonBackReference on the entities or #JsonIgnore on the child but nothing really helps. The Hateoas RessourceAssembler always ends up in a infinite loop as soon as the child entity is embedded. It seems that shose #Json.... annotations help with the JSON serialisation of the entity itself but they don't solve problems with the RessourceAssembler
I have these 2 entities (Storage & Stock)
#Entity
#Table(name = "storage")
#Data
public class Storage {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "storage")
private Set<Stock> storageStock = new HashSet<>();;
}
#Entity
#Table(name = "stock")
#Data
public class Stock {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "storage_id")
private Storage storage;
... other fields ommitted
}
and I'm using a RessourceAssemlber like follows for the parent entity 'Storage':
#Component
public class StorageResourceAssembler implements ResourceAssembler<Storage, Resource<Storage>> {
#Override
public Resource<Storage> toResource(Storage storage) {
return new Resource<>(storage,
linkTo(methodOn(StorageController.class).one(storage.getId())).withSelfRel(),
linkTo(methodOn(StorageController.class).all()).withRel("storages"));
}
}
and in the controller I have 2 get classes to list all or just a single Storage with its childs
public class StorageController {
private final StorageRepository repository;
private final StorageResourceAssembler assembler;
#GetMapping
ResponseEntity<?> all() {
List<Resource<Storage>> storages = repository.findAll().stream()
.map(assembler::toResource)
.collect(Collectors.toList());
Resources<Resource<Storage>> resources = new Resources<>(storages,
linkTo(methodOn(StorageController.class).all()).withSelfRel());
return ResponseEntity.ok(resources);
}
private static final Logger log = LoggerFactory.getLogger(StorageController.class);
StorageController(StorageRepository repository, StorageResourceAssembler assembler) {
this.repository = repository;
this.assembler = assembler;
}
#GetMapping("/{id}")
ResponseEntity<?> one(#PathVariable Long id) {
try {
Storage storage = repository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(id));
Resource<Storage> resource = assembler.toResource(storage);
return ResponseEntity.ok(resource);
}
catch (EntityNotFoundException e) {
log.info(e.getLocalizedMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body (new VndErrors.VndError("Storage not found", "could not find storage with id " + id ));
}
}
... omitted Put/Post/Delete
}
Can anyone enlighten me how I can solve this infinite loop in HateOAS. What I want is that the embedded child entries just either don't link back to the parent (so no links to parent are created) or they contain the link for the one level but no further processing is done.
To handle the problem related to the serialization of the model using Jackson API when the model attributes have a lazy loading defined, we have to tell the serializer to ignore the chain or helpful garbage that Hibernate adds to classes, so it can manage lazy loading of data by declaring #JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) annotation.
#Entity
#Table(name = "storage")
#Data
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Storage {...
#Entity
#Table(name = "stock")
#Data
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Stock {...
or you can just declare unilaterally mapping commenting the Storage entity declaration and changing the private Storage storage; to fetch EAGER #ManyToOne(fetch = FetchType.EAGER) in Stock class.
#Entity
#Table(name = "storage")
#Data
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Storage {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
/*#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "storage")
private Set<Stock> storageStock = new HashSet<>();;*/
}
#Entity
#Table(name = "stock")
#Data
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Stock {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#JsonIgnore
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "storage_id")
private Storage storage;
... other fields ommitted
}
Maybe a little late, but I've had this problem or very similar and I've only found one solution. The same error 500 gave me the clue on how to solve it:
Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.hateoas.PagedModel["_embedded"]->java.util.Collections$UnmodifiableMap["usuarios"]->java.util.ArrayList[0]->org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer$1["content"]->com.tfg.modelos.Usuario["rol"]->org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer$1["content"]->com.tfg.modelos.Rol$HibernateProxy$QFcQnzTB["hibernateLazyInitializer"])
So I only had to add in the application.properties:
spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false

Lazy loading with JPA on #ManyToOne

I have a project with Spring Boot, and I cannot make lazy loading work. I have 2 entities: Question and Answer. A question can have many answers.
What I want is, when I try to get an answer, to get only the answer without the question. And also, if I want both, to have this possibility.
What I did, is I added in application.yml: spring.jpa.open-in-view: true.
The Answer entity is like:
#Entity
#Table(name = "mst_ans_answer", schema = "lquest_sc")
public class Answer implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "lquest_sc.mst_ans_answer_ans_lqs_id_seq")
#SequenceGenerator( name = "lquest_sc.mst_ans_answer_ans_lqs_id_seq", sequenceName = "lquest_sc.mst_ans_answer_ans_lqs_id_seq")
#Column(name = "ans_lqs_id")
private int id;
#Column(name = "qst_lqs_id")
private int questionId;
#Column(name = "ans_text")
private String text;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "qst_lqs_id", insertable=false, updatable=false)
#JsonIgnore
private Question question;
//getters and setters
}
The Question entity is:
#Entity
#Table(name = "mst_qst_question", schema = "lquest_sc")
public class Question implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "lquest_sc.mst_qst_question_qst_lqs_id_seq")
#SequenceGenerator(name = "lquest_sc.mst_qst_question_qst_lqs_id_seq", sequenceName = "lquest_sc.mst_qst_question_qst_lqs_id_seq")
#Column(name = "qst_lqs_id")
private int id;
#Column(name = "qst_title")
private String title;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "question")
#OrderBy("order asc")
private Set<Answer> answers = new HashSet<Answer>();
//getters and setters here
The call in the controller is:
#RequestMapping(value = "/questionId/{id}", method = RequestMethod.GET)
public List<Answer> listAll( #PathVariable("id") int id ){
List<Answer> answers = answerRepository.findByEnabledAndQuestionIdOrderByOrderAsc(1,id);
return answers;
}
and the repository is
public interface AnswerRepository extends JpaRepository<Answer, Long> {
List<Answer> findByEnabledAndQuestionIdOrderByOrderAsc(int enabled,int questionId);
}
The problem is that in the controller, when I try to evaluate
answers.get(0).getQuestion(), I receive the entity of Question, with the properties filled with null values and the error Method threw 'org.hibernate.LazyInitializationException' exception. Cannot evaluate Question_$$_jvst5b6_1.toString(). What am I doing wrong?
I don't know why spring.jpa.open-in-view = true is not working in your case. Maybe the OpenEntityManagerInViewInterceptordoes not get triggered or the thread has been left, when you are evaluating the Question. Or you have an older version which just does not support it.
Lazy loading works only inside of a transaction. A solution could be -
as #Pradeep already gave you the hint - to use #Tranactional inside a business logic class.
Even if you put the #Transactional inside your repository it will not work, because you have to put the annotation on top of the method where you are trying to evaluate answers.get(0).getQuestion().
Furthermore I advise you not to call your repository from the controller directly, but to use a service layer, where you put your business logic.
Example implementation
This is only a example implementation to show you how to structure your application and what the important keywords are. Also note, that you can either use #Inject or #Autowired. When you have implemented your logic, just inject the service into your controller and use it there.
AnswerService.java
public interface AnswerService {
List<Answer> findByEnabledAndQuestionIdOrderByOrderAsc(int enabled, Long id);
}
AnswerServiceImpl.java
#Service
public class AnswerServiceImpl implements AnswerService {
private AnswerRepository answerRepository;
#Inject
public AnswerServiceImpl(AnswerRepository answerRepository) {
this.answerRepository = answerRepository;
}
#Transactional
#Override
public List<Answer> findByEnabledAndQuestionIdOrderByOrderAsc(int enabled, Long id) {
List<Answer> answerList = findByEnabledAndQuestionIdOrderByOrderAsc(int enabled,int questionId);
// do your lazy loading here
// because you are still in the same transactional context
// return the list
return answerList;
}
}

Why the record is posted twice in the database?

Can you tell me, why the record is posted twice in the database. I think. this happens because I use save() method. But shouldn't I save the master-entity and dependent-entity separately?
Controller method:
#RequestMapping(value = "/addComment/{topicId}", method = RequestMethod.POST)
public String saveComment(#PathVariable int topicId, #ModelAttribute("newComment")Comment comment, BindingResult result, Model model){
Topic commentedTopic = topicService.findTopicByID(topicId);
commentedTopic.addComment(comment);
// TODO: Add a validator here
if (!comment.isValid() ){
return "//";
}
// Go to the "Show topic" page
commentService.saveComment(comment);
return "redirect:../details/" + topicService.saveTopic(commentedTopic);
}
Services:
#Service
#Transactional
public class CommentService {
#Autowired
private CommentRepository commentRepository;
public int saveComment(Comment comment){
return commentRepository.save(comment).getId();
}
}
#Service
#Transactional
public class TopicService {
#Autowired
private TopicRepository topicRepository;
public int saveTopic(Topic topic){
return topicRepository.save(topic).getId();
}
}
Model:
#Entity
#Table(name = "T_TOPIC")
public class Topic {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#ManyToOne
#JoinColumn(name="USER_ID")
private User author;
#Enumerated(EnumType.STRING)
private Tag topicTag;
private String name;
private String text;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "topic", cascade = CascadeType.ALL)
private Collection<Comment> comments = new LinkedHashSet<Comment>();
}
#Entity
#Table(name = "T_COMMENT")
public class Comment
{
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#ManyToOne
#JoinColumn(name="TOPIC_ID")
private Topic topic;
#ManyToOne
#JoinColumn(name="USER_ID")
private User author;
private String text;
private Date creationDate;
}
In this concrete case, you do not need to save the master and the client.
Saving the master or the client would be enough (with this concrete mapping)
But I think the main problem is that you do not have a good equals method in your Comment so your ORM Provider think that there are two different comments, and therefore store them twice.

Resources