Generic repository for Java (Part 2)

| tagged as

In the first part of this series I was showing an interface for a generic repository and an in-memory implementation. In this part I will show another implementation with the Java Persistence API (JPA).

An in-memory repository is a good starting point and is handy during the development but at some point you need "real" persistence. One option in the Java world is the "Java Persistence API" (JPA). This blog post is not a JPA tutorial so I expect you to have some knowledge about JPA already. Furthermore I have to admit that I'm not a pro when it comes to JPA. Again: The things described here are working for me but I'm sure there are many improvements possible.

The first step is to include the needed libraries for JPA and setup a persistence.xml (which I won't describe here). After that we can start with the interesting stuff. Let's start with the Identity class. We have a field id that we have to tell JPA about. This is done with the @Id annotation. As we are generating the value of the ID on our own in the constructor we don't need the @GeneratedValue annotation. Additionally we need the @MappedSuperclass annotation on the Identity class. Otherwise JPA wouldn't find the id field in the derived entity classes.

import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.util.Objects;
import java.util.UUID;

@MappedSuperclass
public abstract class Identity {

    @Id
    private final String id;

    public Identity() {
        this.id = UUID.randomUUID().toString();
    }

    ...
}

Our Person class looks like this:

import javax.persistence.Entity;

@Entity
public class Person extends Identity {

    private String name;

    Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    ...
}

There are two changes in the Person class: We have added the @Entity annotation to tell JPA that this class can be persisted. The second change is that we need a no-arg constructor. Otherwise JPA would throw an exception. However, it is possible to make the constructor package scoped.

Now lets look at the generic repository. Again we like to have a generic implementation that works for all entity classes. Similar to the in-memory implementation we create a generic class JpaRepository that overwrites the three methods Set<T> get(), void persist(T entity) and void remove(T entity). Here is a possible implementation:

public class JpaRepository<T extends Identity> implements Repository<T> {

    private EntityManagerFactory emf;
    private Class<T> type;

    public JpaRepository(Class<T> type, String persistenceUnitName) {
        this.type = type;
        emf = Persistence.createEntityManagerFactory(persistenceUnitName);
    }


    @Override
    public Set<T> get() {
        final EntityManager entityManager = emf.createEntityManager();

        final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        final CriteriaQuery<T> criteria = criteriaBuilder.createQuery(type);

        final Root<T> root = criteria.from(type);
        criteria.select(root);

        final TypedQuery<T> query = entityManager.createQuery(criteria);
        final List<T> resultList = query.getResultList();

        entityManager.close();
        return new HashSet<>(resultList);
    }

    @Override
    public void persist(T entity) {
        final EntityManager entityManager = emf.createEntityManager();
        entityManager.getTransaction().begin();

        entityManager.merge(entity);

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

    @Override
    public void remove(T entity) {
        final EntityManager entityManager = emf.createEntityManager();

        entityManager.getTransaction().begin();

        final T managedEntity = entityManager.find(type, entity.getId());
        if(managedEntity != null) {
            entityManager.remove(managedEntity);
        }

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

A you can see it is no problem to provide generic implementations for the three methods. Other than the in-memory version this repository needs some constructor arguments. The first one is the generic class type of the entity. This is needed for queries. The second argument is the persistence unit name that is defined in the persistence.xml.

Please note: I'm using explicit transaction handling here (entityManager.getTransaction().begin()). The reason is that I'm not using an EJB container anymore. If you are using EJB you can remove the transaction handling statements.

The concrete implementation for the Person entity looks like this:

public class PersonJpaRepository extends JpaRepository<Person> {

    public PersonJpaRepository(String persistenceUnitName) {
        super(Person.class, persistenceUnitName);
    }
}

This time we need to provide a constructor.

Improving the JPA version

At this point you might already noticed that the JPA implementation isn't realy smart. The reason is how the default methods of the Repository interface are implemented.

For example the void persist(Collection<T> entities) method. Instead of writing all entities to the database in one transaction it will create and commit a new transaction for each entity. Another example is the Set<T> get(Predicate<T> predicate) method. It will load all entities from the database and then filter them in the memory. This kind of works for small databases but will blow up with slightly bigger ones.

Refactoring

But before we implement specific versions for some of the interface methods we will do some refactoring. If you look at the code you can see some code duplication that can be removed.

Lets start with this method:

private <R> R run(Function<EntityManager, R> function) {
   final EntityManager entityManager = emf.createEntityManager();
   try {
       return function.apply(entityManager);
   } finally {
       entityManager.close();
   }
}

It takes a function as argument that takes the entityManager as argument and returns some generic result. A caller now doesn't need to bother the creation and closing of the entityManager. With this method we can implement the get method like this:

@Override
public Set<T> get() {
    List<T> resultList = run(entityManager -> {
        final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        final CriteriaQuery<T> criteria = criteriaBuilder.createQuery(type);

        final Root<T> root = criteria.from(type);
        criteria.select(root);

        final TypedQuery<T> query = entityManager.createQuery(criteria);
        return query.getResultList();
    });

    return new HashSet<>(resultList);
}

For the persist and remove methods this isn't sufficient because we need the transaction handling. Therefore we create this method:

private <R> R runInTransaction(Function<EntityManager, R> function) {
    return run(entityManager -> {
        entityManager.getTransaction().begin();

        final R result = function.apply(entityManager);

        entityManager.getTransaction().commit();

        return result;
    });
}

It uses the existing run method and adds the transaction handling. For use cases where no result is needed we can provide two variants of the run/runInTransaction methods that take Consumer<EntityManager> as argument. Now the whole repository looks like this:

public class JpaRepository<T extends Identity> implements Repository<T> {

    private EntityManagerFactory emf;

    private Class<T> type;

    public JpaRepository(Class<T> type, String persistenceUnitName) {
        this.type = type;
        emf = Persistence.createEntityManagerFactory(persistenceUnitName);
    }


    @Override
    public Set<T> get() {
        List<T> resultList = run(entityManager -> {
            final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            final CriteriaQuery<T> criteria = criteriaBuilder.createQuery(type);

            final Root<T> root = criteria.from(type);
            criteria.select(root);

            final TypedQuery<T> query = entityManager.createQuery(criteria);
            return query.getResultList();
        });

        return new HashSet<>(resultList);
    }

    @Override
    public void persist(T entity) {
        runInTransaction(entityManager -> {
            entityManager.merge(entity);
        });
    }

    @Override
    public void remove(T entity) {
        runInTransaction(entityManager -> {
            final T managedEntity = entityManager.find(type, entity.getId());
            if (managedEntity != null) {
                entityManager.remove(managedEntity);
            }
        });
    }

    private <R> R run(Function<EntityManager, R> function) {
        final EntityManager entityManager = emf.createEntityManager();
        try {
            return function.apply(entityManager);
        } finally {
            entityManager.close();
        }
    }

    private void run(Consumer<EntityManager> function) {
        run(entityManager -> {
            function.accept(entityManager);
            return null;
        });
    }

    private <R> R runInTransaction(Function<EntityManager, R> function) {
        return run(entityManager -> {
            entityManager.getTransaction().begin();

            final R result = function.apply(entityManager);

            entityManager.getTransaction().commit();

            return result;
        });
    }

    private void runInTransaction(Consumer<EntityManager> function) {
        runInTransaction(entityManager -> {
            function.accept(entityManager);
            return null;
        });
    }
}

Now lets start to implement the other methods of the Repository interface. I won't describe all methods in detail because everyone who can read Java-8-Stream-API based code will have no problem understanding the code (I hope so).

For example this method:

@Override
public void persist(Collection<T> entities) {
    runInTransaction(entityManager -> {
        entities.forEach(entityManager::merge);
    });
}

The difference to the default version is that now all merge operations are done in one single transaction instead of using a new transaction for each entity.

Another interesting one is the void remove(Collection<T> entities) method:

@Override
public void remove(Collection<T> entities) {
    runInTransaction(entityManager -> {
        entities
                .stream()
                .map(Identity::getId)
                .map(id -> entityManager.find(type, id))
                .filter(Objects::nonNull)
                .forEach(entityManager::remove);
    });
}

The remove method of the EntityManager can only handle so called managed entities. Simply speaking, an entity instance is managed when it is freshly loaded from the EntityManager. Therefore we have to reload all entities from the given collection before we can remove them. This is done in a stream. At first we map the Stream of entities to a stream of IDs of each entity. Then we map the stream of IDs to a stream of managed entities by using the find method. If, for some reason, an entity of the argument collection was already removed, the find method would return null. This means the Stream of managed entities may contain null entries that should be filtered out. The last step is to call EntityManager.remove for all managed entities in the stream.

The whole code for the JpaRepository now looks like this:

public class JpaRepository<T extends Identity> implements Repository<T> {

    private EntityManagerFactory emf;

    private Class<T> type;

    public JpaRepository(Class<T> type, String persistenceUnitName) {
        this.type = type;
        emf = Persistence.createEntityManagerFactory(persistenceUnitName);
    }


    @Override
    public Set<T> get() {
        List<T> resultList = run(entityManager -> {
            final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            final CriteriaQuery<T> criteria = criteriaBuilder.createQuery(type);

            final Root<T> root = criteria.from(type);
            criteria.select(root);

            final TypedQuery<T> query = entityManager.createQuery(criteria);
            return query.getResultList();
        });

        return new HashSet<>(resultList);
    }


    @Override
    public Optional<T> get(String id) {
        return Optional.ofNullable(run(entityManager -> {
            return entityManager.find(type, id);
        }));
    }


    @Override
    public void persist(T entity) {
        runInTransaction(entityManager -> {
            entityManager.merge(entity);
        });
    }

    @Override
    public void persist(Collection<T> entities) {
        runInTransaction(entityManager -> {
            entities.forEach(entityManager::merge);
        });
    }

    @Override
    public void remove(T entity) {
        remove(entity.getId());
    }

    @Override
    public void remove(String id) {
        runInTransaction(entityManager -> {
            final T managedEntity = entityManager.find(type, id);
            if (managedEntity != null) {
                entityManager.remove(managedEntity);
            }
        });
    }

    @Override
    public void remove(Collection<T> entities) {
        runInTransaction(entityManager -> {
            entities
                    .stream()
                    .map(Identity::getId)
                    .map(id -> entityManager.find(type, id))
                    .filter(Objects::nonNull)
                    .forEach(entityManager::remove);
        });
    }

    @Override
    public void remove(Predicate<T> predicate) {
        remove(get(predicate));
    }

    private <R> R run(Function<EntityManager, R> function) {
        final EntityManager entityManager = emf.createEntityManager();
        try {
            return function.apply(entityManager);
        } finally {
            entityManager.close();
        }
    }

    private void run(Consumer<EntityManager> function) {
        run(entityManager -> {
            function.accept(entityManager);
            return null;
        });
    }

    private <R> R runInTransaction(Function<EntityManager, R> function) {
        return run(entityManager -> {
            entityManager.getTransaction().begin();

            final R result = function.apply(entityManager);

            entityManager.getTransaction().commit();

            return result;
        });
    }

    private void runInTransaction(Consumer<EntityManager> function) {
        runInTransaction(entityManager -> {
            function.accept(entityManager);
            return null;
        });
    }
}

Maybe you have already noticed that I have no implementation for the method Set<T> get(Predicate<T> predicate). I haven't found a clean way of doing this with JPA in a generic way. As far as I know there is no functional-style of filtering and processing of data with JPA. Instead JPA is heavily based on the relational database model and SQL-like queries. Maybe someone has a good idea for an implementation of the predicate based method. In general this JPA implementation may still be not optimal for some situations where big data bases are used. In such cases you will typically define special queries for each entity type. But the given generic repository may still be a good starting point for some basic CRUD functionality.

All code can be found (and freely used) at github: https://github.com/lestard/blogpost_generic_repository