JPA many-to-many relationship, keeping a list of ids - java

I have 2 POJO classes in Java: Phrase and Tag, in a many-to-many relationship:
Phrase.java
#Entity
#EntityListeners(value={PhraseListener.class})
public class Phrase {
#Id
#GeneratedValue
#Column(name="id")
private Long phraseId;
#Column(nullable=false)
private String text;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(name="phrase_has_tag",
joinColumns={#JoinColumn(name="phrase_id",referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="tag_uname",referencedColumnName="uname")})
private Collection<Tag> tagObjects;
#Transient
private Set<String> tags;
public Phrase() {
tagObjects = new ArrayList<Tag>();
tags = new HashSet<String>();
}
// getters and setters
// …
public void addTagObject(Tag t) {
if (!getTagObjects().contains(t)) {
getTagObjects().add(t);
}
if (!t.getPhrases().contains(this)) {
t.getPhrases().add(this);
}
}
public void addTag(String tagName) {
if (!getTags().contains(tagName)) {
getTags().add(tagName);
}
}
Tag.java
#Entity
public class Tag {
#Id
#Column(name="uname")
private String uniqueName;
private String description;
#ManyToMany(mappedBy="tagObjects")
private Collection<Phrase> phrases;
public Tag() {
phrases = new ArrayList<Phrase>();
}
// getters and setters
// …
The primary key for the tag entity is its name. I want to keep in Phrase.java a Set of tag names "synchronized" with the tagObjects field of the many-to-many relationship, and viceversa. For doing this, I add a listener to Phrase.java:
public class PhraseListener {
#PostLoad
public void postLoad(Phrase p) {
System.out.println("In post load");
for (Tag tag : p.getTagObjects()) {
p.addTag(tag.getUniqueName());
}
}
#PrePersist
public void prePersist(Phrase p) {
System.out.println("In pre persist");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("TestJPA");
EntityManager em = emf.createEntityManager();
for (String tagName : p.getTags()) {
Tag t = em.find(Tag.class, tagName);
if (t == null) t = new Tag(tagName);
p.addTagObject(t);
}
}
}
which after loading, it creates the set of tag names from the tag objects and before persisting it reads the set of tag names, and fetch or create tag objects.
My problem is that if I try to create multiple phrases which share tags, JPA instead of only creating the relationship (insert into the join table) it also create tag objects which violate primary key constraint.
transaction.begin();
Phrase p = new Phrase("Never ask what sort of computer a guy drives. If he's a Mac user, he'll tell you. If not, why embarrass him?", "Tom Clancy");
p.addTag("apple");
p.addTag("macintosh");
em.persist(p);
transaction.commit();
transaction.begin();
p = new Phrase("It's better to be a pirate than to join the Navy.", "Steve Jobs");
p.addTag("apple");
em.persist(p);
transaction.commit();
Exception in thread "main" javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.0.v20130507-3faac2b): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: [SQLITE_CONSTRAINT] Abort due to constraint violation (column uname is not unique)
Error Code: 0
Call: INSERT INTO TAG (uname, DESCRIPTION) VALUES (?, ?)
bind => [apple, null]

You haven't shown your addTag method in Phrase, but I assume that you have somewhere in there an expression new Tag() and it would look somehow similar to this:
public void addTag(String tagName) {
Tag tag = new Tag();
tag.setUniqueName(tagName);
tag.getPhrases().add(this);
this.tagObjects.add(tag);
}
In this case the method addTag will create new object of type Tag everytime the method is called, which will result in different entries in the relational table, cause Hibernate persists the whole objects, not only particular fields of theirs, regardless if these fields are primary keys or not.
After calling the method addTag two times, you will create two different objects and Hibernate could not know if those two objects relate to the same entry in the DB or not. This means that even though they have the same uniqueName, they could have a different description.
Imagine the following scenario:
transaction.begin();
Phrase p = new Phrase("Never ask what sort of computer a guy drives. If he's a Mac user, he'll tell you. If not, why embarrass him?", "Tom Clancy");
Tag t = new Tag();
t.setUniqueName("apple");
t.setDescription("This is an example apple");
p.getTagObjects().add(t);
em.persist(p);
transaction.commit();
transaction.begin();
p = new Phrase("It's better to be a pirate than to join the Navy.", "Steve Jobs");
t = new Tag();
t.setUniqueName("apple");
t.setDescription("Another description of the apple");
em.persist(p);
transaction.commit();
With this example the difference should be more obvious and should illustrate why it is impossible to Hibernate to know when are you referring to the same entry in the DB with two or more different objects.
As a solution I would suggest you to change the method addTag so that it has the following signature public void addTag(Tag tag) {... and keep track of the existing tags somewhere centralized or you can try out em.merge(p); instead of em.persist(p);

Related

How to set unique rows when persist in JPA?

My question is: is there any way to set unique row for rows created by "new" keyword?
I mean like this:
Product product = Product.builder()
.eancode("EAN-1234")
.externalId("123123")
.producerPartNumber("123123")
.name("VERY GOOD LAPTOP")
.vendor(new Vendor("LENOVO", "www.lenovo.com"))
.priceDetails(new PriceDetails(
new BigDecimal("19.99"),
new BigDecimal("20.00"),
new BigDecimal("21.00"),
Currency.PLN))
.build();
I want Vendor entity to be unique because now I've got something like this (when I multiply run this code):
id name url
1 LENOVO lenovo.com
2 LENOVO lenovo.com
I just want it to check it first if that name exist yet. Should I use #EmbeddedId in some way?
Edit: look now
#Override
public Long save(Product item) {
EntityManager em = getEntityManager();
em.getTransaction().begin();
//that line throws PersistentObjectException: detached entity passed to persis
em.persist(item); //cascade is set as PERSIST so why isn't it work
em.getTransaction().commit();
em.close();
return item.getId();
}
You need a VendorRepository contains a method like this Optional<Vendor> getVendorByNameAndUrl(String name, String url).
Then
VendorRepository vendorRepo;
#Transactional
public void saveProduct() {
Product product = Product.builder()
.eancode("EAN-1234")
.externalId("123123")
.producerPartNumber("123123")
.name("VERY GOOD LAPTOP")
.vendor(findVendorOrCreateNew("LENOVO", "www.lenovo.com"))
.priceDetails(new PriceDetails(
new BigDecimal("19.99"),
new BigDecimal("20.00"),
new BigDecimal("21.00"),
Currency.PLN))
.build();
// ...
}
Vendor findVendorOrCreateNew(String name, String url) {
return vendorRepo.getVendorByNameAndUrl(name, url)
.orElse(new Vendor(name, url));
}

JPA-Repository save: Proceed saving after insert Constraint violation

i'm using JPA repository to save simple data objects to the database. To avoid duplicates i created a unique constraint on multiple fields. If now a duplicate according to the unique fields/constraint should be saved i want to catch the exception, log the object and the application should proceed and saves the next object. But here i always get this exception: "org.hibernate.AssertionFailure: null id in de.test.PeopleDBO entry (don't flush the Session after an exception occurs)".
In general i understand what hibernate is doing, but how i can revert the session or start a new session to proceed with saving of the next data objects. Please have a look to the code below:
PeopleDBO.java
#Entity
#Data
#Table(
name = "PEOPLE",
uniqueConstraints = {#UniqueConstraint(columnNames = {"firstname", "lastname"}})
public class PeopleDBO {
public PeopleDBO(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstname;
private String lastname;
}
The Test:
public void should_save_people_and_ignore_constraint_violation(){
final List<PeopleDBO> peopleList = Arrays.asList(
new PeopleDBO("Georg","Smith"),
new PeopleDBO("Georg","Smith"),
new PeopleDBO("Paul","Smith")
);
peopleList.forEach(p -> {
try {
peopleRepository.save(p);
} catch (DataIntegrityViolationException e) {
log.error("Could not save due to constraint violation: {}",p);
}
}
Assertions.assertThat(peopleRepository.count()).isEqualTo(2);
}
The problem is, that with saving of the second people the unique constraint gets violated. The error log happens, and with the next call of peopleRepository.save() the mentioned exception above is thrown:
"org.hibernate.AssertionFailure: null id in de.test.PeopleDBO entry (don't flush the Session after an exception occurs)"
How i can avoid this behaviour? How i can clean the session or start a new session?
Thanks a lot in advance
d.
--------- Edit / new idea ------
I just tried some things and have seen that i could implement a PeopleRepositoryImpl, like this:
#Service
public class PeopleRepositoryImpl {
final private PeopleRepository peopleRepository;
public PeopleRepositoryImpl(PeopleRepository peopleRepository) {
this.peopleRepository = peopleRepository;
}
#Transactional
public PeopleDBO save(PeopleDBO people){
return peopleRepository.save(people);
}
}
This is working pretty fine in my tests. ... what do you think?
One single transaction
The reason is that all inserts occur in one transaction. As this transaction is atomic, it either succeeds entirely or fails, there is nothing in-between.
The most clean solution is to check if a People exists before trying to insert it:
public interface PeopleRespository {
boolean existsByLastnameAndFirstname(String lastname, String firstname);
}
and then:
if (!peopleRepository.existsByLastnameAndFirstname(p.getLastname, p.getFirstname)) {
peopleRepository.save(p);
}
One transaction per people
An alternative is indeed to start a new transaction for each person. But I am not sure it will be more efficient, because there is an extra cost to create transaction.

Delete a record on an OneToMany association with Hibernate

I am working on an application using Hibernate and I want to delete some records in the database. The relevant Entities are:
#Entity
public class Product {
private String serialNumber;
private Set<Part> parts = new HashSet<Part>();
#Id
public String getSerialNumber() { return serialNumber; }
void setSerialNumber(String sn) { serialNumber = sn; }
#OneToMany
public Set<Part> getParts() { return parts; }
void setParts(Set parts) { this.parts = parts; }
...
}
#Entity
public class Part implements Serializable {
#Id
#GeneratedValue
private Long part_id;
private String userCode = "";
//getters and setters
....
}
I have let Eclipse implement equals and hashCode in Entity Part based on part_id and userCode. There is also an Entity Factory from which 'begin' all the associations to the other Entities. Therefore, in order to save all the changes it only necessary to execute the comand:
session.update(factory);
All the changes are saved successfully except from the delete from parts. I do:
products.getParts.remove(part);
The issues comig out are:
1) In some cases is part from the Set not removed although the comparison to a part in the Set with equals true returns (the part is in Set according to equals but it is not removed)
2) Even if the remove in the Set succeeds, the record in the database is not deleted.
Based on the above ascertainments what is the best way to remove the records in this case using not loads of queries?
You need to explicitly remove the child:
session.delete(part);
From Hibernate Docs:
The following code:
Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
c.setParent(null);
session.flush();
will not remove c from the database. In this case, it will only remove
the link to p and cause a NOT NULL constraint violation. You need to
explicitly delete() the Child.
Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
session.delete(c);
session.flush();
When using hibernate to map relationships you must be aware of two main concerns:
Which is the owner of the relationship? The owner is the side of the relation whose changes will be persisted in database. In your case the owner is the Part object.
Is a true parent/child relationship or simply a composition relationship? In your case I think the answer is composition
If you want to manage the relation using the set, you have two options:
use #ElementCollection instead of #OnetoMany
change ownership. Something like this:
#OneToMany
#JoinColumn(name="part_id")
public Set<Part> getParts() { return parts; }
void setParts(Set parts) { this.parts = parts; }
However, the second option is not recommended here. See section 2.2.5.3.1.2.

How do I correctly paginate in Hibernate with nested objects with ManyToMany associations?

Ok, so I have the following (abbreviated) 3 entity and HibernateUtil classes.
public class Tag {
#Id
BigDecimal id;
String tag
#ManyToMany( mappedBy="tags" )
List<Label> labels;
}
public class Label {
#Id
BigDecimal id;
String label;
#ManyToMany( targetEntity=Tag.class )
List<Tag> tags;
}
public class Data {
#Id
BigDecimal id;
BigDecimal data;
#ManyToOne
Label label;
}
public class HibernateUtil {
public static List pagedQuery(DetachedCriteria detachedCriteria, Integer start, Integer size) throws WebApplicationException {
Session session = getSession();
try {
Transaction transaction = session.beginTransaction();
List records = detachedCriteria.getExecutableCriteria(session)
.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
.setFirstResult(start)
.setMaxResults(size)
.list();
transaction.commit();
return records;
} catch (Exception e) {
// Place Logger here...
throw new WebApplicationException(e);
} finally {
session.close();
}
}
}
The issue I have is that when I try to query the Data class with the HibernateUtil.pagedQuery( detatchedCriteria, start, size ), my result list doesn't match the size parameter. I have found that the reason for this is the way hibernate builds the query to include the tags (Data.Label.Tags).
For instance, when a Label has more than one associated Tags the result list for the Data object subquery used in the complete paginated query would look like the following (I found this by parsing the sql Hibernate spits out to the console)
Data-1;Label:Tag-1
Data-1;Label;Tag-2
Data-2;Label;Tag-1
Data-2;Label;Tag-2
etc...
If I were to call this with size=3, then the returned result set would be
Data-1;Label:Tag-1
Data-1;Label;Tag-2
Data-2;Label;Tag-1
However, Hibernate would then group the first two rows together (since they're the same Data object), and my returned List object would have a size of 2 (Data-1 & Data-2)
I attempted to replace the setResultTransformer method with a Projection approach that I found through Google, but that then only returned the id's of the Data objects.
Does anyone have any advice for me? I'm not sure where to go from here...
You are facing a common problem paginating with hibernate. The resultTransformer is applied in the "Java" side, so the pagination has already been made on the DB side.
The simplest (maybe not the most optimized) is to do two queries, one with the projection and pagination (like the one you already did) and another using the projection id's. Here is an example:
//get the projection
Criteria criteria = factory.getCurrentSession().createCriteria(getEntityClass());
criteria.setProjection(Projections.distinct((Projections.projectionList().add(Projections.id()).add(Projections.property("name")))));
//paginate the results
criteria.setMaxResults(pageSize);
criteria.setFirstResult(first);
List<Object[]> idList = criteria.list();
//get the id's from the projection
List<Long> longList = new ArrayList<Long>();
for (Object[] long1 : idList) {
Object[] record = long1;
longList.add((Long) record[0]);
}
if (longList.size() > 0) {
//get all the id's corresponding to the projection,
//then apply distinct root entity
criteria = factory.getCurrentSession().createCriteria(getEntityClass());
criteria.add(Restrictions.in("id", longList));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
} else {
//no results, so let's ommit the second query to the DB
return new ArrayList<E>();
}
return criteria.list();

JPA: Eclipselink does not persist bi-directional relationships in database

My domain model in my Java EE 6 application contains bi-directional relationships like the following:
#Entity
public class Users implements PrimaryKeyHolder<String>, Serializable {
#Id
private String username;
#ManyToMany(mappedBy= "users")
private List<Category> categories;
public List<Category> getCategories() {
if (categories == null) {
categories = new ArrayList<Category>();
}
return Collections.unmodifiableList(categories);
}
public void addCategory(Category category) {
if (categories == null) {
categories = new ArrayList<Category>();
}
categories.add(category);
if (!category.getUsers().contains(this)) {
category.addUser(this);
}
}
public void removeCategory(Category category) {
if (categories == null) {
categories = new ArrayList<Category>();
}
categories.remove(category);
if (category.getUsers().contains(this)) {
category.removeUser(this);
}
}
public void setCategories(Collection<Category> categories) {
if (this.categories == null) {
this.categories = new ArrayList<Category>();
}
for (Iterator<Category> it = this.categories.iterator(); it.hasNext();) {
Category category = it.next();
it.remove();
if (category.getUsers().contains(this)) {
category.removeUser(this);
}
}
for (Category category : categories) {
addCategory(category);
}
}
}
#Entity
public class Category implements PrimaryKeyHolder<Long>, Serializable {
#Id
private Long id;
#ManyToMany
private List<User> users;
public List<User> getUsers() {
if (users == null) {
users = new ArrayList<User>();
}
return Collections.unmodifiableList(users);
}
protected void addUser(User user) {
if (users == null) {
users = new ArrayList<User>();
}
users.add(user);
}
protected void removeUser(User user) {
if (users == null) {
users = new ArrayList<User>();
}
users.remove(user);
}
}
UPDATE: I added relationship management code. Relationships are only set on the user side, therefore, the add/remove methods are protected in the Categoriy class. I set the categories on the user via setCategories.
Eclipselink correctly generates a join table CATEGORY_USERS. However, it does not persist any information in it (it only caches the information). E.g. when I execute a find operation on the entity manager (e.g. a user), it returns the complete object graph (including the category relationship). But when I look at the tables, information are not updated (even though the transactions are committed). I also inserted a flush operation in my code, without success. Basic information (like String, Integer, etc. columns) gets correctly persisted and updated. After turning the log level to FINE, I can see that no SQL statements are executed for the relationships and the join table, respectively. But I do see SQL statements for uni-directional relationships.
My datamodel is covered by extensive unit tests, which all pass successfully. I basically do the same operation as in the container, commit the transaction, reload the entities from the db and check if the relationships are correctly set, which they are (I'm using the in-memory derby database for testing).
My app server is Glassfish v3.1-b17.
Any help is appreciated.
Thanks,
Theo
Ensure you are setting both sides of the relationship. The specification requires that the application sets both sides of the relationship as there is no relationship maintenance in JPA.
After endless hours of trying I finally got to a solution: I simply changed the owning side of the relationship, i.e. I put the mappedBy attribute to the category entity like this:
#ManyToMany(mappedBy= "categories")
private List<User> users;
The explanation for this can be found here
Four points:
1.- When you have an error, it's more simple find solution isolating them in an example (Or unit test) that reproduces the error. In your case, you could do an example with more simple getter and setter (for example, removing unmodifiableList use and other innecesary methods for testing actually issue).
2.- I advise you to use pojos for model, without any logic. So, remove logic from pojos.
3.- We are using eclipselink and we do not have problems persisting relations. So, it is more possible that error will be in your code.
4.- Test annoting relation with "cascade = javax.persistence.CascadeType.ALL"
Apology for my poor English :(

Categories

Resources