I have a hibernate entity with one-to-many association:
#Entity
public class Parent {
#OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
#Cascade(CascadeType.ALL)
private Set<Child> children = new HashSet<Child>();
#Version
private Date version;
}
#Entity
public class Child {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "PARENT_ID")
private Parent parent;
#Basic
private String key;
}
*some annotations removed for clarity
Child entity maps on a table with composite primary key (KEY and PARENT_ID). The problem is when two users adds the same Child (with the same key) to the same Parent the cascade save (session.saveOrUpdate(parent)) fails with Child's primary key violation instead of optimistic lock failure.
If users change some other property in the Parent entity in addition to the collection, the optimistic lock works fine.
I could add some fictive property to the Parent class and change it every time when the collection changes and it will do the trick but it looks like a hack.
Or I could replace composite primary key to a surrogate one (by adding #Id).
The question is: What is the recommended approach of implementing optimistic locking in such a case?
Could be related to Hibernate #Version causing database foreign key constraint failure.
Only unidirectional collection changes are going to be propagated to the parent entity version. Because you are using a bidirectional association, it's the #ManyToOne side that will control this association, so adding/removing an entity in the parent-side collection is not going to affect the parent entity version.
However, you can still propagate changes from child entities to parent entities. This requires you to propagate the OPTIMISTIC_FORCE_INCREMENT lock whenever the child entity is modified.
In short, you need to have all your entities implementing a RootAware interface:
public interface RootAware<T> {
T root();
}
#Entity(name = "Post")
#Table(name = "post")
public class Post {
#Id
private Long id;
private String title;
#Version
private int version;
//Getters and setters omitted for brevity
}
#Entity(name = "PostComment")
#Table(name = "post_comment")
public class PostComment
implements RootAware<Post> {
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
private Post post;
private String review;
//Getters and setters omitted for brevity
#Override
public Post root() {
return post;
}
}
#Entity(name = "PostCommentDetails")
#Table(name = "post_comment_details")
public class PostCommentDetails
implements RootAware<Post> {
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId
private PostComment comment;
private int votes;
//Getters and setters omitted for brevity
#Override
public Post root() {
return comment.getPost();
}
}
Then, you need two event listeners:
public static class RootAwareInsertEventListener
implements PersistEventListener {
private static final Logger LOGGER =
LoggerFactory.getLogger(RootAwareInsertEventListener.class);
public static final RootAwareInsertEventListener INSTANCE =
new RootAwareInsertEventListener();
#Override
public void onPersist(PersistEvent event) throws HibernateException {
final Object entity = event.getObject();
if(entity instanceof RootAware) {
RootAware rootAware = (RootAware) entity;
Object root = rootAware.root();
event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);
LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", root, entity);
}
}
#Override
public void onPersist(PersistEvent event, Map createdAlready)
throws HibernateException {
onPersist(event);
}
}
and
public static class RootAwareInsertEventListener
implements PersistEventListener {
private static final Logger LOGGER =
LoggerFactory.getLogger(RootAwareInsertEventListener.class);
public static final RootAwareInsertEventListener INSTANCE =
new RootAwareInsertEventListener();
#Override
public void onPersist(PersistEvent event) throws HibernateException {
final Object entity = event.getObject();
if(entity instanceof RootAware) {
RootAware rootAware = (RootAware) entity;
Object root = rootAware.root();
event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);
LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", root, entity);
}
}
#Override
public void onPersist(PersistEvent event, Map createdAlready)
throws HibernateException {
onPersist(event);
}
}
which you can register as follows:
public class RootAwareEventListenerIntegrator
implements org.hibernate.integrator.spi.Integrator {
public static final RootAwareEventListenerIntegrator INSTANCE =
new RootAwareEventListenerIntegrator();
#Override
public void integrate(
Metadata metadata,
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
final EventListenerRegistry eventListenerRegistry =
serviceRegistry.getService( EventListenerRegistry.class );
eventListenerRegistry.appendListeners(EventType.PERSIST, RootAwareInsertEventListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.FLUSH_ENTITY, RootAwareUpdateAndDeleteEventListener.INSTANCE);
}
#Override
public void disintegrate(
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
//Do nothing
}
}
and then supply the RootAwareFlushEntityEventListenerIntegrator via a Hibernate configuration property:
configuration.put(
"hibernate.integrator_provider",
(IntegratorProvider) () -> Collections.singletonList(
RootAwareEventListenerIntegrator.INSTANCE
)
);
Now, when you modify a PostCommentDetails entity:
PostCommentDetails postCommentDetails = entityManager.createQuery(
"select pcd " +
"from PostCommentDetails pcd " +
"join fetch pcd.comment pc " +
"join fetch pc.post p " +
"where pcd.id = :id", PostCommentDetails.class)
.setParameter("id", 2L)
.getSingleResult();
postCommentDetails.setVotes(15);
The parent Post entity version is modified as well:
SELECT pcd.comment_id AS comment_2_2_0_ ,
pc.id AS id1_1_1_ ,
p.id AS id1_0_2_ ,
pcd.votes AS votes1_2_0_ ,
pc.post_id AS post_id3_1_1_ ,
pc.review AS review2_1_1_ ,
p.title AS title2_0_2_ ,
p.version AS version3_0_2_
FROM post_comment_details pcd
INNER JOIN post_comment pc ON pcd.comment_id = pc.id
INNER JOIN post p ON pc.post_id = p.id
WHERE pcd.comment_id = 2
UPDATE post_comment_details
SET votes = 15
WHERE comment_id = 2
UPDATE post
SET version = 1
where id = 1 AND version = 0
For me it was enough to set the OptimisticLock annotation:
#OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
#OptimisticLock(excluded = false)
private Set<Child> children = new HashSet<Child>();
First I think you need to declare your primary key and define how the PK is generated.
Example :
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
Then, the best way to add new child to your parent should be like this (on the parent side):
public Child addChild() {
Child child = new Child()
if (childList== null) {
childList= new ArrayList<childList>();
}
child.setparent(this);
childList.add(child);
return child;
}
When the child already exist, simply do the same but without creating a new Child.
I think it should resolve some of your problems.
Related
I have a case where a participant can register courses.
Basically I have the following entity configuration (getters and setters omitted as well as other useless properties) :
#Entity
#Table(name = "course")
public class Course {
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "course")
private Set<Registration> registrations;
}
#Entity
#Table(name = "participant")
public class Participant {
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "participant")
private Set<Registration> registrations;
}
#Entity
#Table(name = "registration")
public class Registration {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "course_id")
private Course course;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "participant_id")
private Participant participant;
#PreRemove
private void removeRegistrationFromHolderEntities() {
course.getRegistrations().remove(this);
participant.getRegistrations().remove(this);
}
}
Then I can from my viewmodel delete a registration or a course (I have also removed unnecessary stuff) :
#Command
public void deleteRegistration(Registration reg) {
registrationMgr.delete(reg);
}
#Command
public void deleteCourse(Course crs) {
courseMgr.delete(crs);
}
Problem :
If I delete a registration, I need the #PreRemove function so I can remove the references. Without this the remove is ignored (no error, simply ignored)
If I delete a course, I have to remove the #PreRemove function else I get a ConcurrentModificationException (evidently...)
I also cannot remove references from the deleteRegistration method (instead of #PreRemove) because participant registrations are lazily loaded (would raise failed to lazily initialize a collection of role: ..., could not initialize proxy - no Session exception).
What is the best approach here ?
I use Java 11 with Spring Boot 1.0.4 (and spring-boot-starter-data-jpa).
EDIT :
The managers/repositories or defined this way (same for registration and participant) so it should be transactional (I don't have #EnableTransactionManagement on my main class but it should not be required as I don't use transactions outside of repositories) :
#Transactional
#Component("courseMgr")
public class CourseManager {
#Autowired
CourseRepository courseRepository;
public void saveOrUpdate(Course course) {
courseRepository.save(course);
}
public void delete(Course course) {
courseRepository.delete(course);
}
}
public interface CourseRepository extends CrudRepository<Course, Long> {
...
}
EDIT2 :
I think I have found a pretty simple solution :
I have removed the #PreRemove method from the entity, then instead of removing the references like this in the deleteRegistration method (which I had tried but was causing failed to lazily initialize a collection of role exception) :
#Command
public void deleteRegistration(Registration reg) {
reg.getCourse().getRegistrations().remove(reg);
reg.getParticipant.getRegistrations().remove(reg);
registrationMgr.delete(reg);
}
I simply set parents to null, I don't care as it will be deleted...
#Command
public void deleteRegistration(Registration reg) {
reg.setCourse(null);
reg.setParticipant(null);
registrationMgr.delete(reg);
}
So now I can also delete a course without triggering the ConcurrentModificationException in the #PreRemove.
EDIT3 : My bad, registration was not removed with the solution above (still no error but nothing happens). I ended with this instead, which finally works :
#Command
public void deleteRegistration(Registration reg) {
// remove reference from course, else delete does nothing
Course c = getRegistration().getCourse();
c.getRegistrations().remove(getRegistration());
courseMgr.saveOrUpdate(c);
// delete registration from the database
registrationMgr.delete(reg);
}
No need to remove reference from participant...
You have setup your repositories incorrectly. You need a composite PK for Registration and you need to understand that bidirectional mappings are really for query only. Further, bidirectional mappings in Course and Participate present challenges because the ManyToOne relationship through the Registration entity is FetchType.EAGER by default. With all the cascade and fetch annotations you have you are asking for a complicated combination of things from JPA and it seems like you really haven't sorted it all out yet. Start with the basics, be sure to print your SQL statements, and proceed from there if you want to try to finesse more from JPA.
#Entity
#Data
public class Course {
#Id
private Integer id;
private String name;
}
#Entity
#Data
public class Participant {
#Id
private Integer id;
private String name;
}
#Entity
#Data
public class Registration {
#EmbeddedId
private RegistrationPK id;
#ManyToOne
#MapsId("participant_id")
private Participant participant;
#ManyToOne
#MapsId("course_id")
private Course course;
}
#Embeddable
#Data
public class RegistrationPK implements Serializable {
private static final long serialVersionUID = 1L;
private Integer course_id;
private Integer participant_id;
}
Is your basic Entities. The RegistrationRepository needs an additional query.
public interface RegistrationRepository extends JpaRepository<Registration, RegistrationPK> {
Set<Registration> findByCourse(Course c);
}
And to use all this in an example:
#Override
public void run(String... args) throws Exception {
create();
Course c = courseRepo.getOne(1);
Set<Registration> rs = read(c);
System.out.println(rs);
deleteCourse(c);
}
private void create() {
Course c1 = new Course();
c1.setId(1);
c1.setName("c1");
courseRepo.save(c1);
Participant p1 = new Participant();
p1.setId(1);
p1.setName("p1");
participantRepo.save(p1);
Registration r1 = new Registration();
r1.setId(new RegistrationPK());
r1.setCourse(c1);
r1.setParticipant(p1);
registrationRepo.save(r1);
}
private Set<Registration> read(Course c) {
return registrationRepo.findByCourse(c);
}
private void deleteCourse(Course c) {
registrationRepo.deleteAll( registrationRepo.findByCourse(c) );
courseRepo.delete(c);
}
OK solution was pretty simple.
I indeed need to remove the references from the deleteRegistration method. This is what I had tried but was causing failed to lazily initialize a collection of role exception :
#Command
public void deleteRegistration(Registration reg) {
reg.getCourse().getRegistrations().remove(reg);
reg.getParticipant.getRegistrations().remove(reg);
registrationMgr.delete(reg);
}
The trick is that I also have to save the course entity before trying to delete the registration.
This works :
#Command
public void deleteRegistration(Registration reg) {
// remove reference from course, else delete does nothing
Course c = getRegistration().getCourse();
c.getRegistrations().remove(getRegistration());
courseMgr.saveOrUpdate(c);
// delete registration from the database
registrationMgr.delete(reg);
}
No need to remove reference from participant...
#PreRemove was doing the job, but that way I can now also delete a course without triggering the ConcurrentModificationException.
I have written a service method importCategories() which retrieves a list of categories from database and recursively fills in properties and parent categories. The problem I'm experiencing is that new categories are created twice, except when I annotate complete() with #Transactional. Can anyone explain to me why that is? I save the child before adding it to the parent, and afterwards save the parent which has CascadeType.ALL on the child collection.
Model:
#Entity
public class Category implements Identifiable<Integer> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private Integer key;
private String name;
#ManyToOne
private Category parent;
#OneToMany(mappedBy="parent", cascade = {CascadeType.ALL})
private List<Category> children = new ArrayList<Category>();
public void add(Category category) {
category.setParent(this);
children.add(category);
}
}
Service:
#Transactional
private void complete(Category category) {
// ... (getting category info such as "name" and "parent key" from web service)
category.setName(name);
category = categoryRepository.saveAndFlush(category);
if (category.getParent() == null) {
Category parentCategory = new Category();
parentCategory.setKey(parentKey);
List<Category> categories = categoryRepository.findByKey(parentKey);
if (categories.size() > 0) {
parentCategory = categories.get(0);
}
parentCategory.add(category);
parentCategory = categoryRepository.saveAndFlush(parentCategory);
if (parentCategory.getParent() == null) {
complete(parentCategory);
}
}
}
public void importCategories() {
List<Category> list = categoryRepository.findAll();
for (Category category : list) {
complete(category);
}
}
If you have a cascade ALL type then you dont need to save your child entity first, just the parent.
category.getchildren().add(children)
save(category)
On that moment category will save/update the entity and will do the same for children.
look another examples to understand how works the hibernate cascade: http://www.mkyong.com/hibernate/hibernate-cascade-example-save-update-delete-and-delete-orphan/
I'm having a few troubles with Java programming. In my case, I'm using Hibernate criteria queries through Spring's getHibernateTemplate without having direct access to the session object.
I have a parent-child relationship mapped with JPA annotations. Here is my DAO method
public List<Child> findByParent(final Parent parent)
{
return getHibernateTemplate().executeFind(new HibernateCallback<List<Child>>()
{
#Override
public List<Child> doInHibernate(Session session) throws HibernateException, SQLException
{
return session.createCriteria(Child.class)
.add(Restrictions.eq("parent", parent))
.list();
}
});
}
I have marked parent as final because I'm passing it to the anonymous class. When I run the code Hibernate does perform the query (... where parentId = ?) but I get no result. Running it in mysql with correct parentId returns results.
When debugging, I see Eclipse can't inspect the value of parent from within the anonymous class. What's wrong? How do I fix this?
[edit] Here are the Pojo. I have no control over Parent (not even source, I just pressed F3 for auto decompile)
#Entity
#SequenceGenerator(name = "SEQ_STORE", sequenceName = "SEQ_CHILD")
#Table(name = "TA_CHILD", uniqueConstraints = { #UniqueConstraint(columnNames = { "A_FIELD", "PARENT_ID" }) })
public class Immobile
{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "CHILD_ID")
protected int id;
#JoinColumn(name = "PARENT_ID")
#ManyToOne(fetch = FetchType.LAZY, optional = false, targetEntity = Organization.class)
protected Parent parent;
#GeneratedValue
#Column(name = "PARENT_ID", insertable = false, updatable = false)
protected String parentId;
}
// Compiled from Parent.java (version 1.6 : 50.0, super bit)
#javax.persistence.Entity
#javax.persistence.Table(name="TA_PARENT")
public class org.Partent extends org.ExtensibleBase implements java.io.Serializable {
// Field descriptor #161 J
private static final long serialVersionUID = 1L;
// Field descriptor #166 Ljava/lang/String;
#javax.persistence.Id
#javax.persistence.Column(name="PARENT_ID",
nullable=false,
length=(int) 20)
private java.lang.String id;
}
In restriction, you have to add criteria for parent entity's id
Try below code,
public List<Child> findByParent(final Parent parent)
{
return getHibernateTemplate().executeFind(new HibernateCallback<List<Child>>()
{
#Override
public List<Child> doInHibernate(Session session) throws HibernateException, SQLException
{
return session.createCriteria(Child.class)
.add(Restrictions.eq("parent.id", parent.getId()))
.list();
}
});
}
There was no problem in the code. Simply, after populating the tables from MySQL workbench I forgot to commit the transaction or leave auto commit enabled.
Both fragments (mine and Pritesh's) are correct and return all results.
Problem solved but that doesn't explain Eclipse being unable to evaluate parent variable, which is out of the scope
I'm using Open JPA in the commercial project and would like to take advantage of the cascading Parent->Child remove and merge.
I mocked up a working code which shows the issues.
I have a persistent Parent object with some children. I'm eliminating one of the Children's relation and passing detached Parent to merge. When transaction is committed a UPDATE statement issued, trying to update orphaned child with NULL foreign key.
#Entity
public class Parent implements Serializable {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String desc;
//#ElementDependent
#OneToMany(mappedBy="parent",
cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}/*,orphanRemoval=true, fetch=FetchType.EAGER*/)
private List<Child> childs = new ArrayList<Child>();
#Entity
public class Child implements Serializable {
#Id private String desc;
#ManyToOne
private Parent parent;
public class StackOverflowTest extends TestCase {
private EntityManager em;
private static EntityManagerFactory factory = Persistence.createEntityManagerFactory("SSICTest", System.getProperties());
private Parent p;
private Child c;
private Child c2;
public void testRemove() {
prepareObjects();
startTr();
em.persist(p);
commitTr();
startTr();
p = em.find(Parent.class, p.getId());
em.remove(p);
commitTr();
}
public void testMerge() {
prepareObjects();
startTr();
em.persist(p);
commitTr();
//remove on detached
Child child = p.getChilds().get(0);
p.getChilds().remove(child);
child.setParent(null);
startTr();
em.merge(p);
commitTr();
startTr();
p = em.find(Parent.class, p.getId());
assertEquals(1, p.getChilds().size());
commitTr();
}
protected void prepareObjects() {
p = new Parent();
c = new Child();
c2 = new Child();
p.setDesc("desc");
c.setDesc(Math.random()+"");
c2.setDesc(Math.random()+"");
p.getChilds().add(c);
c.setParent(p);
p.getChilds().add(c2);
c2.setParent(p);
}
void commitTr() {
em.getTransaction().commit();
em.close();
}
void startTr() {
em = factory.createEntityManager();
em.getTransaction().begin();
}
}
In the example above testRemove works fine but testMerge method not, as I described at the top.
If I remove comment on #ElementDependent it works different.
testRemove fails, because remove not cascaded to Child and referential integrity exception thrown by db, and testMerge is fine.
orphanRemoval=true, fetch=FetchType.EAGER or #ForeignKey(deleteAction=ForeignKeyAction.CASCADE) on inverse relation in child
do not help too.
Please advise. I really appreciate your help!!
#OneToMany(mappedBy="parent",
cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE},
orphanRemoval=true, fetch=FetchType.EAGER)
private List<Child> childs = new ArrayList<Child>();
#ManyToOne
private Parent parent;
Set orphanRemoval to true.See purpose of orphanRemoval
Using Hibernate + annotations, I'm trying to do the following:
Two entities, Entity1 and Entity2.
Entity1 contains a simple generated value primary key.
Entity2 primary key is composed by a simple generated value + the id of entity one (with a many to one relationship)
Unfortunately, I can't make it work.
Here is an excerpt of the code:
#Entity
public class Entity1 {
#Id #GeneratedValue
private Long id;
private String name;
...
}
#Entity
public class Entity2 {
#EmbeddedId
private Entity2PK pk = new Entity2PK();
private String miscData;
...
}
#Embeddable
public class Entity2PK implements Serializable {
#GeneratedValue
private Long id;
#ManyToOne
private Entity1 entity;
}
void test() {
Entity1 e1 = new Entity1();
e1.setName("nameE1");
Entity2 e2 = new Entity2();
e2.setEntity1(e1);
e2.setMiscData("test");
Transaction transaction = session.getTransaction();
try {
transaction.begin();
session.save(e1);
session.save(e2);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
}
When I run the test method I get the following errors:
Hibernate: insert into Entity1 (id, name) values (null, ?)
Hibernate: call identity()
Hibernate: insert into Entity2 (miscData, entity_id, id) values (?, ?, ?)
07-Jun-2010 10:51:11 org.hibernate.util.JDBCExceptionReporter logExceptions
WARNING: SQL Error: 0, SQLState: null
07-Jun-2010 10:51:11 org.hibernate.util.JDBCExceptionReporter logExceptions
SEVERE: failed batch
07-Jun-2010 10:51:11 org.hibernate.event.def.AbstractFlushingEventListener performExecutions
SEVERE: Could not synchronize database state with session
org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:103)
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:91)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:254)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:266)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:167)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1001)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:339)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
at test.App.main(App.java:32)
Caused by: java.sql.BatchUpdateException: failed batch
at org.hsqldb.jdbc.jdbcStatement.executeBatch(Unknown Source)
at org.hsqldb.jdbc.jdbcPreparedStatement.executeBatch(Unknown Source)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:48)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:247)
... 8 more
Note that I use HSQLDB.
Any ideas about what is wrong ?
Do as follows whether you want this kind of behavior (You nedd to use property access instead of field one)
Parent.class (Notice a property of Type MutableInt and addChild as a way to set up both sides)
#Entity
public class Parent implements Serializable {
private MutableInt id = new MutableInt();
private List<Child> childList = new ArrayList();
#OneToMany(mappedBy="parent")
#JoinColumn(name="PARENT_ID", insertable=false, updatable=false)
#Cascade(CascadeType.SAVE_UPDATE)
public List<Child> getChildList() {
return childList;
}
public void setChildList(List<Child> childList) {
this.childList = childList;
}
#Id
#GeneratedValue
public Integer getId() {
return id.intValue();
}
public void setId(Integer id) {
this.id.setValue(id);
}
#Transient
public MutableInt getIdAsMutableInt() {
return id;
}
/**
* Add convenience method
*
* A way to set up both sides (You have a bi-directional relationship, right ???)
*/
public void addChild(Child child) {
if(child.getChildId() == null)
child.setChildId(new Child.ChildId());
child.getChildId().setParentIdAsMutableInt(id);
getChildList().add(child);
child.setParent(this);
}
}
Child.class (Notice static inner class )
#Entity
public class Child implements Serializable {
private ChildId childId;
private Parent parent;
#EmbeddedId
public ChildId getChildId() {
return childId;
}
public void setChildId(ChildId childId) {
this.childId = childId;
}
#ManyToOne
#JoinColumn(name="PARENT_ID", insertable=false, updatable=false)
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
/**
* Composite primary key class MUST override equals and hashCode
*/
#Embeddable
public static class ChildId implements Serializable {
private MutableInt parentId = new MutableInt();
private Integer chId;
public void setParentIdAsMutableInt(MutableInt parentId) {
this.parentId = parentId;
}
#GeneratedValue
public Integer getChId() {
return chId;
}
public void setChId(Integer chId) {
this.chId = chId;
}
#Column(name="PARENT_ID")
public Integer getParentId() {
return parentId.intValue();
}
public void setParentId(Integer parentId) {
this.parentId.setValue(parentId);
}
#Override
public boolean equals(Object o) {
if(!(o instanceof ChildId))
return false;
final ChildId other = (ChildId) o;
return new EqualsBuilder()
.append(getChId(), other.getChId())
.append(getParentId(), other.getParentId()).isEquals();
}
#Override
public int hashCode() {
int hash = 5;
hash = 11 * hash + getParentId().hashCode();
hash = 11 * hash + getChId().hashCode();
return hash;
}
}
}
And to test
Session session = configuration.buildSessionFactory().openSession();
session.beginTransaction();
Parent parent = new Parent();
parent.addChild(new Child());
session.save(parent);
session.getTransaction().commit();
You need a MutableInt because of Integer is immutable instance. But your MutableInt field is encapsulated by a Integer property. See carefully
You can use foreign key in embedded id as a separate read-only column:
#Entity
public class Entity2 {
#EmbeddedId
private Entity2PK pk = new Entity2PK();
#ManyToOne
#JoinColumn(name = "entity1_id")
private Entity1 entity;
...
}
#Embeddable
public class Entity2PK implements Serializable {
#GeneratedValue
private Long id;
#Column(name = "entity1_id", insertable = false, updateable = false)
private Long entity1Id;
}