I have the following entities
RegisteredProgram
#Data
#NoArgsConstructor
#Entity
#EntityListeners(RegisteredProgramAuditListener.class)
public class RegisteredProgram extends Auditable<String> {
#OneToMany(mappedBy = "registeredProgram", cascade = CascadeType.ALL)
#JsonBackReference
private List<Trainer> trainerList;
#OneToMany(mappedBy = "registeredProgram", cascade = CascadeType.ALL)
#JsonBackReference
private List<Official> officialList;
}
Trainer
#Data
#NoArgsConstructor
#EntityListeners(TrainerAuditListener.class)
#Entity
public class Trainer extends Auditable<String> {
#ManyToOne
#JoinColumn(name = "REGISTERED_PROGRAM_ID", nullable = false)
#JsonManagedReference
private RegisteredProgram registeredProgram;
#Type(type = "yes_no")
private Boolean isDeleted = false;
}
Official
#Data
#NoArgsConstructor
#EntityListeners(OfficialAuditListener.class)
#Entity
public class Official extends Auditable<String> {
#ManyToOne
#JoinColumn(name = "REGISTERED_PROGRAM_ID", nullable = false)
#JsonManagedReference
private RegisteredProgram registeredProgram;
#Type(type = "yes_no")
private Boolean isDeleted = false;
}
Basically I have entities with many to one relationship with RegisteredProgram, (Trainer-RegisteredProgram, Official-RegisteredProgram). Now I have a service which already achieves my requirement, to fetch a registered program by id and I should only include all the Trainer and Official with isDeleted false. See the service below:
Service
#Override
public RegisteredProgramRequestDto getRegisteredProgramDto(Long id) {
RegisteredProgram registeredProgram = registeredProgramRepository.getOne(id);
RegisteredProgramRequestDto registeredProgramRequestDto = programRegistrationMapper
.registeredProgramToRequestDto(registeredProgram);
registeredProgramRequestDto.setOfficialDtoList(
registeredProgramRequestDto.getOfficialDtoList()
.stream()
.filter(officialDto -> !officialDto.getIsDeleted())
.collect(Collectors.toList())
);
registeredProgramRequestDto.setTrainerDtoList(
registeredProgramRequestDto.getTrainerDtoList()
.stream()
.filter(trainerDto -> !trainerDto.getIsDeleted())
.collect(Collectors.toList())
);
return registeredProgramRequestDto;
}
Now, I tried to use #Query and #EntityGraph so I can be able to get the desired output using only a single query.
Repository
#Repository
public interface RegisteredProgramRepository extends JpaRepository<RegisteredProgram, Long>, QuerydslPredicateExecutor<RegisteredProgram> {
#Query("select rp from RegisteredProgram rp join rp.officialList rpos join rp.trainerList rpts where rp.id = :id and rpos.isDeleted = false and rpts.isDeleted = false")
#EntityGraph(attributePaths = {"officialList", "trainerList"}, type = EntityGraph.EntityGraphType.LOAD)
RegisteredProgram getByIdNotDeleted(#Param("id") Long id);
}
Updated Service
#Override
public RegisteredProgramRequestDto getRegisteredProgramDto(Long id) {
RegisteredProgram registeredProgram = registeredProgramRepository.getByIdNotDeleted(id);
return programRegistrationMapper
.registeredProgramToRequestDto(registeredProgram);
}
But after implementing it, i am encountering the error below:
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.tesda8.region8.program.registration.model.entities.RegisteredProgram.officialList, com.tesda8.region8.program.registration.model.entities.RegisteredProgram.trainerList]
I already searched through stackoverflow and bumped into this but I still can't get my query to execute properly. Any ideas on how should I approach this?
The regular fix for solving MultipleBagFetchException is change List typed fields on Set typed, like this:
...
public class RegisteredProgram extends Auditable<String> {
#OneToMany(mappedBy = "registeredProgram", cascade = CascadeType.ALL)
#JsonBackReference
private Set<Trainer> trainerList = new HashSet<>();
#OneToMany(mappedBy = "registeredProgram", cascade = CascadeType.ALL)
#JsonBackReference
private Set<Official> officialList = new HashSet<>();
...
}
For more details see: https://thorben-janssen.com/hibernate-tips-how-to-avoid-hibernates-multiplebagfetchexception/
Note: Remember about equals and hashcode for Set data structure and avoiding Lombok & Hibernate pitfalls(https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/). Please pay attention for 'Avoid #Data' topic, because I see you are using that combination, that combination can produce unexpected behavior!
Related
I have 4 Entities, that a related to each other with #OneToMany relationships.
When I try to save Order that contains OrderItem - Orderitem has no backreference.
In the code below only important fields are showed for brevity ( usual strings and primitives are omitted ). I decided to include Dish and User Entities also.
Order:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne(fetch = FetchType.LAZY)
private User user;
#OnDelete(action = OnDeleteAction.CASCADE)
#OneToMany(
mappedBy = "order",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
orphanRemoval = true)
private List < OrderItem > orderItems;
}
Dish:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
public class Dish {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "dish")
#ToString.Exclude
private List < OrderItem > orderItems;
}
OrderItem:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
public class OrderItem {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne(fetch = FetchType.LAZY)
#ToString.Exclude
private Dish dish;
#ManyToOne(fetch = FetchType.LAZY)
private Order order;
private int quantity;
}
User:
#Entity
#NoArgsConstructor
#Getter
#Setter
#ToString
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#OneToMany(
mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List < Order > orders;
}
The problem happens when I try to save Order with Spring data JPA.
Let's print Order to see OrderItem before saving.
public Order saveOrder(Order order) {
System.out.println("SERVICE saving order " + order);
return orderRepository.save(order);
}
As you can see, orderItems backreference is null before saving ( I though spring data jpa should deal with setting it ).
SERVICE saving order Order(id=0,
orderItems=[OrderItem(id=0, quantity=2, order=null)])
Here is what I have in DB ( Order and OrderItem entities ).
In your OrderItem class, add annotation below:
#ManyToOne(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.PERSIST}, fetch=FetchType.LAZY)
#JoinColumn(name="order_id", referencedColumnName="id", nullable = false)
Order order.
One more thing, I suggest you use SEQUENCE_GENERATOR, beacause IDENTITY means: I'll create the entity with a null ID and the database will generate one for me. I don't think Postgres even supports that, and even if it does, a sequence generator is a better, more efficient choice.
The best option that I found for this is doing something like:
order.getOrderItems().forEach(orderItem -> orderItem.setOrder(order));
Before your save() call. Even though order is not persisted at this point, it seems like Hibernate can resolve the relation and the back references will be set correctly.
If you do not want to bother setting the back reference in your business logic, you can add something like this to your entity:
class Order {
...
#PrePersist
public void prePersist() {
setMissingBackReferences();
}
private void setMissingBackReferences() {
orderItems.forEach(oderItem -> {
if (oderItem.getOrder() == null) {
oderItem.setOrder(this);
}
});
}
...
}
I have the following setup:
Micronaut 3.x
Java 15
Entity I'm trying to update:
#Setter
#Getter
#Entity
#ToString
#Table(name = "single_choice_question")
public class SingleChoiceQuestion extends OrderedQuestion {
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
private SingleAnswer answer;
#Override
public void answer(Answer answer) {
if (answer instanceof SingleAnswer single) {
this.answer = single;
}
throw new IllegalArgumentException("answer is not of SingleAnswer type");
}
}
Its child entity I'm trying to persist as part of the one above:
#Getter
#Setter
#Entity
#Table(name = "single_answer")
public class SingleAnswer extends Answer {
#OneToOne private AnswerOption choice;
}
All entities inherit from this:
#MappedSuperclass
public abstract class BaseEntity {
#Id
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid2")
#Column(updatable = false, nullable = false, length = 36)
#Type(type = "pkg.UuidUserType") // Maps UUID to SQL VARCHAR.
private UUID id;
#Version
#Column(nullable = false)
private Integer version;
...
}
the base answer class:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Answer extends BaseEntity {
#Column(nullable = false)
private Boolean correct;
}
The below lines are executed:
#ReadOnly
public class MyService {
#Transactional
public <A extends AnswerDto> A answer(A dto, UUID question) {
var answer = questionsMapper.toAnswer(dto);
answer.setCorrect(isCorrect(dto, question));
var orderedQuestion =
orderedQuestionRepository
.findById(question)
.orElseThrow(() -> new NotFoundException("question", question));
orderedQuestion.answer(answer, false);
orderedQuestionRepository.saveAndFlush(orderedQuestion);
return dto;
}
}
Expected behaviour:
the SingleAnswer instance is persisted, and its FK is saved in the answer_id column of the question.
Actual behaviour:
the SingleAnswer instance is persisted, but its FK is NOT saved in the answer_id column of the question. They are not connected in any way, so orphan removal doesn't seem to work either.
After examining hibernate's logs, I can see it only executes the insert and does not do and update on the question.
Another observation is when I remove flushing, orphan removal does work - SingleAnswer doesn't persist, although the FK situation isn't resolved still.
orderedQuestion.answer(answer);
orderedQuestionRepository.save(orderedQuestion);
I can't see anything wrong with this very basic setup, any help would be appreciated.
The issue was painfully simple, a #ReadOnly sneaked in on a class level at some point by someone, and I didn't notice
Include the #JoinColumn annotation indicating the foreign key in SingleChoiceQuestion entity:
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "answer_id")
private SingleAnswer answer;
I have been working on a project and trying to perform a DTO projection for my base class but I have an issue.
Here's my entities:
#Entity
#Table(name = "QUESTION")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING)
#DiscriminatorValue("DESCRIPTIVE")
public class Question extends BaseEntity<Long> {
#Column(name = "TITLE",
nullable = false)
private String title;
#Column(name = "QUESTION_BODY",
nullable = false)
private String questionBody;
#Column(name = "QUESTION_TYPE",
nullable = false)
private QuestionType questionType;
}
And TestQuestion entity which extends base Question Entity:
#Entity
#DiscriminatorValue("TEST")
public class TestQuestion extends Question {
#OneToMany(mappedBy = "testQuestion",
cascade = CascadeType.ALL,
orphanRemoval = true)
private Set<Option> options;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "RIGHT_OPTION_ID")
private Option rightOption;
}
Now I have a base Question repository extending the JpaRepository which has the findAll method:
public interface QuestionRepository extends JpaRepository<Question, Long> {
#Query("select tq from Test t join t.questions tq where t.id = :testId ")
List<Question> fetchTestQuestions(Long testId);
}
When I fetch all the questions, since I have a rest api and the question entity has all the other attributes with toMany relationships, I get a recursive JSON which is a mess!
I tried to use #JsonIgnore but it didn't work so I decided to use DTO projection.
The problem is I dont know how to perform a dto projection to call the constructor of each of my DTOs like QuestionDTO and TestQuestionDTO for each of the Questions since each of them have their own types.
Is such a thing even possible?
I'm trying to build build service, which saves object with sub-objects, but getting error. In result object data fields saved, but sub-object not.
I have the next object. The main is Order and sub-object is Partner:
#Getter
#Setter
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "order_id")
private int orderId;
#OneToMany(mappedBy = "order", fetch = FetchType.EAGER,
cascade = CascadeType.ALL)
private Set<Partner> partners;
}
#Getter
#Setter
#Entity
#Table(name = "partners")
public class Partner implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "partner_id")
private int id;
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "order_id", nullable = false)
private Order order;
}
I use standard embedded method "save" from Spring Jpa Repository:
#Repository
public interface OrdersRepository extends JpaRepository<Order, Integer> {
}
and service, which call this Repository:
#Service
public class OrdersServiceImpl implements OrdersService {
#Autowired
private OrdersRepository repository;
#Override
public Order save(Order order) {
return repository.save(order);
}
}
Does someone have an idea why Partners are not saved?
Thanks a lot!
Because the relationship owner is Partner, so that you need to save the Order first. Or you can put cascade = CascadeType.PERSIST on private Order order;
Usually I'm able to Google my way out of asking questions here (thank you SO community), but I'm a bit stuck here. This problem has to do with propagating generated keys to joined objects when calling JpaRepository.save()
We have entities that are defined like so:
Parent object
#Entity
#Table(name = "appointment")
public class Appointment implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "APPT_ID", columnDefinition = "integer")
private Long apptId;
...
#OneToMany(targetEntity = ApptReminder.class, mappedBy = "appointment", cascade = {
CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.EAGER)
#NotFound(action = NotFoundAction.IGNORE)
private List<ApptReminder> apptReminders = new ArrayList<>();
}
Child Object:
#Entity
#Table(name = "appt_reminder")
public class ApptReminder implements Serializable {
#EmbeddedId
private ReminderKey reminderKey = new ReminderKey();
...
#ManyToOne
#NotFound(action = NotFoundAction.IGNORE)
private Appointment appointment;
}
Embedded Id Class
#Embeddable
public class ReminderKey implements Serializable {
#Column(name = "APPT_ID", columnDefinition = "integer")
private Long apptId;
#Column(name = "CALL_NUM", columnDefinition = "integer")
private Short callNum;
....
}
Repository:
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
}
And we have a bunch of sets of objects hanging off of the child object all sharing the embedded key attributes. When we call save on the parent object appointmentRepository.save(appointment) the child objects get saved, but the appt_id of the first appointment inserted gets an auto generated key of 1, and the first apptReminder record gets an appt_id of 0.
This affects all joined objects that share the embedded ID of ReminderKey with similar and predictable effects.
When we call appoitnmentRepository.save(appointment) on the top level entity, how do we get the autogenerated keys to propagate through to child entities? I feel like this should be very easy. Perhaps there's an element of the way I laid out the mappings or the usage of an embedded id that's preventing this from working.
One last thing of note is that this is running against an H2 database while in development, but will be used against MySQL afterwards. This could be attributable to H2's MySQL compatibility
I think you need to use JoinColumns annotation to marry Appointment apptId to ReminderKey apptId.
Solved this way:
Detach appointment from apptReminder on persist operations:
public class Appointment implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "APPT_ID", columnDefinition = "integer")
private Long apptId;
...
#OneToMany(targetEntity = ApptReminder.class, mappedBy = "appointment", cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
#NotFound(action = NotFoundAction.IGNORE)
private List<ApptReminder> apptReminders = new ArrayList<>();
}
Create a DAO to handle persistence operations:
#Repository
public class AppointmentDAO {
#Autowired
private AppointmentRepository appointmentRepository;
#Autowired
private ApptReminderRepository apptReminderRepository;
public List<Appointment> save(List<Appointment> appointments) {
appointments.forEach(a -> this.save(a));
return appointments;
}
public Appointment save(Appointment appointment) {
final Appointment appt = appointmentRepository.save(appointment);
List<ApptReminder> apptReminders = appointment.getApptReminders();
apptReminders.forEach(a -> {
a.getReminderKey().setApptId(appt.getApptId());
a.getReminderTags().forEach(t -> t.setApptId(appt.getApptId()));
a.getReminderMessages()
.forEach(m -> m.getReminderMessageKey().setApptId(appt.getApptId()));
a.getMsgQueueReminder().setApptId(appt.getApptId());
});
apptReminderRepository.saveAll(apptReminders);
return appointment;
}
}