Hibernate #OneToMany reference child not updated / lost? - java

Say I have three entities.
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
#ManyToAny(
metaColumn = #Column(name = "node_type"),
fetch = FetchType.LAZY
)
#AnyMetaDef(
idType = "long", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Milestone.class, value = MILESTONE_DISC),
#MetaValue(targetEntity = Phase.class, value = PHASE_DISC)
}
)
#Cascade({org.hibernate.annotations.CascadeType.ALL})
#JoinTable(
name = "process_nodes",
joinColumns = #JoinColumn(name = "process_id", nullable = false),
inverseJoinColumns = #JoinColumn(name = "node_id", nullable = false)
)
private Collection<ProcessNode> nodes = new ArrayList<>();
...
}
#Entity
#ToString
#DiscriminatorValue(MILESTONE_DISC)
public class Milestone implements ProcessNode {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Collection<ResultDefinition> results;
#ManyToOne()
private Process process;
...
}
#Entity
#ToString
public class ResultDefinition {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String externalId;
private String name;
private ResultType resultType;
}
From my client I want to add an Object of type ResultDefinition to a Milestone in a Process like this:
#Transactional
#PostMapping("/{milestone_id}/results")
public ResultDefinitionDto createResult(#PathVariable("milestone_id") Long milestoneId, #RequestBody ResultDefinitionDto dto) {
Process foundProcess = getProcess(milestoneId);
checkFoundProcess(milestoneId, foundProcess);
Milestone milestone = getMilestone(foundProcess, milestoneId);
ResultDefinition resultDefinition = resultDefinitionMapper.fromDTO(dto);
milestone.addResult(resultDefinition);
processService.save(foundProcess);
//TODO: Find out why this is necessary (???)
ResultDefinition savedResult = milestone.getResult(resultDefinition.getName());
return resultDefinitionMapper.fromEntity(savedResult);
}
In my method createResult I add resultDefinition to the milestone results collection.
When I save the parent foundProcess, I see that foundprocess->milestone->resultDefinition get's persisted and gets an ID. When I call resultDefinition.getId() it returns null. Also the ResultDefinition Object in the foundProcess is another reference and not the same that I added to milestone.results.
Why do I get the correct instance when calling milestone.getResult()?
Edit: my implementation of processService / repository
#Override
public Process save(Process entity) {
return processRepository.saveAndFlush(entity);
}
public interface ProcessRepository extends JpaRepository<Process, Long>, JpaSpecificationExecutor<Process> {
...
}

The ResultDefinition gets replaced during the save process. The transient entity gets inserted into the database and will be replaced through a managed entity with an id. Your reference to the ResultDefinition in the createResult method still points to the transient one. That´s why you have to work with the returned entities from a save call.
In your case you are saving the parent process. So you have to access the saved ResultDefinition through the process or milestone entity.

You can try add flush method after save.

Related

Hibernate: How to update parent_id field without retrieving parent from db?

In a Spring Boot app, I have the following entities that have one-to-many relationship (Category is the parent of Recipe):
#Entity
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 50)
private String title;
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = "category_id", referencedColumnName = "id")
private Category category;
}
#Entity
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true, nullable = false, length = 50)
private String name;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
private Set<Recipe> recipes = new HashSet<>();
public void addRecipe(Recipe recipe) {
recipes.add(recipe);
recipe.setCategory(this);
}
public void removeRecipe(Recipe recipe) {
recipes.remove(recipe);
recipe.setCategory(null);
}
}
When I create a Recipe, I send categoryId that is selected from Dropdown list and create Recipe by retrieving and adding category to the recipe as shown below:
#Transactional
public void update(RecipeRequest request) {
final Category category = categoryRepository.findById(request.getCategoryId())
.orElseThrow(() -> new NoSuchElementFoundException(NOT_FOUND_CATEGORY));
/* instead of retrieving category, I want to set the categoryId field of Recipe,
but there is not such kind of setter */
recipe.setCategoryId(request.getCategoryId());
recipe.setTitle(capitalizeFully(request.getTitle()));
recipe.setCategory(category);
recipeRepository.save(recipe);
}
Instead of retrieving category, I want to set the categoryId field of Recipe, but there is not such kind of setter:
recipe.setCategoryId(request.getCategoryId());
So, what is the most proper way for just setting the categoryId of the recipe and then saving it without requiring the category from db? Do I need a setter for categoryId field to the Recipe (I thought it, but does not seem elegant way)?
I would just add a categoryId field along with the corresponding getter and settter methods to the Recipe class.
#Entity
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 50)
private String title;
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = "category_id", referencedColumnName = "id")
private Category category;
#Column(name = "category_id", nullable = false)
private Integer categoryId;
// getters/setters
}
Having a categoryId field means that when we don't have to create an instance of Category when adding new Recipes. Sure, Recipe.category will be null but that's ok if we're just adding new Recipes. This approach could also prove beneficial if we later decide that we need to add many Recipes simultaneously.
If your repository implements org.springframework.data.jpa.repository.JpaRepository you may take advantage of using JpaRepository#getReferenceById method, in that case Hibernate instead of querying DB for data will return proxy object. However, such implementation may cause issues in some cases, for example:
// this call typically returns entity
// or null if entity wasn't found
repository.findById(id);
but:
// this call returns proxy object
repository.getReferenceById(id);
// now instead of returning entity
// repository either returns initialized proxy
// object or throws EntityNotFoundException
// if entity wasn't found
repository.findById(id);

Not able to delete in #OneToMany relationship spring data jpa

In my spring boot project, I have one LineItem entity below is the code
#Entity
#Table(name = "scenario_lineitem")
#Data
#NoArgsConstructor
public class LineItem implements Cloneable {
private static Logger logger = LoggerFactory.getLogger(GoogleConfigConstant.class);
#Id
#GeneratedValue(strategy = IDENTITY)
private BigInteger lineItemId;
#Column
private String name;
#OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL, CascadeType.PERSIST, CascadeType.MERGE })
#JoinColumn(name = "line_item_meta_id")
private List<QuickPopValue> quickPopValues;
}
Another entity is
#Entity
#Table(name = "quick_pop_value")
#Data
#NoArgsConstructor
public class QuickPopValue implements Cloneable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "quick_pop_value_id", columnDefinition = "bigint(20)", unique = true, nullable = false)
private BigInteger quickPopValueId;
#Column(name = "column_name")
private String columnName;
#Column(name = "value")
private String value;
#Column(name = "formula", columnDefinition = "longtext")
private String formula;
}
Now I am trying to delete QuickPopValue one by one but it's not getting deleted and not getting any exception as well.
Below is the delete code :
List<QuickPopValue> quickPopValues = sheetRepository.findByColumnName(columnName);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.delete(qpValue);
}
Such behavior occurs when deleted object persisted in the current session.
for (QuickPopValue qpValue : quickPopValues) {
// Here you delete qpValue but this object persisted in `quickPopValues` array which is
quickPopValueRepository.delete(qpValue);
}
To solve this you can try delete by id
#Modifying
#Query("delete from QuickPopValue t where t.quickPopValueId = ?1")
void deleteQuickPopValue(Long entityId);
for (QuickPopValue qpValue : quickPopValues) {
quickPopValueRepository.deleteQuickPopValue(qpValue.getQuickPopValueId());
}

Spring Data/Hibernate - Propagating Generated Keys

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;
}
}

Hibernate CascadeType.SAVE_UPDATE transcient exception on update

I use Spring boot with H2 in memory database with
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
and Hibernate Core {5.0.12.Final}
I have an object entity
#Entity
#Table(name = "T_OBJECT")
public class ObjectEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "IDT_OBJECT")
private Long id;
#Column(name = "NAME",length = 50, nullable = false)
private String name;
// from org.hibernate.annotations.CascadeType;
#Cascade(CascadeType.SAVE_UPDATE)
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CATEGORY_OBJECT", referencedColumnName = "IDT_CATEGORY")
private CategoryEntity category;
}
and a CategoryEntity
#Entity
#Table(name = "T_CATEGORY")
public class CategoryEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "IDT_CATEGORY")
private Long id;
#Column(name = "NAME")
private String name;
}
I have this objectService
#Transactional(rollbackOn = Exception.class)
public void saveOrUpdateObject(ObjectForm object) {
ObjectEntity objEntity = null;
if(object.getId() != null) {
objEntity = daoObject.findById(object.getId())
}
if(objEntity == null) {
objEntity = new ObjectEntity();
// we can't change the name
objEntity.setName(object.getName);
}
CategoryEntity catEntity = objEntity.getCategory();
if(catEntity == null) {
catEntity = new CategoryEntity();
}
catEntity.setName(object.getCategoryName())
objEntity.setCategory(catEntity);
// execute sessionFactory.getCurrentSession().saveOrUpdate()
daoObject.saveOrUpdate(objEntity);
}
This code perfectly work when I create the object, but I get a transcient error when update with a different category
Caused by: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.appli.entity.ObjectEntity.category -> com.appli.entity.CategoryEntity
of course if I save the CategoryEntity into my service with daoCategory.saveOrUpdate(catEntity) before save the ObjectEntity that works but that shouldn't be necessary with Cascade SAVE_UPDATE
But also just if I change CascadeType.SAVE_UPDATE by persistance.Cascade
#ManyToOne(fetch = FetchType.LAZY,cascade = {javax.persistence.CascadeType.PERSIST, javax.persistence.CascadeType.REFRESH, javax.persistence.CascadeType.MERGE})
#JoinColumn(name = "CATEGORY_OBJECT", referencedColumnName = "IDT_CATEGORY")
private CategoryEntity category;
it works for create and update without change a line of the service. #Cascade(CascadeType.ALL) also works
I though #Cascade(hibernate.annotations.CascadeType.SAVE_UPDATE) was the same than cascade = {javax.persistence.CascadeType.PERSIST, javax.persistence.CascadeType.REFRESH, javax.persistence.CascadeType.MERGE}

Persisting a #ManyToOne-referenced object only if it does not exist

I'm fairly new to Spring/JPA so this is somewhat a trivial question.
I have two entities with a many-to-one relationship: Item and ItemType. Basically, ItemType simply represents a unique name for a set of Items. I use a CrudRepository<Item, Long> to store them. The relevant code is as follows (getters/setters/equals()/hashCode() omitted):
#Entity
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "type_id")
private ItemType itemType;
public Item() {}
public Item(ItemType itemType) {
this.itemType = itemType;
}
}
#Entity
public class ItemType {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(unique = true, nullable = false)
private String name;
public ItemType() {}
public ItemType(String name) {
this.name = name;
}
}
#Controller
public class ItemsController {
#Autowired private ItemsRepo itemsRepo;
#RequestMapping(value = "/item", method = RequestMethod.POST)
#ResponseBody
public Item addQuestionSet(#RequestBody Item item) {
return itemsRepo.save(item);
}
}
When I insert a new Item into the database, I want it to get a type_id from either an ItemType with the given name if it already exists, or from a newly persisted ItemType otherwise.
As of now, I naturally get an exception when trying to insert the second item with the same type:
org.hsqldb.HsqlException: integrity constraint violation: unique constraint or index violation
I could probably make a boilerplate check in my controller before saving a new item into repository. But this task is rather generic, I'm pretty sure there must be a convenient solution in JPA.
Thanks.
It seems you are persist() method on the Item object rather than merge() method. I hope it will resolve your query.
I can see that the problem is when you "persist", try with "lazy" type. You could get the data only when you need it and EAGER always.
I can give you an example how i do it
this is my class "CentroEstudio"
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idCentroEstudio",nullable=false)
private Long idCentroEstudio;
#ManyToOne(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#JoinColumn(name = "idTipoCentroEstudio", nullable = false)
private TipoCentroEstudio tipoCentroEstudio;
#Column(name="nombre",nullable=false)
private String nombre;
#Column(name="activo",nullable=false)
private boolean activo;
this is my class "TipoCentroEstudio"
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="idTipoCentroEstudio",nullable=false)
private Long idTipoCentroEstudio;
#Column(name="descripcion",nullable=false)
private String descripcion;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "tipoCentroEstudio")
private Set<CentroEstudio> centroEstudio = new HashSet<CentroEstudio>(0);
I'm sorry for the Spanish in the example, but I'm peruvian and I speak Spanish.
I hope this helps you ...

Categories