Implementing your own persistence layer

Usually as a developer you are choosing a framework (be it a web framework or for any type of application development) because it already delivers most of the part's you need. Typically, a standard requirement is the support of a persistence layer or technology you are going to use for your project. However, there might be cases where you have to create your own persistence layer, for example, if you are using a proprietary or in-house developed solution or if you need access for a new technology like one of the many available NoSQL databases. There are several steps which need to be completed. All of these are optional; however, it makes sense to develop your own persistence layer as similar to other persistence layers in Play, so you actually make sure it fits best in the concept of Play and is easily understood by most of the users. These steps are:

  • Active record pattern for your models including bytecode enhancement for finders
  • Range queries, which are absolutely needed for paging
  • Having support for fixtures, so it is easy to write tests
  • Supporting the CRUD module when possible
  • Writing a module which keeps all this stuff together

In this recipe, simple CSV files will be used as persistence layer. Only Strings are supported along with Referencing between two entities. This is what an entity file like Cars.csv might look like:

"1"   "BMW"   "320"
"2"   "VW"   "Passat"
"3"   "VW"   "Golf"

The first column is always the unique ID, whereas the others are arbitrary fields of the class. Referencing works like the example in this User.csv file:

"1"   "Paul"   "#Car#1"

The connection to the BMW car above is done via the type and the ID by using additional hashes. As you might have already seen, the shown CSV files do not have any fieldnames. Currently, this persistence layer relies on the order read out of the class fields, which of course does not work when you change the order of fields add or remove other fields. This system is not ready for changes, but it works if you do not need such features as in this example.

The source code of the example is available at examples/chapter6/persistence.

Getting ready

In order to have a running example application, you should create a new application, include the module you are going to write in your configuration, and create two example classes along with their CRUD classes. So you should also include the CRUD module. The example entities used here are a user and Car entity. In case you are wondering about the no args constructor, it is needed in order to support fixtures:

public class Car extends CsvModel {

   public Car() {}   
   public Car(String brand, String type) {
      this.brand = brand;
      this.type = type;
   }
   
   public String brand;
   public String type;
   
   public String toString() {
      return brand + " " + type;
   }
}

Now the user:

public class User extends CsvModel {

   public String name;
   public Car currentCar;
   
   public String toString() {
      return name + "/" + getId();
   }
}

In order to show the support of fixtures, it is always good to show some tests using it:

public class CsvTest extends UnitTest {

   private Car c;
   
   @Before
   public void cleanUp() {
      Fixtures.deleteAllModels();
      CsvHelper.clean();
      Fixtures.loadModels("car-data.yml");
      c = Car.findById(1L);
   }

   // Many other tests

   @Test
   public void readSimpleEntityById() {
      Car car = Car.findById(1L);
      assertValidCar(car, "BMW", "320");
   }

   @Test
   public void readComplexEntityWithOtherEntites() {
      User u = new User();
      u.name = "alex";
      u.currentCar = c;
      u.save();
      
      u = User.findById(1L);
      assertNotNull(u);
      assertEquals("alex", u.name);
      assertValidCar(u.currentCar, "BMW", "320");
   }

   // Many other tests not put in here

   private void assertValidCar(Car car, String expectedBrand, String expectedType) {
      assertNotNull(car);
      assertEquals(expectedBrand, car.brand);
      assertEquals(expectedType, car.type);
   }
}

The YAML file referenced in the test should be put in conf/car-data.yml. It includes the data of a single car:

Car(c1):
  brand: BMW
  type: 320

As you can see in the tests, referencing is supposed to work. Now let's check how the plugin is built together.

How to do it...

You should already have created a csv module, adapted the play.plugins file to specify the CsvPlugin to load, and started to implement the play.modules.csv.CsvPlugin class by now:

public class CsvPlugin extends PlayPlugin {

   private CsvEnhancer enhancer = new CsvEnhancer();
   
  public void enhance(ApplicationClass applicationClass) throws Exception {
       enhancer.enhanceThisClass(applicationClass);
    }
   
   public void onApplicationStart() {
      CsvHelper.clean();
   }

    public Model.Factory modelFactory(Class<? extends Model> modelClass) {
      if (CsvModel.class.isAssignableFrom(modelClass)) {
         return new CsvModelFactory(modelClass);
      }
     return null;
    }
}

Also, this example heavily relies on OpenCSV, which can be added to the conf/dependencies.yml file of the module (do not forget to run play deps):

self: play -> csv 0.1

require:
    - net.sf.opencsv -> opencsv 2.0

The enhancer is pretty simple because it only enhances two methods, find() and findById(). It should be put into your module at src/play/modules/csv/CsvEnhancer.java:

public class CsvEnhancer extends Enhancer {

   public void enhanceThisClass(ApplicationClass applicationClass) throws Exception {
        CtClass ctClass = makeClass(applicationClass);

        if (!ctClass.subtypeOf(classPool.get("play.modules.csv.CsvModel"))) {
            return;
        }

        CtMethod findById = CtMethod.make("public static play.modules.csv.CsvModel findById(Long id) { return findById(" + applicationClass.name + ".class, id); }", ctClass);
        ctClass.addMethod(findById);

        CtMethod find = CtMethod.make("public static play.modules.csv.CsvQuery find(String query, Object[] fields) { return find(" + applicationClass.name + ".class, query, fields); }", ctClass);
        ctClass.addMethod(find);
        
        applicationClass.enhancedByteCode = ctClass.toBytecode();
        ctClass.defrost();
   }
}

The enhancer checks whether a CsvModel class is handed over and enhances the find() and findById() methods to execute the already defined methods, which take the class as argument. The CsvModel class should be put into the module at src/play/modules/csv/ and should look like the following:

public abstract class CsvModel implements Model {

   public Long id;
   
  // Getter and setter for id omitted
  …
   
   public Object _key() {
      return getId();
   }

   public void _save() {
      save();
   }

   public void _delete() {
      delete();
   }
   
   public void delete() {
      CsvHelper helper = CsvHelper.getCsvHelper(this.getClass());
      helper.delete(this);
   }
   
   public <T extends CsvModel> T save() {
      CsvHelper helper = CsvHelper.getCsvHelper(this.getClass());
      return (T) helper.save(this);
   }

   public static <T extends CsvModel> T findById(Long id) {
      throw new UnsupportedOperationException("No bytecode enhancement?");
   }

   public static <T extends CsvModel> CsvQuery find(String query, Object ... fields) {
      throw new UnsupportedOperationException("No bytecode enhancement?");
   }

   protected static <T extends CsvModel> CsvQuery find(Class<T> clazz, String query, Object ... fields) {
  // Implementation omitted
   }
   
   protected static <T extends CsvModel> T findById(Class<T> clazz, Long id) {
      CsvHelper helper = CsvHelper.getCsvHelper(clazz);
      return (T) helper.findById(id);
   }
}

The most important part of the CsvModel is to implement the Model interface and its methods save(), delete(), and _key()—this is needed for CRUD and fixtures. One of the preceding find methods returns a query class, which allows restricting of the query even further, for example with a limit and an offset. This query class should be put into the module at src/play/modules/csv/CsvQuery.java and looks like this:

public class CsvQuery {

   private int limit = 0;
   private int offset = 0;
   private CsvHelper helper;
   private Map<String, String> fieldMap;

   public CsvQuery(Class clazz, Map<String, String> fieldMap) {
      this.helper = CsvHelper.getCsvHelper(clazz);
      this.fieldMap = fieldMap;
   }

   public CsvQuery limit (int limit) {
      this.limit = limit;
      return this;
   }

   public CsvQuery offset (int offset) {
      this.offset = offset;
      return this;
   }

   public <T extends CsvModel> T first() {
      List<T> results = fetch(1,0);
      if (results.size() > 0) {
         return (T) results.get(0);
      }
      return null;
   }

   public <T> List<T> fetch() {
      return fetch(limit, offset);
   }

   public <T> List<T> fetch(int limit, int offset) {
      return helper.findByExample(fieldMap, limit, offset);
   }
}

If you have already used the JPA classes from Play, most of you will be familiar from a user point of view. As in the CsvModel class, most of the functionality boils down to the CsvHelper class, which is the core of this module. It should be put into the module at src/play/modules/csv/CsvHelper.java:

public class CsvHelper {

   private static ConcurrentHashMap<Class, AtomicLong> ids = new ConcurrentHashMap<Class, AtomicLong>();
   private static ConcurrentHashMap<Class, ReentrantLock> locks = new ConcurrentHashMap<Class, ReentrantLock>();
   private static ConcurrentHashMap<Class, CsvHelper> helpers = new ConcurrentHashMap<Class, CsvHelper>();
   private static final char separator = '	';
   private Class clazz;
   private File dataFile;
   
   private CsvHelper(Class clazz) {
      this.clazz = clazz;
      File dir = new File(Play.configuration.getProperty("csv.path", "/tmp"));
      this.dataFile = new File(dir, clazz.getSimpleName() + ".csv");
      

      locks.put(clazz, new ReentrantLock());
      ids.put(clazz, getMaxId());
   }

   public static CsvHelper getCsvHelper(Class clazz) {
      if (!helpers.containsKey(clazz)) {
         helpers.put(clazz, new CsvHelper(clazz));
      }
      return helpers.get(clazz);
   }

   public static void clean() {
      helpers.clear();
      locks.clear();
      ids.clear();
   }

The next method definitions include the data specific functions to find, delete, and save arbitrary model entities:

   public <T> List<T> findByExample(Map<String, String> fieldMap, int limit, int offset) {
      List<T> results = new ArrayList<T>();

     // Implementation removed to save space
      
      return results;
   }

   public <T extends CsvModel> void delete(T model) {
     // Iterates through csv, writes every line except
     // the one matching the id of the entity
   }

   public void deleteAll() {
     // Delete the CSV file
   }

  
 public <T extends CsvModel> T findById(Long id) {
      Map<String, String> fieldMap = new HashMap<String, String>();
      fieldMap.put("id", id.toString());      List<T> results = findByExample(fieldMap, 1, 0);
      if (results.size() > 0) {
         return results.get(0);
      }
      return null;
   }

   public synchronized <T extends CsvModel> T save(T model) {

    // Writes the entity into the file
    // Handles case one: Creation of new entity
    // Handles case two: Update of existing entity

      return model;
   }

The next methods are private and needed as helper methods. Methods for reading entity files, for creating an object from a line of the CSV file, as well as the reversed operation, which creates a data array from an object, are defined here. Furthermore, file locking functions and methods to find out a the next free id on entity creation are defined here:

   private List<String[]> getEntriesFromFile() throws IOException {
      // Reads csv file to string array
   }

   private <T extends CsvModel> String[] createArrayFromObject(T model, String id) throws IllegalArgumentException, IllegalAccessException {
    // Takes and object and creates and array from it
    // Dynamically converts other CsvModels to something like
    // #Car#19
   }

   private <T extends CsvModel> T createObjectFromArray(String[] obj) throws InstantiationException, IllegalAccessException {
    // Takes an array and creates an object from it
    // Dynamically loads #Car#19 to the real object

      return model;
   }

   private void getLock() {
    // helper method to create a lock for the csv file
   }
  
 
   private void releaseLock() {
    // helper method to remove a lock for the csv file
   }
   
   private synchronized void moveDataFile(File file) {
    // Replaces the current csv file with the new one
   }

   private Long getNewId() {
      return ids.get(clazz).addAndGet(1);
   }

   private AtomicLong getMaxId() {
     // Finds the maximum id used in the csv file
      return result;
   }

}

The last class file needed is the CsvModelFactory, which has been referenced in the CsvPlugin already. This is needed to support the CRUD plugin and have correct lookups of entities when referencing them in the GUI, for example in select boxes. As this class is almost similar to JpaModelLoader used by JPA, you can directly look into its source as well. The interface will be explained in some time.

How it works...

In order to understand what is happening in the source, you should build the module, include it together with the crud module in an example application, create some test classes (such as the User and Car entities as we did some time back), and click around in the CRUD module.

The CsvHelper is the core of the module. It uses HashMaps internally to hold some state per class, like the current ID to use when creating a new entity. Furthermore, it uses a lock in order to write any CSV file and holds CsvHelper instances per model entity. The main method in order to find entities is the findByExample() method, which allows the developer to hand over a map with fields containing specific values. This method loops through all entries and checks whether there are any entries matching the fields and their corresponding value. The findById() method for example is just a findByExample() invocation with a search for a specific ID value. Saving objects is either appending them to the file or alternatively replacing them in the CSV file.

Some persistence layer APIs feature functions to map Java objects to the persistence layer. Naturally these functions heavily differ from the library you are using. The Morphia/MongoDB module for example uses Morphia, which already includes much of this mapping functionality and helps to keep the glue code between Play and the external library as small as possible. Whenever you implement a persistence layer make sure you are choosing a library, where in the best case you only need to extend it with Play functionality instead of writing it from scratch like this one. It gets very complex to support, as the following things can pose serious problems, and do in this concrete example:

  • Multiple accesses from different threads: When you take a closer look at the implementation, it is not thread safe at all. If two threads add a new entity each at the same time, then the added entity of the first thread will be lost after the second thread has stored its entity. This is because both threads take a backup from the data when both entities were not yet added.
  • No referential integrity: This example has no referential integrity. If you remove a car, which is referenced in another CSV file, no one will notice.
  • Circular dependencies: It is possible to bring down this application in an endless loop by referencing entity a in entity b and vice versa.
  • Single node: All the data is stored on a single node. This is not scalable.
  • Complex object support: Currently everything is stored as a string. This is not really a solution for number focused applications. However, many web applications are number specific, for example e-commerce or statistical ones.
  • Collection support: This is a major problem. Of course you want to be able to store collections, such as @OneToMany and @ManyToOne annotations in JPA, with your persistence layer. A basic implementation in this example could be a string such as #Car#5,#Car#6 in the string representation of another entity. However, there is no implementation done for this here.

In most cases most of the preceding problems should be solved by your database or the driver you are using. Otherwise, you should split this code away from your Play framework support and provide it as an own library.

As a last step it makes a lot of sense to take a more exact look at the interfaces provided by the Play framework in order to have support for CRUD and fixtures. Take a look at the play.db.Model interface, which incorporates all the other interfaces needed.

Every model class, like the CsvModel, should implement the Model interface itself. The interface makes sure there are methods provided for saving, deleting entities, and as getting a unique key.

The Factory interface referenced at the CsvModelFactory provides methods needed for finding, counting, and searching entities. The keyName(), keyType(), and keyValue() methods return different information about the unique key of a model class. In this case the name of the unique key field is id, the type is Long, and the value can be an arbitrary long out of the CSV file. The findById() method provides a generic way to find an entity with any unique key. The fetch() method is the most complex to be implemented. Fetching allows you to page through results by setting a limit and offset parameter. You can optionally define only to return certain fields, add ordering (direction and fields to order by), and add additional filtering by a where clause. This looks a little bit SQL centric, but can be implemented for any persistence layer. In order to support search you can additionally provide a list of fields to search in along with a keyword to search for. This allows you to do a search over on more than one field. Make sure that your database system does not suffer severe performance problems because of this. The count() method basically does the same as fetch(), but it does not need limits, offsets, or order information in order to run. The deleteAll() method deletes all data of all entities. This is especially important for testing in order to make sure to start with zero entries in your persistence layer. The last method to be implemented is listProperties(), which returns a representation of all properties of an entity. The representation includes a lot of metadata, which helps the CRUD controllers to display data.

This Property class also consists of several important fields. The name, type, and field properties resemble data of the field in the entity and are mostly read out of it. You could possibly read another name out of an annotation if you wanted. The isSearchable Boolean marks the field as being searchable. The JPA plugin uses this for only making fields searchable, which have this attribute set. The isMultiple Boolean marks a field as a collection. The isRelation Boolean should be used when the field is also a model entity. The last Boolean is the isGenerated Boolean, which marks a field you cannot set manually. This is often the unique key of an entity because it is being generated by the persistence layer, but could possibly also have fields like the creation date or the last modified date. The relationType class holds the class of related fields. If you have a one-to-one connection, it would be the entity type of the other class. If you have a one-to-many collection, it would be the entity type of the lists contents and not the list itself. This is important for select boxes filled with options of other entities in CRUD controllers. The last field to be explained is the choices field. It is responsible for the content in select boxes referencing entities. You can put an appropriate query in the list() method of the Choices interface and the returned results will then occur in the select boxes. For the sake of simplicity you can return all entities here. This is also done in the example code.

There is a last interface defined in the Model. The BinaryField interface allows you to treat binaries special. The play.db.jpa.Blob class is actually doing this. It stores files on the hard disk as they cannot be stored inside any database supporting JPA.

The factory itself is always returned in the plugin, where the developer may decide whether the plugin is responsible for the class asked for.

There's more...

As documentation about support for the CRUD module is sparse currently, the quickest thing to get up and running is to look at existing plugins.

Check the JPA and Morphia plugins

If you want to know more, these two plugins are the place to start in order to get some more information about implementing own persistence layers. Also, the crudsiena plugin is worth taking a look at.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.129.90