-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 
Author Message
 Post subject: NonUniqueObjectException with many-to-many relation
PostPosted: Wed Nov 02, 2005 8:51 am 
Newbie

Joined: Wed Nov 02, 2005 8:13 am
Posts: 4
Hi there, great product.

This is probably really simple but, believe me, I've searched everywhere for an answer.

I have 2 Business Objects - User and Group - each having a (Set) collection of the other (many-to-many).
Everything seemed fine until it came to edit a persisted User object. I receive a nonUniqueObjectException upon update()ing a load()ed instance, only when the User holds a Group object it contained when loaded.
In case that was a bit vague - User object to be edited is load()ed. From a form submission, User attributes are amended. The User Set 'groups' is replaced with a new Set made up of load()ed Groups as specified in the form (drop down of group ID's). The User is then passed to my service to be update()ed (I know i should be able to use saveOrUpdate() as well).
I realise the problem is with the User's Groups being loaded into the session and then trying to persist them again. Doesn't hibernate realise this relation already exists and skip it (or replace all relations completely) for the User instance?
I've attempted to evict() the Groups of that User before updating, but shouldn't I be able to just call save at the service level and let hibernate use the mappings for collections. Sure I'm wrong here though. Also, marking it as lazy is not an option.

Anywho. The User mapping:



Code:
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 2.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping
>
    <class
        name="com.affiliate.bo.User"
        table="users"
    >

        <id
            name="id"
            column="user_id"
            type="java.lang.Long"
        >
            <generator class="increment">
              <!-- 
                  To add non XDoclet generator parameters, create a file named
                  hibernate-generator-params-User.xml
                  containing the additional parameters and place it in your merge dir.
              -->
            </generator>
        </id>

        <property
            name="dateOfCreation"
            type="calendar"
            column="dateOfCreation"
        />

        <property
            name="email"
            type="java.lang.String"
            column="email"
            not-null="true"
            unique="true"
        />

        <property
            name="firstName"
            type="java.lang.String"
            column="firstName"
        />

        <property
            name="lastLogin"
            type="calendar"
            column="lastLogin"
        />

        <property
            name="lastName"
            type="java.lang.String"
            column="lastName"
        />

        <property
            name="telephone"
            type="java.lang.String"
            column="telephone"
        />

        <property
            name="title"
            type="java.lang.String"
            column="title"
        />

        <property
            name="accountNonLocked"
            type="boolean"
            column="accountNonLocked"
            not-null="true"
        />

        <set
            name="groups"
            table="userGroups"
            lazy="false"
            cascade="save-update"
            sort="unsorted"
            order-by="group_id asc"
        >

              <key
                  column="user_id"
              >
              </key>

              <many-to-many
                  class="com.affiliate.bo.Group"
                  column="group_id"
                  outer-join="auto"
               />

        </set>

        <property
            name="enabled"
            type="boolean"
            column="enabled"
            not-null="true"
        />

        <property
            name="password"
            type="java.lang.String"
            column="password"
            not-null="true"
        />

        <property
            name="expirationDate"
            type="calendar"
            column="expirationDate"
        />

        <property
            name="credentialsExpirationDate"
            type="calendar"
            column="credentialsExpirationDate"
        />

        <!--
            To add non XDoclet property mappings, create a file named
                hibernate-properties-User.xml
            containing the additional properties and place it in your merge dir.
        -->

    </class>

        <query name="findUserByEmail"><![CDATA[
            from User user where user.email = :email
        ]]></query>

</hibernate-mapping>


The Group mapping:

Code:
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 2.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping
>
    <class
        name="com.affiliate.bo.Group"
        table="groups"
    >

        <id
            name="id"
            column="group_id"
            type="java.lang.Long"
        >
            <generator class="increment">
              <!-- 
                  To add non XDoclet generator parameters, create a file named
                  hibernate-generator-params-Group.xml
                  containing the additional parameters and place it in your merge dir.
              -->
            </generator>
        </id>

        <property
            name="name"
            type="java.lang.String"
            column="name"
            unique="true"
        />

        <set
            name="members"
            table="userGroups"
            lazy="false"
            cascade="save-update"
            sort="unsorted"
            order-by="user_id"
        >

              <key
                  column="group_id"
              >
              </key>

              <many-to-many
                  class="com.affiliate.bo.User"
                  column="user_id"
                  outer-join="auto"
               />

        </set>

        <!--
            To add non XDoclet property mappings, create a file named
                hibernate-properties-Group.xml
            containing the additional properties and place it in your merge dir.
        -->

    </class>

</hibernate-mapping>


The editUser logic (Webwork):

Code:
package com.affiliate.controller.actions;

import java.util.HashSet;
import java.util.Set;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

import com.affiliate.bo.User;
import com.affiliate.exceptions.*;

public class EditUser extends BaseAction{

   private static final Log logger = LogFactory.getLog(EditUser.class);
   private String userId;
   private String firstName;
   private String lastName;
   private String password;
   private String password2;
   private String telephone;
   private String title;
   private String[] group;
   
   private boolean isSubmitted = false;
   private User user; //the persisted user instance
   
   public String execute(){
      if(userId == null){
         if(logger.isWarnEnabled()){
            logger.warn("No user id specified");
         }
         return INPUT;
      }
      
      try{
         user = getAffService().getUser(Long.valueOf(userId));
      }
      catch(ResourceNotFoundException e){
         if(logger.isWarnEnabled()){
            logger.warn("Attempt to load user with id " + userId + " failed");
            addActionError("User by id; " + userId + " not found");
         }
         return INPUT;
      }

      //collate the group selections
      Set groupSet = new HashSet();
      if(group != null){
         for(int i = 0;i < group.length;i++){
            try{
               groupSet.add(getAffService().getGroup(Long.valueOf(group[i])));
            }
            catch(ResourceNotFoundException e){
               if(logger.isWarnEnabled()){
                  logger.warn("Attempt to load group with id " + group[i] + " failed");
               }
               return INPUT;
            }
         }
      }
      
      //set the fields
      user.setFirstName(firstName);
      user.setLastName(lastName);
      user.setTitle(title);
      user.setTelephone(telephone);
      if(password != null){ user.setPassword(password);}
      user.setGroups(groupSet);
      
      try{
         getAffService().updateUser(user);
      }
      catch(PersistenceException e){
         if(logger.isErrorEnabled()){
            logger.error("Couldn't update user record: " + userId + " " + e.getMessage());
         }
         return INPUT;
      }
      
      return SUCCESS;
   }
   
   public String getUserId(){
      return userId;
   }
   public void setUserId(String userId){
      this.userId = userId;
   }

   public String getFirstName() {
      return firstName;
   }

   public void setFirstName(String firstName) {
      this.firstName = firstName;
   }

   public String[] getGroup() {
      return group;
   }

   public void setGroup(String[] group) {
      this.group = group;
   }

   public String getLastName() {
      return lastName;
   }

   public void setLastName(String lastName) {
      this.lastName = lastName;
   }

   public String getPassword() {
      return password;
   }

   public void setPassword(String password) {
      this.password = password;
   }

   public String getPassword2() {
      return password2;
   }

   public void setPassword2(String password2) {
      this.password2 = password2;
   }

   public String getTelephone() {
      return telephone;
   }

   public void setTelephone(String telephone) {
      this.telephone = telephone;
   }

   public String getTitle() {
      return title;
   }

   public void setTitle(String title) {
      this.title = title;
   }
   
   public boolean getIsSubmitted(){
      return isSubmitted;
   }
   public void setIsSubmitted(boolean submitted){
      this.isSubmitted = submitted;
   }
   
}


The logs showing the exception:

Code:
9:46:08,678 ERROR EditUser,TP-Processor5:75 - Couldn't update user record: 1 a different object with the same identifier value was already associated with the session: 1, of class: com.affiliate.bo.User; nested exception is net.sf.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: 1, of class: com.affiliate.bo.User


Any help would be marvellous.


Top
 Profile  
 
 Post subject:
PostPosted: Wed Nov 02, 2005 4:04 pm 
Regular
Regular

Joined: Thu Oct 27, 2005 8:06 am
Posts: 55
Location: München, Germany
When getting this kind of error where SETs of objects are concerned, I'd first make sure the equals and hashCode methods of the referenced classes are implemented properly.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Nov 03, 2005 8:01 am 
Newbie

Joined: Wed Nov 02, 2005 8:13 am
Posts: 4
Yes, I understand the need for this but is not the cause of the error on this occasion.

I've considered evict()ing all the Groups related to the User prior to the update() to clear the session of the existing Group but I don't believe this should be necessary. Shouldn't hibernate be able to overlook this.

Btw, I'm using Spring's HibernateTemplate for my session control.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Nov 03, 2005 8:34 am 
Beginner
Beginner

Joined: Fri Oct 28, 2005 10:46 am
Posts: 37
Do you know that Session.update is just for reattaching a detached instance? If an entity is in the Session (due to a load or get or anything else) and its state is changed, the changes will be synchronized to the database when the Session is flushed.

Definition of entity states, essential to know when reading documentation:
http://www.hibernate.org/hib_docs/v3/re ... ure-states


Top
 Profile  
 
 Post subject:
PostPosted: Thu Nov 03, 2005 10:01 am 
Newbie

Joined: Wed Nov 02, 2005 8:13 am
Posts: 4
hmmmm. I've looked at that - i think this could be the problem. As I'm using Spring's HibernateTemplate, I've got to look at when the session is flushed and whether i can do it explicitely. Any ideas?


Top
 Profile  
 
 Post subject:
PostPosted: Fri Nov 04, 2005 8:14 am 
Beginner
Beginner

Joined: Fri Oct 28, 2005 10:46 am
Posts: 37
Well, there are a lot of variables. In the application I'm currently writing, we also use the HibernateTemplate via Spring's HibernateDAOSupport. We started off with an extremely simple model where there was no Session management beyond what the HibernateTemplate did for us. In general, our data access code looks something like this:
Code:
public Foo findById(int id) {
    return (Foo) getHibernateTemplate().load(Foo.class, id);
}

With our early model, this would result in a new Session's being opened, the object's being read, and the Session's being closed, all within the span of the one method call. That's part of what the HibernateTemplate does for you. It's also listed in the documentation (reference guide I think) as an anti-pattern. So recently, I've been experimenting with other ways of managing things.

Right now I'm trying out Spring's OpenSessionInViewFilter, which lets me easily implement session-per-request. So all data access within one request is part of the same Session, which is flushed at the end of the request. Therefore all I have to do is load an object and change it. Then at the end of the request, the database will be updated. I also never have to call Session.update because the object is still persistent and not detached.

You'll find the OpenSessionInViewFilter in the ...orm.hibernate(3).support package. Here is the Hibernate 3 version. It is a Servlet Filter that opens a Session at the beginning of the request, binding it to a ThreadLocal, and closes it at the end of the request. From reading the API docs for it, you'll see that by default it creates a Session with FlushMode.NEVER and doesn't flush it at the end of the request. This (the FlushMode.NEVER) means that the HibernateTemplate won't allow any write operations to be performed with the Session. However you can extend the Filter and override a couple of methods to get a writable Session and flush it at the end of the request. This is also mentioned in the docs. That will give you the session-per-request pattern. Then of course you have to consider what you'll do if a data access exception is thrown during a request because a Session is intended to be discarded immediately in that case. If you carry on with the same Session, the user might see corrupt data.

You can also use the OpenSessionInViewFilter in non-singleSession mode. You can read about that in the API docs because I haven't tried it out yet, and all I know is what I've read. I'll also mention that if your problem actually is that you're doing a get/load and an update of the same object in the same Session, you might consider trying a newer version of Hibernate. In 3.0.5 and possibly as early as 3.0.1, it seems that this situation no longer causes an exception. When I switched to Session per request, I was prepare to have to resolve hundreds, if not thousands, of exceptions because our code is littered with things like:
Code:
Foo foo = fooDao.findById(id);
foo.setX(x);
fooDao.store(foo);

In session-per-request, that is a load and an update of the same object in the same Session, which according to the docs, should throw an exception like the one you're getting. In 3.0.5, it doesn't happen. In tracing through the code, it seems that an update first checks to see if the object is associated with the Session and just returns if it is. No exception. I still plan to remove as many unnecessary update calls as I can to keep the code clean and as streamlined as possible.

Recommended reading:
http://www.hibernate.org/43.html
http://www.hibernate.org/hib_docs/v3/re ... tions.html (parts of it)

I hope this is helpful and not too much info.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Nov 04, 2005 12:24 pm 
Newbie

Joined: Wed Nov 02, 2005 8:13 am
Posts: 4
You are the boy!

I'll get to work upgrading to 3 straight away. Ta muchly.


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.