Spring Data/Hibernate - Propagating Generated Keys - java

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

Related

JPA one to one mapping creates multiple query when child entity is not found

I have a parent entity 'contracts' that has a one-to-one relation with another entity 'child-contract'. the interesting thing is that the mapping field ('contract_number')id not a primary key-foreign key but is rather a unique field in both the tables. Also it is possible for a contracts to not have any child contract altogether. With this configuration I have observed hibernate to generate 1 additional query every time a contracts does not have a child-contract. I filed this behavior very strange. Is there a way to stop these unnecessary query generation or have I got something wrong.
below is a piece of my code configuration.
#Data
#Entity
#Table(name = "contracts")
public class Contracts implements Serializable {
#Id
#JsonIgnore
#Column(name = "id")
private String id;
#JsonProperty("contract_number")
#Column(name = "contract_number")
private String contractNumber;
#OneToOne(fetch=FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JsonProperty("crm_contracts")
#JoinColumn(name = "contract_number", referencedColumnName = "contract_number")
private ChildContract childContract ;
}
#Data
#NoArgsConstructor
#Entity
#Table(name = "child_contract")
#BatchSize(size=1000)
public class ChildContract implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#JsonProperty("id")
#Column(name = "id")
private String id;
#JsonProperty("contract_number")
#Column(name = "contract_number")
private String contractNumber;
}
Please help.
Thank-you
You can use NamedEntityGraph to solve multiple query problem.
#NamedEntityGraph(name = "graph.Contracts.CRMContracts", attributeNodes = {
#NamedAttributeNode(value = "crmContract") })
Use this on your repository method as
#EntityGraph(value = "graph.Contracts.CRMContracts", type = EntityGraphType.FETCH)
// Your repo method in repository

Java Spring Data App doesn't save sub-objects

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;

Hibernate #OneToMany reference child not updated / lost?

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.

Spring Jpa Data Repository save (update) with LinkedEntity for ManyToMany relationship

There are 2 entities (lets say Rule and Label) with many-to-many relationship using linked entity
as per hibernate reference documentation
Rule enity:
#Entity
#Table(name = "rule")
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "name")
public class Rule implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NaturalId
#NotBlank
#Column(unique = true)
private String name;
#Lob
#Column(columnDefinition = "TEXT")
private String content;
#OneToMany(mappedBy = "rule", cascade = {CascadeType.PERSIST,
CascadeType.MERGE})
private List<RuleLabel> labels = new ArrayList<>();
...
Label entity:
#Entity
#Table(name = "label")
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Label implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank
private String name;
#OneToMany(mappedBy = "label", cascade = {CascadeType.PERSIST,
CascadeType.MERGE})
private List<RuleLabel> rules = new ArrayList<>();
...
Link entity:
#Entity
public class RuleLabel implements Serializable {
#Id
#ManyToOne
private Rule rule;
#Id
#ManyToOne
private Label label;
...
Repositories:
#Repository
public interface LabelRepository extends JpaRepository<Label, Long>
...
#Repository
public interface RuleRepository extends JpaRepository<Rule, Long>
...
Creating new entity via RuleRepository.save(Rule) works fine, but when I'm trying to update existing entity (the same method RuleRepository.save(Rule), but entity to be saved contains id field) it leads to infinite loop of Hibernate: select... queries:
Hibernate: select rule0_.id as id1_7_1_, rule0_.is_active as is_activ2_7_1_, rule0_.content as content3_7_1_, rule0_.is_deleted as is_delet4_7_1_, rule0_.import_section as import_s5_7_1_, rule0_.name as name6_7_1_, rule0_.rule_configuration as rule_con7_7_1_, labels1_.rule_id as rule_id1_8_3_, labels1_.label_id as label_id2_8_3_, labels1_.rule_id as rule_id1_8_0_, labels1_.label_id as label_id2_8_0_ from rule rule0_ left outer join rule_label labels1_ on rule0_.id=labels1_.rule_id where rule0_.id=?
and StackOverflowError as a result
java.lang.StackOverflowError: null
at com.mysql.jdbc.ServerPreparedStatement.getInstance(ServerPreparedStatement.java:332)
...
(LabelRepository acts in the same manner)
How it can be fixed?
Update:
After changing fetch strategy to Lazy
#Id
#ManyToOne(fetch = FetchType.LAZY)
private Rule rule;
#Id
#ManyToOne(fetch = FetchType.LAZY)
private Label label;
infinite loop problem has gone, but new one has appeared - related entities are not being populated and when Hibernate is trying to insert values into link table
Hibernate: insert into rule_label (rule_id, label_id) values (?, ?)
we get
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
...
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'rule_id' cannot be null
Okay, well I've always used an EmbeddableId for link entities with JPA. I haven't tried the hibernate example you refer to in terms of using cascade to do the work for me. It could be interesting but there are some differences between pure JPA and Spring Data Repositories. By using an EmbeddableId you can create a separate spring repository for the link entity. Then you manage the relationships yourself. If you don't want to do that then you should use a ManyToMany annotation, but the link entity allows you to create link entity attributes, not shown here. This code will work for you and get you to point B and you can experiment from there:
#Entity
public class Label {
#Id #GeneratedValue private Long id;
#OneToMany(mappedBy = "ruleLabelId.labelId")
private List<RuleLabel> rules = new ArrayList<>();
#Entity
public class Rule {
#Id #GeneratedValue private Long id;
#OneToMany(mappedBy = "ruleLabelId.ruleId")
private List<RuleLabel> labels = new ArrayList<>();
#Entity
public class RuleLabel {
#EmbeddedId
private RuleLabelId ruleLabelId;
#SuppressWarnings("serial")
#Embeddable
public class RuleLabelId implements Serializable {
private Long ruleId;
private Long labelId;
public interface RuleRepository extends JpaRepository<Rule, Long> {
#Query("from Rule r left join fetch r.labels where r.id = :id")
public Rule getWithLabels(#Param("id") Long id);
}
public interface RuleLabelRepository extends JpaRepository<RuleLabel, RuleLabelId> {}
and to use it:
Rule rule = new Rule();
Label label = new Label();
ruleRepo.save(rule);
labelRepo.save(label);
RuleLabel ruleLabel = new RuleLabel();
RuleLabelId ruleLabelId = new RuleLabelId();
ruleLabelId.setRuleId(rule.getId());
ruleLabelId.setLabelId(label.getId());
ruleLabel.setRuleLabelId(ruleLabelId);
ruleLabelRepo.save(ruleLabel);
rule = ruleRepo.getWithLabels(1L);
System.out.println(rule + Arrays.toString(rule.getLabels().toArray()));
Yes because its what you are telling hibernate to do.
By default, all #ManyToOne and #OneToOne associations are EAGER loaded, so when it querying Rule then its also querying RuleLabel and then inside there is Rule again which is causing infinite select queries. It's better to have them LAZY loaded.
You can do field lazy load like this #ManyToOne(fetch=FetchType.LAZY)
This is what JPA 2.0 spec say about defaults:
OneToMany: LAZY
ManyToOne: EAGER
ManyToMany: LAZY
OneToOne: EAGER
A good read on Lazy and Eager loading

Hibernate one to many automatically insert id

So i have a very basic construction where i have a client and this client could have multiple addresses.
So in hibernate i did something like this
#Entity
#Table(name ="tbl_clients")
#Access(value = AccessType.FIELD)
public class Client {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_client")
private Integer id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "fkIdClientAddress", cascade = CascadeType.ALL)
private List<AddressClient> addressClientList = new ArrayList<>();
And the other class looks like this :
#Entity
#Table(name ="tbl_clients_address")
#Access(value = AccessType.FIELD)
public class AddressClient {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_client_address")
private Integer id;
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="id_client")
private Client Client;
#Column
private Integer fkIdClientAddress;
When inserting a client into the database who has 2 addresses it works but the fields fkIdClientAddress and id_client is are empty in the database. So i have no idea to who the address belong.
How can i fix this? And what is wrong with this construction?
first improvements
Class AddressClient
#Entity
#Table(name ="tbl_clients_address")
#Access(value = AccessType.FIELD)
public class AddressClient {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_client_address")
private Integer id;
#ManyToOne
#JoinColumn(name="id_client")
private Client client;
Class Client
#Entity
#Table(name ="tbl_clients")
#Access(value = AccessType.FIELD)
public class Client {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_client")
private Integer id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.ALL)
private List<AddressClient> addressClientList = new ArrayList<>();
This is looking better but the field id_client is still null
When i created a for each and save the AddressClients again the id is successful saved.
#RequestMapping(value = "/addclient",method = RequestMethod.POST)
public void addClient(#AuthenticationPrincipal Principal user,#RequestBody Client client) {
//FIND THE ACTIVE USER
LOGGER.info("SQL: GET PRINCEPAL USER");
User getuser = userDao.findByEmail(user.getName());
for (AddressClient addressClient : client.getAddressClientList())
{
addressClient.setClient(client);
}
clientDao.save(client);
}
Your mapping is wrong. First of all, you don't need two different columns (id_client and fkIdClientAddress) to know that a given address belongs to a given client. So the first thing to do is to remove fkIdClientAddress.
Then the mappedBy attribute in OneToMany is usd to tell Hibernate which field in address represents the owning ManyToOne association. So it must be set to Client. (or, if you respect the Java naming conventions and rename the field to client, it must be set to client).
Finally, having cascade=ALL on a ManyToOne doesn't make much sense: you don't want to delete the client when you delete one of its addresses. That would fail anyway, since other addresses would still reference the client.
Your mappings are wrong. When you define the collection in Client class, you should indicate the field in the AddressClient class which points back to the Client and that is the field client.
#OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.ALL)
private List<AddressClient> addressClientList = new ArrayList<>();
And in ClientAddress class you have:
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="id_client")
private Client client;
You do not need the field fkIdClientAddress
In some cases, you still can keep column
#Column
private Integer fkIdClientAddress;
But you have to set data for this column instead of setting value for
private Client client;
But this approach is not appropriate.

Categories