Performance in large transactions

#1

Hello,

we have a performance problem when we use a large transaction.
Let's do exactly the same in several transactions the ObjectDB works much faster.

In our use case, we create many new entities and between the creations we call a select query. The execution of the query becomes slower at each iteration. (In the query, we search for an item of type Entity2. However, there is only one entity.)

 

Pseudocode for the fast and the slow solution

FAST:

for (i = 0; i <100; i ++) {
    EntityManager entityManager = getEntityManagerFactory().createEntityManager();
    entityManager.setFlushMode(FlushModeType.AUTO);
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin ();
    // the "problem" Query
    Entity2 x = //query (getSingleResult): "select e from Entity2 e where e.start <= "+i+" AND "+i+" <= vec.end"

    Collection<Entity> entities = create10000Entites();
    entityManager.merge(entities);

    x.end += 1;

    transaction.commit();
    transaction.close();
    entityManager.close();
}

SLOW:

EntityManager entityManager = getEntityManagerFactory().createEntityManager();
entityManager.setFlushMode(FlushModeType.AUTO);
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin ();
for (i = 0; i <100; i ++) {
    // the "problem" Query
    Entity2 x = //query (getSingleResult): "select e from Entity2 e where e.start <= "+i+" AND "+i+" <= vec.end"

    Collection<Entity> entities = create10000Entites();
    entityManager.merge(entities);

    //query
    x.end += 1;
}
transaction.commit();
transaction.close();
entityManager.close();

Can you give us help you understand it?

#2

Now we have an example that illustrates the behavior.

1) Import all Eclipse Project's

2) And then Run the JUnit Tests as PlugIn-Test "com.btc.ep.base.dal.tests.it". (Use the launcher: com.btc.ep.base.dal.tests.it)

3) The both relevant TestCases (test1_OneTransaction  and  test2_ManyTransactions) are contained in com.btc.ep.base.dal.tests.it.PerformanceTest

#3

PerformanceTest (and test1_OneTransaction  and  test2_ManyTransactions), mentioned in step 3, are not found in the projects.

Please follow the posting instructions and use the single class format for examples and test cases. It is much more difficult to work and understand test cases with complex structures and many non essential files, and this could only be justified when the project structure itself is the subject of the question or issue.

Regarding the question, running queries in a clean transaction, i.e. when there are no updates to the database since the last commit, is more efficient. If there are updates, ObjectDB may have to apply them before running the query, and in a multitasked application, these uncommitted updates may have to be applied each time again and again, in order to check their up to date validity and integrate them with other concurrent updates.

ObjectDB Support
#4

Attached a new Example in single class format:

 

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.OneToMany;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;

public class TransactionsPerformance {

static int steps = 10000;

public static void main(String[] args) {
  test_OneTransaction();
  test_ManyTransactions();
}

public static void test_OneTransaction() {
  EntityManagerFactory emf = Persistence
    .createEntityManagerFactory("objectdb:$objectdb/db/test.tmp;drop");

  EntityManager em = emf.createEntityManager();
  em.setFlushMode(FlushModeType.AUTO);
  em.getTransaction().begin();
  // SLOW
  long startTime = System.currentTimeMillis();

  MyEntity myEntity = new MyEntity();
  myEntity = em.merge(myEntity);

  for (int m = 0; m < steps; m++) {

   TypedQuery<MyEntity> q = em.createQuery("SELECT m FROM MyEntity m",
     MyEntity.class);
   myEntity = q.getSingleResult();

   MyEntityElement myEntityElement = new MyEntityElement();
   myEntityElement.getValues().addAll(createFloatArray(m));
   myEntity.getList().add(myEntityElement);
  }

  em.getTransaction().commit();
  em.close();
  System.out.println("test_OneTransaction  create time: "
    + (System.currentTimeMillis() - startTime));
  emf.close();
}

public static void test_ManyTransactions() {
  EntityManagerFactory emf = Persistence
    .createEntityManagerFactory("objectdb:$objectdb/db/test.tmp;drop");

  EntityManager em = emf.createEntityManager();
  em.setFlushMode(FlushModeType.AUTO);
  em.getTransaction().begin();
  // SLOW
  long startTime = System.currentTimeMillis();

  MyEntity myEntity = new MyEntity();
  myEntity = em.merge(myEntity);
  em.getTransaction().commit();
  em.close();

  for (int m = 0; m < steps; m++) {
   em = emf.createEntityManager();
   em.setFlushMode(FlushModeType.AUTO);
   em.getTransaction().begin();

   TypedQuery<MyEntity> q = em.createQuery("SELECT m FROM MyEntity m",
     MyEntity.class);
   myEntity = q.getSingleResult();

   MyEntityElement myEntityElement = new MyEntityElement();
   myEntityElement.getValues().addAll(createFloatArray(m));
   myEntity.getList().add(myEntityElement);

   em.getTransaction().commit();
   em.close();
  }
  System.out.println("test_ManyTransactions  create time: "
    + (System.currentTimeMillis() - startTime));
  emf.close();
}

private static List<Float> createFloatArray(float value) {
  int values = 1;
  List<Float> floats = new ArrayList<Float>(values);
  for (float i = 0; i < values; i++) {
   floats.add(value + (i / values));
  }
  return floats;
}

@Entity
public static class MyEntity {
  @OneToMany(cascade = CascadeType.ALL)
  private List<MyEntityElement> list = new ArrayList<>();

  public List<MyEntityElement> getList() {
   return list;
  }
}

@Entity
public static class MyEntityElement {
  @ElementCollection
  private List<Float> values = new ArrayList<>();

  public List<Float> getValues() {
   return values;
  }
}
}

Our exection times:

test_OneTransaction  create time: 58516
test_ManyTransactions  create time: 135564

#5

The test cases in #4 above demonstrates several topics:

Reusing EntityManager Instances

A first improvement of test_ManyTransactions is to reduce opening and closing entity managers.

Instead of:

    for (int m = 0; m < steps; m++) {
        em = emf.createEntityManager();
        em.setFlushMode(FlushModeType.AUTO);
            :
        em.close();
    }

use the following, if possible:

    em = emf.createEntityManager();
    em.setFlushMode(FlushModeType.AUTO);
    for (int m = 0; m < steps; m++) {
            :
    }
    em.close();

Embedded vs. Inverse Collections 

One of the differences between ObjectDB and ORM JPA is the ability of ObjectDB to manage to-many relationships as embedded collections, i.e. collections with content that is stored directly in entity objects. This could be very effective in many cases, but ineffective in others.

In these test cases it would be better to use conventional inverse (mapped by) collection in MyEntity:

    @Entity
    public static class MyEntity {
        @OneToMany(cascade = CascadeType.ALL, mappedBy = "ownerEntity")
        private List<MyEntityElement> list = new ArrayList();
       
        public List<MyEntityElement> getList() {
            return list;
        }
    }

    @Entity
    public static class MyEntityElement {
        MyEntity ownerEntity;

        @ElementCollection
        private List<Float> values;
       
        public void setValues(List<Float> values) {
            this.values = values;
        }

        public List<Float> getValues() {
            return values;
        }
    }

Note that the collection in MyEntity is inverse (mapped by) now but the collection in MyEntityElement, which is small, is still embedded. A large embedded collection, which is modified in high frequency (as in this test case) is inefficient, since every small change requires storing the entire large object again (a MyEntity instance with an embedded collection of up to 10,000 MyEntityElement references).

Untrackable New Objects

JPA implementations have to detect modifications to managed objects automatically in order to apply changes to the database on flush/commit. This could be done by keeping a copy of every managed object and comparing copies to actual objects on every flush or commit, but this is inefficient. Enhanced classes enable tracking object modification when it happens very efficiently. Detecting modification efficiently in collections, such as java.util.ArrayList is done by subclassing these types, so every loaded entity object will have fields of type objectdb.java.util.ArrayList (which can track modifications efficiently like an enhanced class) rather than java.util.ArrayList fields when loaded.

There is a problem however with a new managed object that has never been loaded from the database yet, and therefore includes java.util.ArrayList rather than objectdb.java.util.ArrayList. Usually the effect on performance is reasonable, but test_OneTransaction demonstrates an extreme case in which up to 10,000 new MyEntityElement instances in the persistence context are untrackable and requires comparison on every flush.

To avoid this, we introduce now a new method, con.objectdb.Utilities.newTrackable(em, cls) in ObjectDB 2.6.1_03:

    MyEntityElement myEntityElement = new MyEntityElement();
    myEntityElement.setValues(Utilities.newTrackable(em, ArrayList.class));

Instead of using java.util.ArrayList (which is untrackable) a user can build a new entity object with the trackable version. Using this method is recommended if the object is expected to remain in a live persistence context after first persistence for a long time.

We will try to demonstrate these ideas in separate posts.

 

ObjectDB Support
#6

Following is the revised test case from #4 above:

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.OneToMany;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;

import com.objectdb.*;

public class T1622b {

    static int steps = 10000;

    public static void main(String[] args) throws Exception {
        test_OneTransaction();
        test_ManyTransactions();
    }

    public static void test_OneTransaction() {
        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory(
                "objectdb:$objectdb/db/test.tmp;drop");

        EntityManager em = emf.createEntityManager();
        em.setFlushMode(FlushModeType.AUTO);
        em.getTransaction().begin();

        long startTime = System.currentTimeMillis();

        MyEntity myEntity = new MyEntity();
        myEntity = em.merge(myEntity);

        for (int m = 0; m < steps; m++) {

            TypedQuery<MyEntity> q = em.createQuery(
                "SELECT m FROM MyEntity m", MyEntity.class);
            myEntity = q.getSingleResult();

            MyEntityElement myEntityElement = new MyEntityElement();
            myEntityElement.setValues( // use trackable ArrayList
                Utilities.newTrackable(em, ArrayList.class));
            myEntityElement.getValues().addAll(createFloatArray(m));
            myEntityElement.ownerEntity = myEntity; // set inverse
            //myEntity.getList().add(myEntityElement);
            em.persist(myEntityElement);
        }

        em.getTransaction().commit();
        em.close();
        System.out.println("test_OneTransaction time: "
            + (System.currentTimeMillis() - startTime));
        emf.close();
    }

    public static void test_ManyTransactions() {
        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory(
                "objectdb:$objectdb/db/test.tmp;drop");

        EntityManager em = emf.createEntityManager();
        em.setFlushMode(FlushModeType.AUTO);
        em.getTransaction().begin();
        long startTime = System.currentTimeMillis();
        MyEntity myEntity = new MyEntity();
        myEntity = em.merge(myEntity);
        em.getTransaction().commit();
        em.close();

        em = emf.createEntityManager();
        em.setFlushMode(FlushModeType.AUTO);

        for (int m = 0; m < steps; m++) {
            em.getTransaction().begin();
            TypedQuery<MyEntity> q = em.createQuery(
                "SELECT m FROM MyEntity m", MyEntity.class);
            myEntity = q.getSingleResult();
            // myEntity.getList().size();
            MyEntityElement myEntityElement = new MyEntityElement();
            myEntityElement.setValues( // use trackable ArrayList
                Utilities.newTrackable(em, ArrayList.class));
            myEntityElement.getValues().addAll(createFloatArray(m));
            myEntityElement.ownerEntity = myEntity; // set inverse
            //myEntity.getList().add(myEntityElement);
            em.persist(myEntityElement);
            em.getTransaction().commit();
        }
        em.close();
        System.out.println("test_ManyTransactions time: "
            + (System.currentTimeMillis() - startTime));
        emf.close();
    }

    private static List<Float> createFloatArray(float value) {
        int values = 1;
        List<Float> floats = new ArrayList<Float>(values);
        for (float i = 0; i < values; i++) {
            floats.add(value + (i / values));
        }
        return floats;
    }

    @Entity
    public static class MyEntity {
        @OneToMany(cascade = CascadeType.ALL, mappedBy = "ownerEntity")
        private List<MyEntityElement> list = new ArrayList();
       
        public List<MyEntityElement> getList() {
            return list;
        }
    }

    @Entity
    public static class MyEntityElement {
        MyEntity ownerEntity;

        @ElementCollection
        private List<Float> values;
       
        public void setValues(List<Float> values) {
            this.values = values;
        }

        public List<Float> getValues() {
            return values;
        }
    }
}

Execution time:

test_OneTransaction time: 5373
test_ManyTransactions time: 1519
ObjectDB Support

Reply