Hibernate OneToMany mappedby inserts duplicate records when saved - java

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

Related

Validation of database integrity with Hibernate validator

Hibernate validator works well for me to validate objects fetched by hibernate, but the problem is that I would like to make sure that certain conditions are met after persisting/updating objects in database. For example:
My condition is: User can host at most 3 games
Constraint annotation:
#Target({ FIELD, TYPE })
#Retention(RUNTIME)
#Constraint(validatedBy = GamesCountValidator.class)
#Documented
public #interface ConstrainHostGamesCount {
String message() default "{com.afrixs.mygameserver.db.constraint.ConstrainHostGamesCount.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Validator:
public class GamesCountValidator implements ConstraintValidator<ConstrainHostGamesCount, User> {
#Override
public void initialize(ConstrainHostGamesCount constraintAnnotation) {
}
#Override
public boolean isValid(User user, ConstraintValidatorContext context) {
if (user == null)
return true;
return user.getGames().size() <= 3;
}
}
User class:
#Entity
#Table(name="Users")
#ConstrainHostGamesCount
public class User {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id", nullable=false, unique=true, length=11)
private int id;
#Column(name="name", length=30, unique=true)
private String name;
#OneToMany(mappedBy = "user", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<Game> games = new HashSet<>();
//generic getters and setters
}
Game class:
#Entity
#Table(name="Games")
public class Game {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id", nullable=false, unique=true, length=11)
private int id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="user_id")
#ConstrainHostGamesCount
private User user;
//generic getters and setters
}
Test method:
public class Test {
public static void hostGames(String userName, int count) {
try {
Session session = DatabaseManager.getSessionFactory().getCurrentSession();
session.beginTransaction();
Query userQuery = session.createQuery("from User where name = :name");
userQuery.setParameter("name", name);
User user = (User)userQuery.uniqueResult();
for (int i = 0; i < count; i++) {
Game = new Game();
game.setUser(user);
session.persist(game);
}
session.getTransaction().commit();
} catch (Exception e) {
DatabaseManager.getSessionFactory().getCurrentSession().getTransaction().rollback();
e.printStackTrace();
}
}
}
Desired behavior for Test.hostGames("afrixs", 4) would be to fail. However the validator validates the state of the user object before the update, ie. with games.size() equal to 0, so the constraint condition is met and nothing fails until Test.hostGames("afrixs", 4) is called for the second time. Of course in this situation we could manually add games to user user.getGames().add(game) but this attitude is error prone (the game needs to be added to user this way everywhere in the code) and it doesn't solve the problem if for example two Test.hostGames("afrixs", 2) are called asynchronously.
So my question is: what is the proper way of constraining the database integrity using hibernate? Is there a way to make the validator check the final state of objects after storing them into database? Or do I need to do the constraining manually (like performing another transaction after session.getTransaction().commit and check the conditions and roll back the updating transaction if they are not met)? Or should I leave out hibernate and use SQL triggers for this? Thank you for your answers, they will help a lot
And here is my current hibernate validation configuration:
<property name="javax.persistence.validation.group.pre-persist">javax.validation.groups.Default</property>
<property name="javax.persistence.validation.group.pre-update">javax.validation.groups.Default</property>
<property name="hbm2ddl.auto">validate</property>
Ok, I have made some experiments, writing down a small test class. To make things simple I changed the constraint to "User can host at most 1 game".
public class DBTest {
#Test
public void gamesCountConstraintWorking() {
DBManager.deleteHostedGames("afrixs");
boolean ok1 = DBManager.createOneGame("afrixs");
boolean ok2 = DBManager.createOneGame("afrixs");
int gamesCount = DBManager.deleteHostedGames("afrixs");
System.out.println("Sync test: count: "+gamesCount+", ok1: "+ok1+", ok2: "+ok2);
assertTrue(gamesCount <= 1);
assertTrue(!(ok1 && ok2));
}
#Test
public void gamesCountConstraintWorkingAsync() throws InterruptedException {
DBManager.deleteHostedGames("afrixs");
for (int i = 0; i < 30; i++) {
CreateOneGameRunnable r1 = new CreateOneGameRunnable(1);
CreateOneGameRunnable r2 = new CreateOneGameRunnable(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
int maxCount = 0;
while (r1.running || r2.running) {
int count = DBManager.selectHostedGamesCount("afrixs");
System.out.println("count: "+count);
maxCount = Math.max(maxCount, count);
}
t1.join();
t2.join();
int gamesCount = DBManager.deleteHostedGames("afrixs");
System.out.println("Async test: count: "+gamesCount+", maxCount: "+maxCount+", ok1: "+r1.ok+", ok2: "+r2.ok);
assertTrue(maxCount <= 1 && gamesCount <= 1);
assertTrue(!(r1.ok && r2.ok));
}
}
private class CreateOneGameRunnable implements Runnable {
public boolean ok;
public boolean running = true;
private int number;
CreateOneGameRunnable(int number) {
this.number = number;
}
#Override
public void run() {
System.out.println("Starting "+number);
ok = DBManager.createOneGame("afrixs");
System.out.println("Finished "+number);
running = false;
}
}
}
First I tried out #Guillaume's suggestion to use user.getGames().add(game); along with game.setUser(user); when assigning the relation. gamesCountConstraintWorking test was successful, however, gamesCountConstraintWorkingAsync wasn't. It means that this attitude was successful in maintaining the session consistency (at the cost of fetching all user games), however, the database integrity wasn't maintained.
A solution that actually worked for both tests was (as #OrangeDog suggested) to add the constraint directly into database schema. MySQL:
DELIMITER $$
CREATE TRIGGER check_user_games_count
AFTER INSERT
ON Games FOR EACH ROW
BEGIN
DECLARE gamesCount INT;
SET gamesCount = (SELECT COUNT(id) FROM Games WHERE user_id = new.user_id);
IF gamesCount > 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'User may host at most 1 game';
END IF;
END $$
DELIMITER ;
So my summary is that Hibernate works great as a layer above the database to work with, but if you want to make sure the persisted data look like you want, you need to dive directly into your database schema and perform actions in there. (But that's only the result of this experiment, maybe someone knows a solution for this using Hibernate)
Note: I tried the tests with BEFORE UPDATE triggers and random delays inside the triggers and the tests were successful as well, it seems like some kind of lock is acquired for the table while inserting, so yes, this is a safe solution. (Note2: BEFORE UPDATE trigger for this needs gamesCount+1 > 1 condition and the constraint could fail (not tested) in the case of inserting multiple rows in one query)

How to delete Entity Type with Xodus?

Here's my code to delete all entities for a given type:
#Override
public boolean deleteEntities(String instance, final String storeName) {
final boolean[] success = {false};
final PersistentEntityStore entityStore = manager.getPersistentEntityStore(xodusRoot, instance);
try {
entityStore.executeInTransaction(new StoreTransactionalExecutable() {
#Override
public void execute(#NotNull final StoreTransaction txn) {
EntityIterable result = txn.getAll(storeName);
final boolean[] hasError = {false};
for(Entity entity : result) {
if(!entity.delete()) {
hasError[0] = true;
}
}
success[0] = !hasError[0];
}
});
} finally {
////entityStore.close();
}
return success[0];
}
Question:
Is this the right approach to delete all existing entities for a given entity type?
When this method is executed, all entities are indeed removed but the Enity type is sitll there, how to properly delete a enity type?
There is PersistentEntityStore#renameEntityType to rename entity type as part of a public api. To delete entity type at all you can use PersistentEntityStoreImpl#deleteEntityType. It's not a part of PersistentEntityStore api but method is public and you can use it.
Also when you deleting entity type do not forget that you also need to clear all links points to entities of this type.

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

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.

Generic querydsl orderBy dynamic path generation with left joins

I've run into a problem while using JPA with Querydsl and Hibernate for data storage management. The sample model is as follows:
#Entity
public class User {
....
#ManyToOne
#JoinColumn(name = "CATEGORY_ID")
private Category category;
}
#Entity
public class Category {
..
private String acronym;
#OneToMany(mappedBy = "category")
List<User> userList;
}
In my Spring MVC webapp I have a search form with User parameters and orderBy select. The orderBy select can be either User property or Category property. The orderBy parameters are stored as Map (f.e. {"login" : "adm", {"firstName" : "John"}. The search function receives the search parameters (as string) and the map above with order specification. The simplified code for ordering is as follows:
Map<String, String> orderByMap = new HashMap<String, String>();
orderByMap.put("firstName", "asc");
orderByMap.put("unit.acronym", "desc");
PathBuilder<User> pbu = new PathBuilder<User>(User.class, "user");
....
for (Map.Entry<String, String> order : orderByMap.entrySet())
{
// for simplicity I've omitted asc/desc chooser
query.orderBy(pbu.getString(order.getKey()).asc());
}
The problem starts when I want to introduce sorting by Category's parameter, like {"category.acronym", "desc"}. As explained here, the above code will make querydsl to use cross join with Category table and omitt the Users without Categories, which is not expected behavior.
I know, I have to introduce the left join with Categories and use the alias for the sorting to make it work, hovewer I'm looking for efficient way to do it dynamically. Stripping each String looking for category or any other entity (like "user.category.subcategory.propetry") will introduce a lot of ugly code and I'd rather not do that.
I'd appreciate the help with some more elegant solution.
I added now a protoype of the implementation to the test side of Querydsl https://github.com/mysema/querydsl/issues/582
I will consider a direct integration into Querydsl if this a common use case
public class OrderHelper {
private static final Pattern DOT = Pattern.compile("\\.");
public static PathBuilder<?> join(JPACommonQuery<?> query, PathBuilder<?> builder, Map<String, PathBuilder<?>> joins, String path) {
PathBuilder<?> rv = joins.get(path);
if (rv == null) {
if (path.contains(".")) {
String[] tokens = DOT.split(path);
String[] parent = new String[tokens.length - 1];
System.arraycopy(tokens, 0, parent, 0, tokens.length - 1);
String parentKey = StringUtils.join(parent, ".");
builder = join(query, builder, joins, parentKey);
rv = new PathBuilder(Object.class, StringUtils.join(tokens, "_"));
query.leftJoin((EntityPath)builder.get(tokens[tokens.length - 1]), rv);
} else {
rv = new PathBuilder(Object.class, path);
query.leftJoin((EntityPath)builder.get(path), rv);
}
joins.put(path, rv);
}
return rv;
}
public static void orderBy(JPACommonQuery<?> query, EntityPath<?> entity, List<String> order) {
PathBuilder<?> builder = new PathBuilder(entity.getType(), entity.getMetadata());
Map<String, PathBuilder<?>> joins = Maps.newHashMap();
for (String entry : order) {
String[] tokens = DOT.split(entry);
if (tokens.length > 1) {
String[] parent = new String[tokens.length - 1];
System.arraycopy(tokens, 0, parent, 0, tokens.length - 1);
PathBuilder<?> parentAlias = join(query, builder, joins, StringUtils.join(parent, "."));
query.orderBy(parentAlias.getString(tokens[tokens.length - 1]).asc());
} else {
query.orderBy(builder.getString(tokens[0]).asc());
}
}
}
}

JPA deleting bidirectional association from inverse side

In my domain model there are a lot of bidirectional associations (both OneToMany and ManyToMany)
I've read this article and made all my associations on the basis of the sample pattern. (the ManyToMany associations has a two-sided addXY methods, following the pattern)
Using the pattern in this article the question is, what about deleting from the inverse side?
Example:
public class Customer implements Serializable {
...
#ManyToOne()
private CustomerStatus customerStatus;
#PreRemove
public void preRemove(){
setCustomerStatus(null);
}
public void setCustomerStatus(CustomerStatus customerStatus) {
if(this.customerStatus != null) { this.customerStatus.internalRemoveCustomer(this); }
this.customerStatus = customerStatus;
if(customerStatus != null) { customerStatus.internalAddCustomer(this); }
}
On the other side:
public class CustomerStatus implements Serializable {
private static final long serialVersionUID = 1L;
#OneToMany(mappedBy="customerStatus")
private List<Customer> customers;
#PreRemove
public void preRemove(){
for(Customer c : customers){
c.setCustomerStatus(null); // this causes ConcurrentException
}
}
public List<Customer> getCustomers() {
return Collections.unmodifiableList(this.customers);
}
public void addCustomer(Customer c){
c.setCustomerStatus(this);
}
public void removeCustomer(Customer c){
c.setCustomerStatus(null);
}
void internalAddCustomer(Customer c){
this.customers.add(c);
}
void internalRemoveCustomer(Customer c){
this.customers.remove(c);
}
The problem is, that the preRemove method causes ConcurrentException. How to handle this?
The goal is, to delete the CustomerStatus, and set NULL all the Customers, where there was that status.
UPDATE
Without the preRemove method, I've got MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails
You cannot call this.customers.remove(c) while you are iterating over the customer collection. This question has come up before so you may find other solutions as in here:
How to avoid ConcurrentModificationException when iterating over a map and changing values?
but a simple solution is to just create a new list from the old to iterate over on preRemove:
public void preRemove(){
List<Customer> tempList = new ArrayList(customers);
for(Customer c : tempList){
c.setCustomerStatus(null);
}
}

Categories

Resources