Optimistic locking: prevent version increment on entity collection attribute

#1

Hello,

I have an entity E with an attribute which is a collection of entities (one to many relationship) with an optimistic locking strategy supported by a version field. Is it possible to prevent the increment of the version of entity E when entities are added to or removed from the collection? Something like the equivalent of @OptimisticLock(excluded=true) in Hibernate?

Thanks,

Farid

#2

If it is a bidirectional relationship that is owned by the entities in the collection (i.e. the collection is defined as a mapped by) then adding and removing entities should not change the version. Otherwise the version will be updated, and currently you cannot disable this update.

Could you please explain the reason for avoiding this object version update?

ObjectDB Support
#3

Thank you for this. I will try it and will let you know.

In the case of an order <-> order line relationship, it makes sense for the version of the parent entity to be updated because order lines are an intrinsic part of the order. In the case of a document <-> readers of that document, it makes less sense (in my case there is a limited number of readers).

Hope this makes sense,

Regards,

farid

#4

It makes sense. In that case a bidirectional relationship between Document and Reader, in which the Reader is the owner, may be appropriate. It should solve the version update problem, because a document will not store any details on readers (the relationship is stored on the reader side).

Regarding performance, it may improve performance of adding / removing readers, but traversal from a document to its readers is expected to be slower, because it will require executing a query. Adding an index on the owner side could make this query run faster.

ObjectDB Support
#5

Hello, unfortunately I seem to have some issues. I have changed my classes to have a OneTomany mappedBy relationship as follows. When testing this in JUnit (see test() method below) it all works fine and I get my collection back. When I use it in my web app it fails; the collection is null!

In the application (web app), I create the readings in one call and I try to read them in another call. I checked the database and all the entities are created (document and readings) but when I try to get the collection of readings null is returned.

I have tried to clear the entity manager in the second call to make sure the reading will be done from the DB but no success.

I have tried to add ManyToOne of the owning side (Readings), added EAGER fetch to the collection no change.

I have tried to persist the document first and then each Reading separately but still no result.

I am guessing this must be an issue with the EntityManager's life cycle but cannot figure out what. I am using Guice injection and Guice persist and should get one EntityManager per request.

Also, please note that in the web app I actually get the documents through a query of this type:

getEntityManager().createQuery("select e from Document e", Document.class).getResultList()

and then loop through the result.

Hope you can help.

Thanks

@Entity
public class Document {
  
    // ID is generated by the database
    @Id private long id;
  
    @OneToMany(mappedBy="document")
    private ArrayList<Reading> readings = new ArrayList<Reading>();
  
    public long getId() {
        return id;
    }
  
    public ArrayList<Reading> getReadings() {
        return readings;
    }
  
    public void addReading(Reading reading) {
        this.readings.add(reading);
    }
}
@Entity
public class Reading {
  
    // ID is generated by the database
    @Id private long id;
    private Document document = null;
  
    public Reading(Document document) {
        this.document = document;
        this.document.addReading(this);
    }
  
    public long getId() {
        return id;
    }
        public Document getDocument() {
        return document;
    }
}
public void test() {
    // em is the EntityManager
    em.getTransaction().begin();
  
    Document doc = new Document();
  
    // Note that these readings will be added to the
    // document in the Reading constructor
  
    Reading reading1 = new Reading(doc);
    Reading reading2 = new Reading(doc);
    em.persist(doc);
  
    em.getTransaction().commit();
  
    Document tmpDoc = em.find(Document.class, doc.getId());
    ArrayList<Reading> readings = tmpDoc.getReadings();
  
    // Both these turn out to be false!
    assertTrue(readings != null);
    assertTrue(readings.size() == 2);
}

 

 

#6

> In the application (web app), I create the readings in one call and I try to read them in another call.

What happens when you do that in the same call?

If the problem is that the entity is detached between the calls, check this issue and the no-detach solution.

> I have tried to clear the entity manager in the second call to make sure the reading will be done from the DB but no success.

Actually clear detaches managed objects and could make things harder in this case.

> I have tried to add ManyToOne of the owning side (Readings),

This should not make any difference.

> added EAGER fetch to the collection no change.

This should affect. An eager collection should be loaded with the containing entity. Check what the object contains in the debugger, and try to access the collection when the object is retrieved.

ObjectDB Support
#7

Hello, thank you for your reply.

I have added some code to re-retrieve the document entity in the initial call and all works fine. The Reading entities are all in the collection. This makes sense because I populate the collection myself in the Reading's constructor and we are still in the same transaction (before any commit or rollback).

In the debugger, when I check the document instance in the second call, the collection of readings is null but the rest of the document object is all there. It is as if ObjectDB was not executing the select statement to populate the inverse collection (this even with an EAGER read).

Please note that the operations being executed in 2 separate calls and my EntityManagers being per request I should have a different EntityManager for each call anyway so the clearing of the EntityManager should not make any difference.

Also, each of my calls to the server is made in a transaction (the entry point in the servlet is annotated with @Transaction for that purpose) so not sure how Documents found by query in that transaction could be detached when I try to access the collection with the same EntityManager in the same transaction.

Finally I have tested it with the no-detached property set to true (System.setProperty("objectdb.temp.no-detach", "true");). This has not made any difference. The collection of readings is still null.

I am obviously doing something wrong but cannot figure out what that could be. This is intriguing especially when the data is all there in the database. The collection of Readings in the Document is null but I guess this is how inverse relationships work.

Any other suggestion?

Thanks

#8

Please try to create a simple console test. Unlike the test in #5 that passes, the test should reflect the way that your web application works, i.e. using different transactions and maybe different entity managers, etc.

ObjectDB Support
#9

Ok, here is a test. This is the same as the first example:

- Document class

- Reading class

- The Test class with a test method that creates a document, adds 2 Readings to it and commits the transaction.

- I then clear the EntityManager so I don't have the cached Document and then find the Document by Id.

- At that point the Document is found but the collection of Readings is null. If I don't clear the EntityManager it all works fine because I have the cached value of the Document returned.

 

@Entity
public class Document {
   
    private static long ID_SEQ = 0;
   
    @Id private long id = ID_SEQ++;

    @OneToMany(mappedBy="document")
    private ArrayList<Reading> readings = new ArrayList<Reading>();

    public long getId() {
        return id;
    }

    public ArrayList<Reading> getReadings() {
        return readings;
    }

    public void addReading(Reading reading) {
        this.readings.add(reading);
    }
}
@Entity
public class Reading {
   
    private static long ID_SEQ = 0;
   
    @Id private long id = ID_SEQ++;
    private Document document = null;

    public Reading(Document document) {
        this.document = document;
        this.document.addReading(this);
    }

    public long getId() {
        return id;
    }
        public Document getDocument() {
        return document;
    }
}
/******************* Test class **************************/

public class Test {

    @org.junit.Test
    public void test() throws IOException {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("./test.odb;drop");
        EntityManager em = emf.createEntityManager();
       
        em.getTransaction().begin();
       
        Document doc = new Document();
       
        // Note that these readings will be added to the
        // document in the Reading constructor
        Reading reading1 = new Reading(doc);
        Reading reading2 = new Reading(doc);
        em.persist(doc);
    
        em.getTransaction().commit();
    
        // If we comment this out all works fine because I populate
        // the collection of Readings myself in the constructor
        em.clear();
       
        Document tmpDoc = em.find(Document.class, doc.getId());
        ArrayList<Reading> readings = tmpDoc.getReadings();
    
        // This works fine
        assertTrue(tmpDoc != null);
       
        // This turn out to be false!
        assertTrue(readings != null);
       
        em.close();
        emf.close();
    }
}

Let me know what you think,

Thanks,

Farid

#10

Your test passes after fixing the relationship on the Document side to:

    @OneToMany(mappedBy="document", cascade=CascadeType.PERSIST)
    private List<Reading> readings = new ArrayList<Reading>();
  • The cascade mode is required in order to persist the Reading instances with the Document instance (this is probably a problem only in the test and not in your application).
  • The type of the readings field must be List rather than ArrayList. This is a JPA request, which is also mentioned in the manual, but following this report an error message, which is missing in this case, should be added.
ObjectDB Support
#11

Ok, thank you for that. I have cascade persist set at the database configuration level so this is not the problem. On the other hand, changing ArrayList to List in the declaration of the collection fixed the issue!

thank you very much for your great help.

farid

#12

Hello, I have one more question regarding this. Because Reading is the owner part of the relationship and I have no cascading of REMOVE, clearing the collection of Readings in a Document should clear the collection and not delete any of the Readings from the DB. Also, refreshing or reading the Document from the DB with a new EntityManager (or cleared one) should re-retrieve the original collection of readings.

This is not the behaviour I am observing. In my case, clearing the collection of Readings in the Document entity removes the Readings from the DB as if I had a cascading remove with orphan remove = true.

 

I have joined a simple test for you. Thank you for your help and sorry in advance if I missed something obvious!

@Entity
public class Document {

    private static long ID_SEQ = 0;

    @Id private long id = ID_SEQ++;

    @OneToMany(mappedBy="document", cascade=CascadeType.PERSIST)
    private List<Reading> readings = new ArrayList<Reading>();

    public long getId() {
        return id;
    }

    public List<Reading> getReadings() {
        return readings;
    }

    public void addReading(Reading reading) {
        this.readings.add(reading);
    }
}
@Entity
public class Reading {

    private static long ID_SEQ = 0;

    @Id private long id = ID_SEQ++;
    private Document document = null;

    public Reading(Document document) {
        this.document = document;
        this.document.addReading(this);
    }

    public long getId() {
        return id;
    }
        public Document getDocument() {
        return document;
    }
}
@org.junit.Test
public void test() throws IOException {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("./test.odb;drop");
    EntityManager em = emf.createEntityManager();

    em.getTransaction().begin();

    Document doc = new Document();

    // Note that these readings will be added to the
    // document in the Reading constructor
    Reading reading1 = new Reading(doc);
    Reading reading2 = new Reading(doc);
    em.persist(doc);

    em.getTransaction().commit();

    // Just to make sure the readings are from the DB
    em.clear();

    em.getTransaction().begin();

    Document tmpDoc = em.find(Document.class, doc.getId());
    // This works fine
    assertTrue(tmpDoc.getReadings().size() == 2);

    tmpDoc.getReadings().clear();
    em.merge(tmpDoc);
    // This is fine as well but should get the collection back from the DB
    assertTrue(tmpDoc.getReadings().size() == 0);

    em.getTransaction().commit();
   
    em.clear();
   
    Document finalDoc = em.find(Document.class, doc.getId());
    // This is false!
    assertTrue(finalDoc.getReadings().size() == 2);

    em.close();
    emf.close();
}
#13

Thank you for the test. orphanRemoval=true was wrongly applied by default to mapped by relationships.

Please try build 2.5.0_05 that should fix it.

ObjectDB Support
#14

I will try that.

Thanks

Farid

Reply