SQL is soooo…

Working with pure SQL and JDBC drivers is not so funny for a long time. Play framework provides two plugins for Java language enabling working with data in a bit more convenient way. These are play-java-jpa and play-ebean plugins. In this post, I would like to check what we can get with the Ebean plugin.
 

What Ebean is

ebean-logoEbean is an object-relation mapping framework for Java and Kotlin. It was created by Rob Bygrave.

The author of the framework emphasises that working with Ebean should be simpler than with other JPA implementations. Here you can find a good summary of features of the framework and its differences comparing to JPA and Hibernate. The two key points are session less architecture and a querying designed in a way it supports partial objects and avoids N+1 issues. What I found interesting as well are so-called stateless updates – entities do not need to be fetched before updating them.
 

Play Ebean

What’s inside

I am using the plugin in version 3.0.2, supporting Ebean v7.6.x.

It uses JDBC and Evolution libraries, I have described before.

Ebean support consists of two components – runtime library talking to a database and SBT plugin enhancing entities during compilation phase of a project build.

You can find a documentation of the plugin usage on Play Framework pages.

Setup

A key part of Ebean runtime is com.avaje.ebean.EbeanServer interface. Its implementation allows managing entities of an application within a given data source.

We can set up both, data source name and entities managed by it, in application.conf file. The values are configured in the following way: ebean.datasourceName = [classes and/or packages to be managed]. The data source has to be defined in JDBC Datasource section of the application configuration.

You can provide a more detailed configuration of an EbeanServer in two ways:

  • with a properties file
    • by default ebean.properties name is considered,
    • you need to put the file to project’s classpath,
  • or by implementing a com.avaje.ebean.event.ServerConfigStartup interface
    • you have to place the implementation class to the package managed by Ebean.

The following content of a properties file:

ebean.autoCommitMode=false
ebean.ddl.generate=true

can be provided in a configuration startup implementation like this one:

public class ConfigStartup implements ServerConfigStartup {
    public void onStart(ServerConfig serverConfig) {
        serverConfig.setAutoCommitMode(false);
        serverConfig.setDdlGenerate(true);
    }    
}

You can find a full list of configuration properties in the Ebean’s code. Simply check ServerConfig.loadSettings method for details.

Entities and Actions

When you define an entity in Ebean you can map it with JPA annotations. There is no need to learn a set of new ones and it definitely eases switching to Ebean from Spring Data JPA for example. Here you can find detailed documentation how to map entities with Ebean.

You can operate on entities in two ways:
– with com.avaje.ebean.Model class as a super class of your entity or
– by using com.avaje.ebean.Ebean singleton.

The Model class utilises Active Record pattern. It provides a bunch of handful operations required to play with data.

Insert

Storing of a new entity will look like the following:

Todo t = new Todo(someName, dueDate);
t.insert();

This will insert a non-existing entity into a database. There is Model.save() method yet inserting a new data or updating an existing one – Ebean will automatically detect what kind of situation it is applied in.

Find

When it comes to finding an entity, Model provides Find class that gives us everything we would need to look for data in a database. The simplest way of using it is to create a static field in an entity class:

public static final Find find = new Finder<>(Todo.class);

Next, querying for an entity can look like the following:

Todo t = Todo.find
    .where()
    .eq("name", someName)
    .findUnique();

There is even an option to fetch data partially. This is really useful when we do not need a complete entity data but some fields only:

Todo t = Todo.find
    .select("status")
    .where()
    .eq("name", someName)
    .findUnique();

Delete

Removal of an entity is really simple as well:

Todo.find.byId(id).delete();

Update

I mentioned updating of data above. Model class has dedicated method for this purpose:

Todo t = new Todo(id, newName);
t.update();

It updates an existing entity. If there is no any then the method does nothing, so it seems pretty valid to check for an existence of an entity before calling the action.

The Model.update() method requests database to update a row fulfilling provided criteria. If there is no any then it does nothing. So it looks quite valid to check at first whether an entity with a given identifier exists:

Todo t = Todo.find.byId(id);
if (t != null) {
  t.setName("whatever");
  t.update();
}

// or even better with Option from vavr.io

Option.of(Todo.find.byId(123L))
  .peek(t -> t.setName("whatever"))
  .forEach(t -> t.update());

Non-default Ebean server

Calling non-parameterised methods for insertion, updating and removal on an entity triggers an action against a default Ebean server. However, these methods have an extra version with a name of a database server the change should be applied to.
Looking for an entity can be done on various Ebean servers thanks to the Find.on(serverName) method which provides a finder for a server with given name. Usage of the method will look like the following:

Todo t = Todo.find.on("nonDefaultServer").byId(123L);

I see this quite useful when one has to support separate databases for various clients.

Calling actions with Ebean class

You can still operate on your entities with no Model at all. If so, all actions manipulating data will be called from com.avaje.ebean.Ebean class. Operations will look like the following pattern:

// finding by id
Todo t = Ebean.find(Todo.class, id);
// or by name
// Ebean.find(Todo.class).where().eq("name", someName);

// update
t.setName("whatever");
Ebean.update(t);

// remove
boolean isRemoved = Ebean.delete(t);

Transactional actions

Scopes

Ebean provides com.avaje.ebean.TxType enum that defines types of supported transactions. A default one is TxType.REQUIRED.

The enum is used to define a transaction with com.avaje.ebean.TxScope class. We can specify on what server the transaction should run and what level of isolation it should use. What is interesting, we can specify for what exceptions a transaction should be rolled back and for what it should not.

Actions

If you would like to use transactional actions in Ebean, you can combine two or more of them within a single transaction in the following ways:

  1. Ebean.execute methods and com.avaje.ebean.TxRunnable/TxCallable classes with an option to specify a scope by using TxScope:
TxScope scope = ... // some scope definition just right here;
Ebean.execute(scope, () -> {
  Author a = new Author("James Joyce");
  a.insert();
  Task t = new Task("Write a book", a);
  t.insert();
});
  1. Annotating a method with play.db.ebean.Transactional so in a case of any error thrown everything done inside the method will be rolled back:
@Transactional
public void createAuthorWithTask(String authorName, String taskName) {
  Author a = new Author("James Joyce");
  a.insert();
  Task t = new Task("Write a book", a);
  t.insert();  
}
  1. Operating transactions manually (with an option of providing TxIsolation or TxScope):
Ebean.beginTransaction();
try {
  Author a = new Author("James Joyce");
  a.insert();
  Task t = new Task("Write a book", a);
  t.insert();
  Ebean.commitTransaction();
} finally {
  Ebean.endTransaction();
}

Testing

To test classes using Ebean we need to setup a whole test application. Well, I have not found any other way to do so at least.

Of course, it does make sense to run our tests against some in-memory database. We can do this by extending play.test.WithApplication class and overriding provideApplication() method. In my case, I provided an in-memory database to a fake application:

  @Override
  protected Application provideApplication() {
      return fakeApplication(inMemoryDatabase());
  }

After every test case, I remove all data stored from a database so it is in a clean state:

  @After
  public void cleanup() {
      Ebean.createSqlUpdate("DELETE FROM todos WHERE 1=1").execute();
  }

Here is a test class I have created for the Ebean repository.

You can find a bunch of tips about testing Ebean in the framework documentation.

Summary

Ebean looks like a better way of handling database connection than using a raw SQL. The framework is really interesting and has quite a lot to offer. The thing I do not like is leaning on com.avaje.ebean.Ebean singleton class. And this can truly complicate writing of tests.

Here is my sample project using Ebean framework.

Ebean is based on a philosophy different from you can find in Spring Data projects or Hibernate. Such experience can be really refreshing. I urge you to give the framework a try!


0 Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.