Generic repository for Java (Part 1)
javaPersisting data is part of most apps and there are several ways of doing persistence. In this blog post I'm sharing some of my techniques and personal oppinions. Maybe it doesn't fit for everybody but it works for me and maybe this is helpful for some people.
When it comes to persistence my favorite is EventSourcing: Instead of persisting the current state of your data, only events of changes to your data are persisted. The current state can then be (re-)calculated from the events. In my oppinion this is the most natural and elegant way of handling persistence but, to be honest, it's not the easiest to implement, especially in Java.
But this is another story. Today I want to further investigate persistence for use cases where EventSourcing is not an option and/or classical state-based persistence is needed. In such cases I'm typically using the repository pattern. The key of this design pattern is to define a common interface for persistence functionality that is independent from the underlaying persistence technique. For each specific persistence technique (for example a database, the filesystem or a cloud storage) an implementation of the interface can be provided. This way you can exchange the persistence technique without touching the rest of your application. Additionally this improves the testability because you can provide a mock implementation of the repository interface for unit tests.
Entities
The first part is to define "Entities". An "Entity" is an object-oriented abstraction of a part of the reality. For example when we create a contact management app a possible Entity would be "Person".
An entity has an identity: The state of an entity can change over time but it is still the "same" entity. For example the name of a person may change but it is still the same person. Another important point: Two persons may have the same name (and other properties) but they are still different persons. This means the distinction of instances of entities isn't done based on their properties. Instead we need an identifier. I'm typically using a UUID.
A base class for entities can look like this:
import java.util.Objects;
import java.util.UUID;
public abstract class Identity {
private final String id;
public Identity() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Identity identity = (Identity) o;
return Objects.equals(id, identity.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "[id=" + id.substring(0,8) + "...]";
}
}
As you can see the equals
and hashcode
methods are only working with the id
property. The id
is marked as
final
and therefore can't be changed after it is assigned in the constructor.
A concrete entity could look like this:
public class Person extends Identity {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Interface for a Repository
Now we can define an interface for our Repository. For the person it could look like this:
public interface PersonRepository {
/**
* @return the person with the given id.
*/
Optional<Person> get(String id);
/**
* @return a collection of all persons.
*/
Set<Person> get();
/**
* Persist the given person instance.
*/
void persist(Person person);
/**
* Remove the given person instance.
*/
void remove(Person person);
}
The cool thing is: As we have the Identity
base class we can define a generic interface for the repository
functionality:
public interface Repository<T extends Identity> {
Optional<T> get(String id);
Set<T> get();
void persist(T entity);
void remove(T entity);
}
This way we can have repositories for many entity types that share a common interface for persistence. The given example only defines some base functionality but we could extend the interface a little bit. For example we may want to persist/remove many entities at once. Or we like to make more specific queries to get entity instances that match a given condition.
public interface Repository<T extends Identity> {
Optional<T> get(String id);
/**
* return all entities that match the given predicate.
*/
Set<T> get(Predicate<T> predicate);
Set<T> get();
void persist(T entity);
void persist(T... entities);
void persist(Collection<T> entities);
void remove(T entity);
void remove(T... entities);
void remove(Collection<T> entities);
/**
* remove the entity with the given id.
*/
void remove(String id);
/**
* remove all entities that match the given predicate.
*/
void remove(Predicate<T> predicate);
}
These additional methods can be intersting for many use cases. The downside is that the implementor of a specific Repository has a lot of methods to implement. But this can be fixed with a default methods feature of Java 8 like this:
public interface Repository<T extends Identity> {
default Optional<T> get(String id) {
return get()
.stream()
.filter(entity -> entity.getId().equals(id))
.findAny();
}
default Set<T> get(Predicate<T> predicate) {
return get()
.stream()
.filter(predicate)
.collect(Collectors.toSet());
}
Set<T> get();
void persist(T entity);
default void persist(T... entities) {
persist(Arrays.asList(entities));
}
default void persist(Collection<T> entities) {
entities.forEach(this::persist);
}
void remove(T entity);
default void remove(T... entities) {
remove(Arrays.asList(entities));
}
default void remove(Collection<T> entities) {
entities.forEach(this::remove);
}
default void remove(String id) {
remove(entity -> entity.getId().equals(id)); // predicate to match the id
}
default void remove(Predicate<T> predicate) {
get(predicate).forEach(this::remove);
}
}
Now there are only three methods that needs to be implemented: Set<T> get()
, void persist(T entity)
and
void remove(T entity)
. For all other methods there are default implementations that are using these three methods
directly or indirectly. An interesting detail is that we can use the String getId()
method of the Identity
class
because we have the type constraint for T
to be a subclass of Identity
.
Of cause the default implementations may not be optimal when it comes to performance. But every implementor can choose to overwrite the default implementations with a more advanced implementation that makes better use of the underlaying persistence technique.
In-Memory Repository
Most of the time when I'm developing apps I'm starting with the frontend. In the frontend I only have a static dependency (i.e. an import) to the repository interfaces. The specific implementation for the interfaces are provided by a dependency-injection library. During the development I'm typically using an in-memory implementation for the repositories because it's easy and fast (but loses the persisted data on every application start of cause).
Given the interface from above we can even provide a generic in-memory implementation for the repository:
public abstract class InMemoryRepository<T extends Identity> implements Repository<T> {
private Set<T> entities = new HashSet<>();
@Override
public Set<T> get() {
return Collections.unmodifiableSet(entities);
}
@Override
public void persist(T entity) {
entities.add(entity);
}
@Override
public void remove(T entity) {
entities.remove(entity);
}
}
A concrete implementation for the Person
entity looks like this:
public class PersonInMemoryRepository extends InMemoryRepository<Person> {
}
Thats all. No code is needed at all. Of cause an in-memory repository isn't sufficient for a real application. In the second part of this series I will show an implementation based on the Java Persistence API that can be used to store the data in a relational database.