JPA: How to update grandchild entities when updating grandfather entity? - java

I've created tables to store information from parsing xml files that are very nested. Therefore the tables in my database are nested too. The top level entity is called SpecimenTb.
#Entity
#Table(name="SPECIMEN_TB")
public class SpecimenTb implements Serializable {
private String mrn;
#Column(name="SPECIMEN_NO", unique = true)
private String specimenNo;
#OneToMany(mappedBy="specimenTb", cascade=CascadeType.ALL)
private List<FmReportTb> fmReportTbs;
}
SpecimenTb has a child entity called FmReportTb which has its own onetomany relationship to FmReportGeneTb
#Entity
#Table(name="FM_REPORT_TB")
public class FmReportTb implements Serializable {
//bi-directional many-to-one association to SpecimenTb
#ManyToOne
#JoinColumn(name="SPECIMEN_ID")
private SpecimenTb specimenTb;
#OneToMany(mappedBy="fmReportTb", cascade=CascadeType.ALL)
private List<FmReportGeneTb> fmReportGeneTbs;
}
It is possible a newer version of file will come at a later time, so I need to implement update scheme in my code. When I persist, I look for record in SpecimenTb by specimenNo. If it does not exist, insert new record. Otherwise, update the same records, father, children and grandkids.
SpecimenDao specimenDao = new SpecimenDao(em);
SpecimenTb specimenTb = specimenDao.findSpecimenBySpecno(blockId);
FmReportTb report = null;
if (specimenTb != null) { // update
report = specimenTb.getFmReportTbs().get(0);
} else { // insert
report = new FmReportTb();
}
if (report.getFmReportGeneTbs() != null) { // update
geneList = report.getFmReportGeneTbs();
geneList.clear();
} else { //insert
geneList = new ArrayList<FmReportGeneTb>();
}
// parse "Genes" section of xml and store in geneList
for (Node gene : genes) {
FmReportGeneTb geneTb = new FmReportGeneTb();
<< set a bunch of stuff for geneTb>>
geneTb.setFmReportTb(report);
}
geneList.add(geneTb);
report.setFmReportGeneTb(geneList);
if (specimenTb == null) { // insert new record
specimenTb = new SpecimenTb();
specimenTb.setSpecimenNo(blockId);
specimenTb.setMrn(mrn);
report.setSpecimenTb(specimenTb);
reports.add(report);
specimenTb.setFmReportTbs(reports);
specimenDao.persistSpecimen(specimenTb);
} else { // update
report.setSpecimenTb(specimenTb);
reports.add(report);
specimenDao.updateSpecimen(specimenTb, mrn, reports);
}
In the DAO class, persist and update methods are as follows:
// insert
public void persistSpecimen(SpecimenTb specimen) {
EntityTransaction transaction = entityManager.getTransaction();
try {
transaction.begin();
entityManager.persist(specimen);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
}
}
// update
public void updateSpecimen(SpecimenTb specimen, String mrn, List<FmReportTb> reports) {
EntityTransaction transaction = entityManager.getTransaction();
try {
transaction.begin();
specimen.setSpecimenNo(blockId);
specimen.setMrn(mrn);
specimen.setFmReportTbs(reports);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
}
}
Insert works exactly what I wanted, it persists father entity and all entities underneath thanks to cascade=CascadeType.ALL. When I update though, it does update the old record in SpecimenTb and FmReportTb but tends to insert a new series of records in FmReportGeneTb. So I ended up appending a new set of records in FmReportGeneTb that is linked to the same FmReportTb instead of updating (GeneList in old and new versions may differ in length). There are actually child entities and grandchild entities of FmReportGeneTb that I did not show. How do I update all related tables from top down?

I don't know what other fields do you set on FmReportTb, but seen your code, on the first line there is a
FmReportTb report = new FmReportTb();
That explain why you ended having two records of FmReportTb in your specimen.
If you want to update the existing FmReportTb you should set its id before update it.

Related

Spring data - "ERROR: current transaction is aborted": how to rollback/commit transaction when DataIntegrityViolationException is catched?

If I have some instances to insert or update in DB, like:
#Data
#NoArgsConstructor
#AllArgsConstructor
#Entity
#EqualsAndHashCode
#Table(name="user_purchases")
public class UserPurchase implements Serializable, Persistable<String> {
#Id
#Column(name="id")
#NotNull
private String id; // an UUID
#Column(name="user_id")
#NotNull
private String userId; // in user_info: "sub"
/**
* Seconds since epoch: when the last purchase happened.
*/
#Column(name="last_date")
private Date lastBuyDate;
#Transient
private boolean isNewObject;
// Add UUID before persisting
#PrePersist
public void prepareForInsert() {
this.id = UUID.randomUUID().toString();
}
#Override
public boolean isNew() {
return isNewObject;
}
// This part for Persistable is not required, because getId(), by accident,
// is the getter for the "id" field and returns a String.
//#Override
//public getId() {
// return id;
//}
}
We know that id is surrogate id and will be generated before persisting. And userId is unique in DB.
To know more about the interface Persistable<ID>, check this answer.
Now, when we have an instance without id, the userId could or not be duplicated in DB, and there is no way to tell if we are persisting or updating in DB.
I want to save a full-table scanning before every persisting/updating, so I am trying to catch DateIntegrationViolationException after the first try of repository.save(entity).
#Transactional(rollbackFor = DataIntegrityViolationException.class)
public UserPurchase saveUserPurchase(UserPurchase purchase) throws RuntimeException {
UserPurchase saved = null;
try {
saved = repository.saveAndFlush(purchase);
log.debug("UserPurchase was saved/updated with id {}, last buy time: {}", saved.getId(),
DateTimeUtil.formatDateWithMilliPart(saved.getLastBuyDate(), false));
} catch (DataIntegrityViolationException e) {
log.info("Cannot save due to duplication. Rolling back..."); // we don't distinguish userId and id duplication here.
UserPurchase oldPurchase = repository.findByUserId(purchase.getUserId()); // <--------- here we cannot proceed
if (oldPurchase != null) {
purchase.setId(oldPurchase.getId()); // set the existent ID to do updating
purchase.setNewObject(false); // for Persistable<String>
saved = repository.saveAndFlush(purchase); // now should be updating
} else {
log.error("Cannot find ID by user id");
}
}
return saved;
}
This gives me the error of:
ERROR: current transaction is aborted, commands ignored until end of transaction
Because I am doing two things in one transaction, where the transaction shoule be rolled back.
OK, so I throw the exception, and tried to do the operation of updating outside(because Spring shall rollback automatically when it sees an exception is thrown, or so I read):
#Transactional(rollbackFor = DataIntegrityViolationException.class)
public UserPurchase saveUserPurchase(UserPurchase purchase) throws RuntimeException {
UserPurchase saved = null;
try {
saved = repository.saveAndFlush(purchase);
log.debug("UserPurchase was saved/updated with id {}, last buy time: {}", saved.getId(),
DateTimeUtil.formatDateWithMilliPart(saved.getLastBuyDate(), false));
} catch (DataIntegrityViolationException e) {
log.info("Cannot save due to duplication. Rolling back..."); // we don't distinguish userId and id duplication here.
throw e; // or throw new RuntimeException(e); is the same
}
return saved;
}
At where I call save():
try {
userPurchaseService.saveUserPurchase(purchase);
} catch (DataIntegrityViolationException e) {
log.info("Transaction rolled back, updating...");
// ... 1. select 2. getId() 3.setId() and save again
}
But, it fails again.
Now, with Spring Data, we have no EntityManager to rollback().
What to do now? Must I do a manual findByUserId() before every insert/update? My approach of lazy-selection wouldn't work under any circumstance?

Realm RecyclerAdapter returns single item

I am fetching data from json api I have set PrimaryKey as state. This is the Json data I am fetching:
records[
  {
         "id":"192693681",
         "timestamp":"1500204608",
         "state":"Rajasthan",
         "district":"Rajasamand",
         "market":"Rajasamand",
         "commodity":"Sugar",
         "variety":"Shakkar",
         "arrival_date":"16/07/2017",
         "min_price":"4000",
         "max_price":"4100",
         "modal_price":"4050"
      },
      {
         "id":"192693701",
         "timestamp":"1500204608",
         "state":"Rajasthan",
         "district":"Rajasamand",
         "market":"Rajasamand",
         "commodity":"Wheat",
         "variety":"Farmi",
         "arrival_date":"16/07/2017",
         "min_price":"1600",
         "max_price":"1650",
         "modal_price":"1625"
      },
      {
         "id":"192693721",
         "timestamp":"1500204608",
         "state":"Rajasthan",
         "district":"Rajasamand",
         "market":"Rajasamand",
         "commodity":"Wheat",
         "variety":"Lokwan",
         "arrival_date":"16/07/2017",
         "min_price":"1550",
         "max_price":"1600",
         "modal_price":"1575"
      }
   ]
This is my query to return the market data for every state:
private void populateMarketData(String state){
cityAdapter = new CityAdapter(mRealm.where(Records.class).equalTo(Constants.STATE, state).findAll(), this);
cityRecyclerView.setAdapter(cityAdapter);
Log.d(TAG, "Total Cities" + String.valueOf(cityAdapter.getData().size()));
cityAdapter.notifyDataSetChanged();
}
This is my query to return all commodities for the market with the states:
#Override
public void onMarketClicked(String cityName) {
tradeAdapter = new TradeAdapter(mRealm.where(Records.class)
.equalTo(Constants.MARKET, cityName).findAll(), this);
tradeRecyclerView.setAdapter(tradeAdapter);
}
This is GcmTaskService to update data in background service:
Realm.init(mContext);
List<Records> records = new MandiDataHandler().getMandiQuotes(url);
SaveMandi saveMandi = new SaveMandi(state, records);
Realm realm = new RealmController(getApplication()).getRealm();
realm.beginTransaction();
realm.copyToRealmOrUpdate(saveMandi);
realm.commitTransaction();
realm.close();
This is DataHelper to save the data from Json API
#PrimaryKey
private String state;
private RealmList<Records> recordsRealmList = new RealmList<>();
public SaveMandi(){}
public SaveMandi(String state, List<Records> recordsList) {
try {
this.state = state;
for (Records records: recordsList ) {
records = new Records(records.getId(), DateUtils.getRelativeTimeSpanString(
Long.parseLong(records.getTimestamp())).toString(), records.getState(), records.getMarket(),
records.getCommodity(), records.getVariety(), records.getMin_price(), records.getMax_price());
this.recordsRealmList.add(records);
}
}catch (Exception e){
e.printStackTrace();
}
}
The problem in my RealmRecyclerView it either returns single item when I set the PrimaryKey to state, else when I set the PrimaryKey to id it returns multiple duplicate data. I am not sure about where I might be wrong.
You want to choose your PrimaryKey to be something that will only match when they are (what you deem as) "equal", much like an equals() method. If you put two RealmObjects with the same PrimaryKey into Realm only one of them will show up in Realm since there can only be one object for each distinct PrimaryKey.
For example if I have a RealmObject a with a PrimaryKey of 1 in realm and I put another RealmObject b with a PrimaryKey of 1 into Realm, a will be deleted with b since there can only be one object for every PrimaryKey.
A good tool to look through your Realm databases is Stetho, I highly recommend it for debugging applications using Realm.

put in hashmap using two lists and database

I have two classes, User and Customer, using hibernate, I have mapped these tables, now I want to retrieve the data from the database, where I can put the values in hashmap such that for each user, if a particular customer exists in the mapping table, then the key of map is set as the customer name, and value to true or false, depending upon the mapping table.
I have retrieved both the lists:
static List<User> listUsers = new ArrayList<User>();
static List<Customer> listCustomers = new ArrayList<Customer>();
List<UserTO> list = new ArrayList<UserTO>();
public static List<Customer> getListOfCustomers() {
HibernateUtil.openSession();
Session session = HibernateUtil.getSession();
Transaction tx = null;
try {
tx = session.getTransaction();
tx.begin();
listCustomers = session.createQuery("from Customer").list();
tx.commit();
} catch (Exception e) {
if (tx != null) {
tx.rollback();
}
e.printStackTrace();
} finally {
session.close();
}
return listCustomers;
}
(similarly list of users)
in UserTO class, I have:
public class UserTO implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
private Long id;
private String userId;
private Map<String, Boolean> map = new HashMap<String, Boolean>();
(getter and setter)
I tried doing this:
public static void execute() {
getListOfUsers();
getListOfCustomers();
for (User user : listUsers) {
UserTO u = new UserTO();
Map<String, Boolean> map = new HashMap<>();
for (Customer customer : listCustomers) {
if (customer.getCompanyName() == user.getCustomers(customer)) {
map.put(customer.getCompanyName(), true);
} else {
map.put(customer.getCompanyName(), false);
}
}
user.getUserId();
u.setUserId(user.getUserId());
u.setMap(map);
listUsers.add(user);
}
}
which gives me Concurrent Modification Exception
I don't know where I am doing wrong, and what should I do next.
you are adding user to listUsers when you are iterating over this listuser. This results in the given exception.
Use list.add(u);
Reason for ConcurrentModificationException is you are trying to add to the list at the same time when you are iterating like:
for (User user : listUsers) {
....
listUsers.add(user);
One way to solve this would be to create a temporary list and keep adding to that list and after your for loop, use addAll method on listUsers to add all your users that you added in the loop.
Note: Inorder to select the data, you don't need transaction as select wont do any side effect to your table.
What you are trying to do is that you are getting a User object from the list, and then again adding it to the list. It wont work for the following reasons:
You are iterating the list and adding in the same loop which is not allowed and throws ConcurrentModificationException.
Also you are adding in every iteration. Which means that the list will grow with every iteration and your loop will never end. First you should remove the object and then again add it in the same place.
Solution:
public static void execute() {
getListOfUsers();
getListOfCustomers();
for (int i=0;i<listUsers.size();i++) {
User user = listUsers.remove(i);
UserTO u = new UserTO();
Map<String, Boolean> map = new HashMap<>();
for (Customer customer : listCustomers) {
if (customer.getCompanyName() == user.getCustomers(customer)) {
map.put(customer.getCompanyName(), true);
} else {
map.put(customer.getCompanyName(), false);
}
}
user.getUserId();
u.setUserId(user.getUserId());
u.setMap(map);
listUsers.add(i,user);
}
}
P.S. - But still I don't get that why there is a need to add an object which already exists in the list without any change.
You are getting concurrent modification exception as HashMap is not thread safe. Use concurrentHashMap instead. Thanks hope it helps.

Hibernate OneToMany mappedby inserts duplicate records when saved

I have a simple oneToMany relationship provided in Parent and corresponding ManyToOne in the Chile Entity class:
Parent:
#Entity
#Table(name = "FormExtraInfo")
#PrimaryKeyJoinColumn(name="form_container_id")
public class Form extends Container {
private List<Reason> reasons = new ArrayList<Reason>();
#OneToMany(mappedBy="form",cascade={javax.persistence.CascadeType.ALL},orphanRemoval=true)
#Cascade(value={CascadeType.ALL})
public List<Reason> getReasons() {
return reasons;
}
public void setReasons(List<Reason> reasons) {
this.reasons = reasons;
}
public void addReason(Reason reason) {
if (this.reasons == null) {
this.reasons = new ArrayList<Reason>();
}
this.reasons.add(reason);
}
}
Child class:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="Container_id")
public Form getForm() {
return form;
}
public void setForm(Form form) {
this.form = form;
}
Action class:
//Set the reasons
String[] reasonStatus = strutsForm.getMultiValueProperty(REASON_STATUS);
String[] reasonText = strutsForm.getMultiValueProperty(REASON_TEXT);
List<Reason> reasons = new ArrayList<Reason>();
logger.debug("form container ID : " + form.getId() +". # of Reasons for this form: "+ reasonText.length);
for (int i = 0; i < reasonText.length; i++) {
Reason r = new Reason();
r.setComment(reasonText[i]);
r.setStatusTypeCode(reasonStatus[i]);
r.setForm(form);
reasons.add(r);
}
form.setReasons(reasons);
Example case:
Status_code Reason_text
abc abc1
xyz xyz1
save the form:
Status_code Reason_text
abc abc1
xyz xyz1
abc abc1
xyz xyz1
With any operation : New insert or delete or update, it first duplicates the old data to the DB and then the operation that I performed.
Try replacing the cascade clause for this
#Cascade (value={CascadeType.SAVE_UPDATE,CascadeType.DELETE_ORPHAN})
Take a look at my blog post on mapping one-to-many http://arecordon.blogspot.com.ar/2013/05/hibernate-mapping-associations-one-to_20.html
If you can use a Set instead of a List; then, try changing the collection to a Set and make sure you overwrite equals() hashCode() as specified in here:
https://community.jboss.org/wiki/EqualsAndHashCode?_sscc=t
Also, remove the duplicated cascaded, you ca use:
#OneToMany(mappedBy="form",cascade={javax.persistence.CascadeType.ALL},orphanRemoval=true)
or
#OneToMany(mappedBy="form")
#Cascade(value={CascadeType.ALL, org.hibernate.annotations.CascadeType.DELETE_ORPHAN})

A different object with the same identifier value was already associated with the session:. neither merge nor saveOrUpdate works

I have a GUI where a list of teachers is shown. Two are selected by the user - they are going to be the form teachers of a new school class that gets created.
The teacher - class relationship is n-m.
School class: (it inherits its id from its group)
#Entity
#Table(name="school_classes")
#Cacheable(true)
public class SchoolClass extends Group{
#ManyToMany(cascade=CascadeType.ALL, mappedBy="classes", fetch=FetchType.EAGER)
private SortedSet<Teacher> teachers;
Teacher:
#Entity
#Table(name="teachers")
#Cacheable(true)
public class Teacher extends creator.models.school.Entity{
// name etc
#ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinTable(name="class_teachers",
joinColumns = #JoinColumn(name="teacher_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="class_id", referencedColumnName="id"))
private SortedSet<SchoolClass> classes;
I try to create a new class like this:
String className = request.getParameter("class_name");
String id1 = request.getParameter("class_teacher1");
String id2 = request.getParameter("class_teacher2");
Teacher t1 = DataRetrieveModule.getTeacher(id1);
Teacher t2 = DataRetrieveModule.getTeacher(id2);
Layout l = new Layout();
SchoolClass newClass = new SchoolClass(className);
newClass.setLayout(l);
newClass.addTeacher(t1);
t1.addClass(newClass);
newClass.addTeacher(t2);
t2.addClass(newClass);
DataInsertionModule.insert(newClass);
This statement DataRetrieveModule.getTeacher(id1) opens a session, retrives the teacher by ID and closes it.
DataInsertionModule.insert(newClass) also opens a session, calls session.saveOrUpdate(newClass). (I also tried session.merge(newClass))
Retrieving a teacher:
public static Teacher getTeacher(String id) {
Session session = null;
Teacher t = null;
try{
sessionFactory = MyFactory.getSessionFactory();
session = sessionFactory.openSession();
t = (Teacher) session.get(Teacher.class, Long.parseLong(id));
}
catch(Exception e) {
System.out.println("in DAO:");
e.printStackTrace();
if(session!=null)
session.close();
}
finally{
if(session!=null)
session.close();
}
return t;
}
Data insertion:
public static void insert(Object o) {
Session session = null;
try
{
sessionFactory = MyFactory.getSessionFactory();
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(o);
tx.commit();
System.out.println("insertion done");
}
catch (Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(session!=null)
session.close();
}
}
But insertion never works.
There is always an object with the same id that already exists.
a different object with the same identifier value was already associated with the session: [creator.models.school.Teacher#3]
I searched on stackoverflow and have overridden my getHashCode and equals method in the class all my business objects inherit from:
#Override
public int compareTo(Entity o) {
if(this.id < o.getId())
return -1;
else if(this.id > o.getId())
return 1;
return this.getClass().getName().compareTo(o.getClass().getName());
}
#Override
public boolean equals(Object o) {
if(o instanceof Entity)
return this.compareTo((Entity)o)==0;
else return this.equals(o);
}
#Override
public int hashCode() {
char[] bla = this.getClass().getName().toCharArray();
int blue=0;
for(char c:bla)
blue = blue*10 + c;
return (int) (id+blue);
}
Furthermore I tried to change the CascadeType of ManyToMany to MERGE (reverted again).
At the moment I merge the teacher objects after retrieving both of them with session.get(..), as retrieving takes a lot of data out of the DB. There are futher ..ToMany relations. Therefore it probably might happen, that the call causes that both teachers are loaded.
public static Object merge(Object o) {
Session session = null;
Object returnVal = null;
try
{
sessionFactory = MyFactory.getSessionFactory();
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
returnVal = session.merge(o);
tx.commit();
}
catch (Exception e)
{
System.out.println(e.getMessage());
e.printStackTrace();
}
finally
{
if(session!=null)
session.close();
}
return returnVal;
}
.
Teacher t1 = DataRetrieveModule.getTeacher(id1);
Teacher t2 = DataRetrieveModule.getTeacher(id2);
t1= DataInsertionModule.merge(t1);
t2= DataInsertionModule.merge(t2);
Therefore I thought, that if I merge the one my get-method returned with the one that must have been loaded by the get call for the other teacher, it should work. (like here: http://www.stevideter.com/2008/12/07/saveorupdate-versus-merge-in-hibernate/) But it does not :(
Is it probably because an object of the same superclass (Object or my Entity class) has the same ID?
Please help me!
One potential situation that could lead to this error is when the id of teacher 1 and teacher 2 is the same. Since you close the session between each get they will be detached, and you would end up loading two different objects that both represent the same row in the database (they have the same primary key.) Then, when these are both going to be reattached to a session through a SchoolClass being saved, Hibernate will see two different objects both representing the same row in the database, and not know which one represent the correct state to persist to the database.
Don't know for sure if this is the case here, but you should run a debugger and check the state of the objects referenced by teacher1 and teacher2.

Categories

Resources