-->
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.  [ 6 posts ] 
Author Message
 Post subject: A problem more persistent than Hibernate's persistence
PostPosted: Tue May 15, 2007 3:58 pm 
Newbie

Joined: Sun Feb 25, 2007 12:46 pm
Posts: 4
Hi everyone,

The situation is as follows:
I have an object "Invoice" which contains a list of Product objects mapped as follows:

Code:

<class name="Invoice" table="invoices">
<id name="id" column="id" unsaved-value="-1">
            <generator class="native"/>
</id>
...
<list name="products" table="invoice_products_join" lazy="false" cascade="all-delete-orphan">
            <key column="invoice_id" not-null="true"/>
            <list-index column="id"/>
            <many-to-many class="Product" column="product_id" unique="true"/>               
        </list>
</class>
<class name="Product" table="invoice_products">
        <id name="id" column="id" unsaved-value="-1">
            <generator class="native"/>
        </id> 
        <property name="quantity" />
        <property name="netPrice" column="net_price"/>
</class>


Note that I use a table to hold invoices (invoices), another table to hold products (invoice_products) and a join table invoice_products_join that holds invoice_id, product_id and list index id to associate products with invoices.

My requirement is to load an Invoice object along with all its properties, then duplicate this object and after changing it in UI, save the duplicated invoice and its products in a new records without altering the originals.

In my approach, I copy the properties of the invoice into an new Invoice object, along with the Product list and then set the invoice and its products' id to -1 (unsaved value). When I try to save the product, I get an exception:
org.hibernate.HibernateException: Don't change the reference to a collection with cascade="all-delete-orphan": entities.Invoice.products
at org.hibernate.engine.Collections.prepareCollectionForUpdate(Collections.java:226)
..
...

A question then raises. Since the invoice and its products IDs are set to the default unsaved value (-1), how does Hibernate figures out that the collection already exists in db when trying to save it?

I have also tried to change the product list cascade property to "all" instead of "all-delete-orphan" and manually delete unreferenced child-product objects. Hibernate then does save the duplicated invoice and its products in new records, keeping the original invoice untouched, but the associated products of the original are deleted from "invoice_products" and "invoice_products_join" tables.

I save or update invoice objects using the following method:

Code:
Transaction tx = null;
        Session session = InitSessionFactory.getInstance().openSession();
        try {
            if (originalInvoice == null ){
                tx = session.beginTransaction();               
                for (int i=0;i<invoice.getProducts().size();i++) {
                    session.save(invoice.getProducts().get(i));                   
                    productLog.debug(invoice.getProducts().get(i));
                }       
                session.save(newInvoice);
                //session.flush();                               
            } else {               
                tx = session.beginTransaction();
                for (int i=0;i<invoice.getProducts().size();i++) {
                    session.update(invoice.getProducts().get(i));
                    productLog.debug(invoice.getProducts().get(i));
                }
                session.update(invoice);
                //session.flush();
            }
            tx.commit();
            session.close()



I have stuck with that a couple of days now so any help or suggestion would be really appreciated.

-00-
Dim


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 16, 2007 1:24 am 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
You've omitted posting the important bit: how do you copy the list of products? newInvoice.setProducts(oldInvoice.getProducts()) won't work, as the collection is persistent. newInvoice.getProducts().addAll(oldInvoice.getProducts()) also won't work, as all the old products are persistent. You'll need to make copies of all the old products, and add them to the new invoice's new products list. You can't reuse any objects (including the list) at all.

_________________
Code tags are your friend. Know them and use them.


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 16, 2007 2:32 am 
Newbie

Joined: Tue Aug 08, 2006 3:53 am
Posts: 17
Location: Tanznaia
aSiMaS,

As far as I know, copying the collectio contents into a new collection and then applying that collection to the new duplicate should work. You may have problems if your Invoice class also has other LazyInitialised properties, as they might not be loaded properly if your ID get's changed. I'd recommend a clone() function on the Invoice setting all the properties and the list contents.

something like this:
Code:
public Object clone() {
     Invoice newInv = new Invoice(getClientName(), getRefNumber(), etc...);
     newInv.getProducts().addAll(getProducts());
     return newInv;
}


I suppose you may be able to get hibernate to do some of the work for you, although this could get sticky...

Code:
     Invoice inv = mySession.get(eg.Invoice, 1234);
     Session.initialize(inv); //to ensure no Lazy Loading problems...

     inv.setId(-1); //or whatever the unsaved value is (maybe null?)
     List products = new ArrayList();
     products.addAll(getProducts());
     inv.setProducts(products);

     Session.save(inv);


You may possibly have a problem if your final Invoice object is still wrapped up in the original hibernate proxy when you come to save it - but you should be able to unwrap it.

All in all, a proper programatic 'clone' function is the best solution as it works independantly of Hibernate (think unit testing...) and is just plain cleaner. It would allow better encapsulation. On invoice you should normally make the get/setProducts functions protected (hibernate can still use them) and implement some functions within invoice to manipulate them. This would make a clone function essential.

I hope this helps...

Ben.[/code]


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 16, 2007 6:21 am 
Newbie

Joined: Sun Feb 25, 2007 12:46 pm
Posts: 4
tenwit wrote:
You've omitted posting the important bit: how do you copy the list of products? newInvoice.setProducts(oldInvoice.getProducts()) won't work, as the collection is persistent. newInvoice.getProducts().addAll(oldInvoice.getProducts()) also won't work, as all the old products are persistent. You'll need to make copies of all the old products, and add them to the new invoice's new products list. You can't reuse any objects (including the list) at all.


First, i evict the invoice and its products objects from session, then I create a deep copy of products and assign them to a new productList, finally I create a deep copy of the invoice and then I set the invoice products.

Code:
invoiceManager.evictInvoiceFromSession(currentInvoiceRecord);
productList = oldnvoice.getProducts();
for (int i=0;i<productList.size();i++) {    productManager.evictProductFromSession(productList.get(i));
newProductList.add(productManager.productDeepCopyProduct(productList.get(i)));
}
newInvoice = invoiceManager.invoiceDeepCopy(oldInvoice);
newInvoice.setProducts(newProductList);


Invoice deep copy method.
Note that the invoice products are not coppied from the oldInvoice to the newInvoice.
Code:
public Invoice copyInvoice(Invoice invoiceTocopy) {
        Invoice newInvoice = new Invoice();       
        newInvoice .setCustomer(invoiceTocopy.getCustomer());
        newInvoice .setStatus(invoiceTocopy.getStatus());
        ...
        newInvoice .setId(-1);//Unsaved value
        return newInvoice ;


Products list deep copy method.
Code:
public Product copyProduct(Product productToCopy) {
        Product newProduct = new Product(productToCopy.parent);
        newProduct .setDiscount(productToCopy.getDiscount());
        ...
        newProduct .setQuantity(productToCopy.getQuantity());
        newProduct .setId(-1);
        return newProduct ;
    }


Besides the approach above I've tried to use HibernateBeanReplicator and create deep copies of the invoice but I get the same exception.

org.hibernate.HibernateException: Don't change the reference to a collection with cascade="all-delete-orphan": entities.Invoice.products
at org.hibernate.engine.Collections.prepareCollectionForUpdate(Collections.java:226)
..
...



benhathaway wrote:
You may possibly have a problem if your final Invoice object is still wrapped up in the original hibernate proxy when you come to save it - but you should be able to unwrap it.


benhathaway how do I unwrap the object from hibernate proxy? I've used evict to clear the object in session. But it didn't work.


Thanx for you replies.


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 16, 2007 7:47 am 
Newbie

Joined: Sun Feb 25, 2007 12:46 pm
Posts: 4
I managed to partialy solve the problem.
In the invoice.clone() method I set id=-1 in each of the products in the list.

Code:
newInvoice.getProducts().addAll(oldInvoice.getProducts());
for (int i=0;i<invoice.getProducts().size();i++)
       invoice.getProducts().get(i).setId(-1);


Now I get an object references an unsaved transient instance - save the transient instance before flushing: Invoice.Product exception.
But in the Invoice mapping I have set cascade="all-delete-orphan"> :
Code:
<list name="products" table="invoice_products_join" lazy="false" cascade="all-delete-orphan">
            <key column="invoice_id" not-null="true"/>
            <list-index column="id"/>
            <many-to-many class="Product" column="product_id" unique="true" lazy="false"/>               
        </list>


So it shouldn't require an explicit save of products before saving invoice. Even in explicit save, I get the same exception.

Code:
tx = session.beginTransaction();               
for (int i=0;i<invoice.getProducts().size();i++)
     session.save(invoice.getProducts().get(i));
session.save(invoice);
//session.flush();
tx.commit();


Top
 Profile  
 
 Post subject:
PostPosted: Wed May 16, 2007 5:15 pm 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
From the looks of that, I'd say that your exception isn't caused by the new invoice, but by the old one. Why are you evicting anything if you're making brand new objects to copy all the values into? Try doing everything that you're currently doing, but don't save any of the new objects. When you finally flush/commit, you'll get that exception, even though you would expect nothing to happen.

It's important to remember that persistent collections are separate objects. The object that contains them and the objects that they contain can all be evicted, but it might be possible to leave the collection itself in the session. The best way to avoid this sort of thing is to avoid evicting. I think that it's almost always possible to avoid evict() in production code. It's most useful in test code, where you're trying to reproduce strange circumstances.

_________________
Code tags are your friend. Know them and use them.


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 6 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.