-->
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.  [ 8 posts ] 
Author Message
 Post subject: Optimistic Lock: version Parent when Child is updated
PostPosted: Wed Sep 24, 2008 4:35 am 
Beginner
Beginner

Joined: Mon Jul 05, 2004 9:29 am
Posts: 38
Hi,

currently in Hibernate with a typical (bidirectional & mapped) Parent-Child relationship, the parent gets versioned when a child is added or removed from it.

I would like to know if it is possible to version the parent when a child is updated without artificially updating a dummy property of the parent.

I'd like to do that to preserve an objects' graph coherence (invariants).

example:

I have the following entities:

[Order] 1 --- * [OrderLine] * --- 1 [Product]

and have a rule (at the order level) that the total amount for an order (= sum quantity * price for each line) must not be greater than $5000.

if two users concurrently modify the same order by changing the quantity of 2 different lines, the last one committing won't see the changes made by the first one and won't be able to enforce the invariants.

however, if the order itself is versioned at each commit, the last one committing will get an OptimisticLockException (and the order won't be corrupted).

is there any way to specify a sort of cascaded versioning in the mapping of hibernate or should it be a new feature request.

Thanks,

Xavier.


Top
 Profile  
 
 Post subject:
PostPosted: Wed Sep 24, 2008 3:30 pm 
Red Hat Associate
Red Hat Associate

Joined: Mon Aug 16, 2004 11:14 am
Posts: 253
Location: Raleigh, NC
Try session.load(Order.class, ..., LockMode.FORCE) which will force a version increment on the Order. Does that fit the bill?

-Chris

_________________
Chris Bredesen
Senior Software Maintenance Engineer, JBoss


Top
 Profile  
 
 Post subject:
PostPosted: Thu Sep 25, 2008 2:49 am 
Beginner
Beginner

Joined: Mon Jul 05, 2004 9:29 am
Posts: 38
Thanks, that's a start... but I was thinking more of something declarative (xml or annotation mapping) rather than programative: if I load the order in an extended persistence context and let the user change whatever he wants (including changing nothing), I don't want the order to be versioned if he didn't change anything. So, changing a line would lead to version the order whereas changing nothing won't. I think that using Session#get|load(..., LockMode.FORCE) will anyway increment the order's version whether the user has changed anything or not.

What I would like to see in Hibernate is the ability to manage a version for a graph/aggregate of objects by having a single version property on the root of that graph/aggregate. Every modification inside that graph/aggregate will increment the root's version.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Sep 26, 2008 2:20 am 
Beginner
Beginner

Joined: Mon Jul 05, 2004 9:29 am
Posts: 38
Thanks for the help, I created a specific Annotation to be put on parent property inside its children and a PreUpdateEventListener which locks (FORCE) the parent when a child is updated. This way, I don't have to explicitly have to lock the parent in my code.


Top
 Profile  
 
 Post subject:
PostPosted: Sat Sep 27, 2008 6:53 am 
Pro
Pro

Joined: Tue Jun 12, 2007 4:13 am
Posts: 209
Location: Berlin, Germany
kalgon wrote:
Thanks for the help, I created a specific Annotation to be put on parent property inside its children and a PreUpdateEventListener which locks (FORCE) the parent when a child is updated. This way, I don't have to explicitly have to lock the parent in my code.

Hi Kalgon,

could you please post your solution here? I think it can be sometimes very useful for all of us!

Carlo


Top
 Profile  
 
 Post subject:
PostPosted: Mon Sep 29, 2008 3:06 am 
Beginner
Beginner

Joined: Mon Jul 05, 2004 9:29 am
Posts: 38
I'll post the code as soon as I figure out why hibernate validator doesn't fire when lock() is called inside the PreUpdateListener.


Top
 Profile  
 
 Post subject:
PostPosted: Mon Sep 29, 2008 4:16 am 
Beginner
Beginner

Joined: Mon Jul 05, 2004 9:29 am
Posts: 38
Here is my solution so far: if someone with a greater knowledge of hibernate's internals could take a look at it and make it safe, that would be great!

Code:

Code:
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target( {METHOD, FIELD})
public @interface CascadedVersion {}


Code:
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import org.hibernate.EntityMode;
import org.hibernate.LockMode;
import org.hibernate.annotations.common.reflection.ReflectionManager;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.annotations.common.reflection.java.JavaReflectionManager;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.EntityEntry;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.event.EventSource;
import org.hibernate.event.Initializable;
import org.hibernate.event.PreUpdateEvent;
import org.hibernate.event.PreUpdateEventListener;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.persister.entity.EntityPersister;

public class CascadedVersionEventListener implements PreUpdateEventListener, Initializable {

    private static final ReflectionManager manager = new JavaReflectionManager();

    private static final long serialVersionUID = 1L;

    private final Map<String, Set<String>> cascadeProperties = new HashMap<String, Set<String>>();

    public void initialize(Configuration configuration) {

        Iterator<?> persistentClassIterator = configuration.getClassMappings();

        while (persistentClassIterator.hasNext()) {

            PersistentClass persistentClass = PersistentClass.class.cast(persistentClassIterator.next());
            Set<String> properties = new HashSet<String>();
            Iterator<?> propertyIterator = persistentClass.getReferenceablePropertyIterator();

            while (propertyIterator.hasNext()) {

                Property property = Property.class.cast(propertyIterator.next());

                if (isCascadedVersion(property)) {

                    properties.add(property.getName());
                }
            }

            this.cascadeProperties.put(persistentClass.getEntityName(), properties);
        }
    }

    public boolean onPreUpdate(PreUpdateEvent event) {

        Set<Object> checkedEntities = new HashSet<Object>();
        Queue<Object> entitiesToCheck = new LinkedList<Object>();
        entitiesToCheck.add(event.getEntity());

        while (!entitiesToCheck.isEmpty()) {

            Object entity = entitiesToCheck.poll();
            String entityName = entity.getClass().getName();

            if (checkedEntities.add(entity)) {

                EventSource source = event.getSession();
                PersistenceContext context = source.getPersistenceContext();
                EntityPersister persister = source.getEntityPersister(entityName, entity);
                EntityMode mode = persister.guessEntityMode(entity);
                EntityEntry entry = context.getEntry(entity);
                Object[] oldState = entry.getLoadedState();
                Object[] newState = persister.getPropertyValues(entity, mode);
                boolean dirty = persister.findDirty(oldState, newState, entity, event.getSession()) != null;
                boolean versionable = persister.getVersionProperty() >= 0;

                if (versionable && !dirty) {

                    Serializable id = persister.getIdentifier(entity, mode);

                    if (entity != event.getEntity()) {

                        PreUpdateEvent event2 = new PreUpdateEvent(entity, id, newState, oldState, persister, source);

                        // force hibernate validator here...
                        for (PreUpdateEventListener listener : source.getListeners().getPreUpdateEventListeners()) {

                            listener.onPreUpdate(event2);
                        }

                        source.lock(entity, LockMode.FORCE);
                    }
                }

                for (String propertyName : this.cascadeProperties.get(entityName)) {

                    entitiesToCheck.offer(persister.getPropertyValue(entity, propertyName, mode));
                }
            }
        }

        return false;
    }

    private boolean isCascadedVersion(Property property) {

        Class<?> mappedClass = property.getPersistentClass().getMappedClass();
        XClass xClass = manager.toXClass(mappedClass);

        for (XProperty xProperty : xClass.getDeclaredProperties(property.getPropertyAccessorName())) {

            if (xProperty.getName().equals(property.getName()) && xProperty.isAnnotationPresent(CascadedVersion.class)) {

                return true;
            }
        }

        return false;
    }
}


Test:

Code:
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Version;

import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.hibernate.validator.AssertTrue;

@Entity
@Table(name = "ORDERS")
public class Order {

    @Id
    @GeneratedValue
    Long id;

    @Version
    Integer version;

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
    @Cascade( {CascadeType.ALL, CascadeType.DELETE_ORPHAN})
    private Set<OrderLine> lines = new HashSet<OrderLine>();

    public void add(Product product, int quantity) {
        Iterator<OrderLine> iterator = this.lines.iterator();
        while (iterator.hasNext()) {
            OrderLine line = iterator.next();
            if (line.getProduct().equals(product)) {
                line.addQuantity(quantity);
                if (line.getQuantity() <= 0) {
                    iterator.remove();
                }
                return;
            }
        }
        this.lines.add(new OrderLine(this, product, quantity));
    }

    public Set<OrderLine> getLines() {
        return Collections.unmodifiableSet(this.lines);
    }

    public double getTotal() {
        double total = 0;
        for (OrderLine line : this.lines) {
            total += line.getSubTotal();
        }
        return total;
    }

    @AssertTrue
    boolean checkTotal() {
        return getTotal() < 150;
    }
}


Code:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import org.hibernate.annotations.NaturalId;

@Entity
@Table(name = "ORDER_LINES")
public class OrderLine {

    @Id
    @GeneratedValue
    Long id;

    @ManyToOne
    @JoinColumn
    @NaturalId
    @CascadedVersion
    Order order;

    @ManyToOne
    @JoinColumn
    @NaturalId
    private Product product;

    private int quantity;

    protected OrderLine(Order order, Product product, int quantity) {
        this.order = order;
        this.quantity = quantity;
        this.product = product;
    }

    public boolean equals(Object that) {
        return super.equals(that) || getClass().isInstance(that) && equals(getClass().cast(that));
    }

    public Product getProduct() {
        return this.product;
    }

    public int getQuantity() {
        return this.quantity;
    }

    public double getSubTotal() {
        return this.quantity * this.product.getPrice();
    }

    public int hashCode() {
        return this.order.hashCode() ^ this.product.hashCode();
    }

    void addQuantity(int quantity) {
        this.quantity += quantity;
    }

    private boolean equals(OrderLine that) {
        return this.order.equals(that.order) && this.product.equals(that.product);
    }
}


Code:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;

import org.hibernate.annotations.NaturalId;

@Entity
@Table(name = "PRODUCTS")
public class Product {

    @Id
    @GeneratedValue
    Long id;

    @Version
    Integer version;

    @NaturalId
    private String name;

    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public boolean equals(Object that) {
        return super.equals(that) || getClass().isInstance(that) && equals(getClass().cast(that));
    }

    public String getName() {
        return this.name;
    }

    public double getPrice() {
        return this.price;
    }

    public int hashCode() {
        return this.name.hashCode();
    }

    private boolean equals(Product that) {
        return this.name.equals(that.name);
    }
}


Code:
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class Main {

    public static void main(String[] args) {

        EntityManagerFactory factory = Persistence.createEntityManagerFactory("default");

        // Create initial data: 2 products + 1 order
        EntityManager manager = factory.createEntityManager();
        Product product1 = new Product("hibernate in action", 45f);
        Product product2 = new Product("java persistence with hibernate", 50f);
        Order order = new Order();
        order.add(product1, 1);
        order.add(product2, 1);
        manager.getTransaction().begin();
        manager.persist(product1);
        manager.persist(product2);
        manager.persist(order);
        manager.getTransaction().commit();
        manager.close();

        // Reload the order 2x within different managers
        EntityManager manager1 = factory.createEntityManager();
        EntityManager manager2 = factory.createEntityManager();
        Order order1 = manager1.find(Order.class, 1L);
        Order order2 = manager2.find(Order.class, 1L);

        // Modify the order concurrently
        order1.add(product1, 1);
        order2.add(product2, 1);

        // Commit the concurrent changes
        manager1.getTransaction().begin();
        manager2.getTransaction().begin();
        manager1.getTransaction().commit();
        try {
            manager2.getTransaction().commit();
        } catch (RuntimeException e) {
            System.err.println(e.getMessage());
        }
        manager1.close();
        manager2.close();

        // Reload the order one last time within a new manager
        EntityManager manager3 = factory.createEntityManager();
        Order order3 = manager3.find(Order.class, 1L);
        manager3.close();

        // Assert the total is < 150
        System.out.println("total price: " + order3.getTotal());
        System.out.println("invariant status: " + (order3.getTotal() < 150 ? "OK" : "KO"));

    }
}


in persistence.xml

Code:
<property name="hibernate.ejb.event.pre-update" value="CascadedVersionEventListener" />


Add or remove the CascadedVersion on OrderLine.order to see the effect.

I hope it'll help...

Xavier.


Top
 Profile  
 
 Post subject:
PostPosted: Mon Sep 29, 2008 5:03 am 
Pro
Pro

Joined: Tue Jun 12, 2007 4:13 am
Posts: 209
Location: Berlin, Germany
Hi Kalgon,

great work - thanks for publishing here!

Carlo


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