Data Persistence with the NRG Entity Service Framework
XNAT data types provide a lot of power for handling data within the application's primary domain of research data. Along with this power, however, comes a fair amount of overhead to create and maintain the data types. For data outside XNAT's core functionality, you can create a Java-based service that provides its own persistent data types and even its own discoverable and documented REST API.
XNAT's data services and REST API use the Spring Framework and Hibernate data persistence framework. The plugin framework provides a number of ways you can integrate your own code into XNAT's own Spring and Hibernate implementations. This documentation focuses primarily on how to accomplish this integration in the XNAT context and not on details of how to write Spring and Hibernate code. Where appropriate, we provide links to sources for more information on particular aspects or features of these frameworks.
This guide describes how to implement your own data types, using the NRG entity service framework to provide basic functionality as well as support the relationship between the actual data type (also referred to as the entity class) and the supporting data access (DAO) and service implementations.
Understanding the Entity-DAO-Service Relationship
The standard data entity structure in XNAT has three parts:
The entity is the actual package of data you want to save. For example, if you want to be able to store information about different medical imaging scanners, you might have a class named Scanner with properties like name, manufacturer, modality, etc.:
Sample bean class
JAVApublic class Scanner { private String name; private String manufacturer; private String modality; }
You would also have getter and setter methods for these properties or use Lombok annotations to generate them, but these are omitted here for clarity.
- The DAO (for data access object) or repository provides methods to create, retrieve, update, and delete instances of the entity class (known as CRUD operations), including ways to locate one or multiple instances of the entity class based on property values. For example, with the Scanner entity class, you might want to find all scanners from a particular manufacturer or with a particular imaging modality.
- The service class provides methods for higher-level operations, as well as transaction management for aggregate data operations.
This model provides a lot of power. The idea is that the DAO implements single atomic operations (create an object, get an object), while the service implements larger operations that include one or more of the DAO's lower-level operations. Because the service provides a transactional wrapper for its operations, if something goes wrong with one of the operations in a service transaction, all of the operations can be rolled back. This helps prevent your database ending up in a corrupt or inconsistent state.
The most important aspect of the entity-DAO-service architecture is the division of individual operations at the DAO level and aggregate operations at the service transaction level. By making these aggregate operations transactional, an error in any atomic operation at any point in the aggregate operation can result in the entire operation being rolled back, so that your database is protected from partial operations that can cause issues with data integrity.
Implementing Entity-DAO-Service Classes
The NRG entity service framework can be found in the NRG Framework library. You can look at the source code for this library to better understand how the various parts work together:
$ git clone https://bitbucket.org/xnatdev/framework.git
You can reference this library in your own code by including a reference in build.gradle:
implementation "org.nrg:framework:1.7.6"
Note that the NRG Framework library is referenced by the XDAT core library as well as XNAT Web, so you should automatically have access to NRG Framework through transitive dependencies if you include those libraries in your code as well.
Data entity
A data entity class should be, as much as possible, nothing but a container for data. It provides no functionality, no operations, no means of transformation. It is simply a noun, a thing, a collection of properties. The NRG entity service framework provides an abstract version of an entity from which full implementations can be created. This abstract class, implemented in org.nrg.framework.orm.hibernate.AbstractHibernateEntity, provides the following properties:
- long id
- boolean enabled
- Date created
- Date timestamp
- Date disabled
The Scanner data entity has three properties unique to its representation, the scanner name, the manufacturer, and the imaging modality for the scanner. You might also want a unique readable scanner ID to make it easy to identify each instance. In Java, this would look something like this:
Sample entity class
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "scannerId"),
@UniqueConstraint(columnNames = {"name", "manufacturer"})})
@Data
@Accessors(prefix = "_")
public class Scanner extends AbstractHibernateEntity {
private String _scannerId;
private String _name;
private String _manufacturer;
private String _modality;
}
This sample uses Lombok annotations to generate the standard get and set methods so you don't need to explicitly define, e.g., getScannerId(). If you'd rather define the methods yourself, just omit the @Data and @Accessors annotations. You can find more information about Lombok on the Project Lombok web site, as well as in lots of tutorials and walkthroughs.
Notice that this class contains no code to manage database transactions, SQL queries, or anything else related to storing an instance of the entity in the database. So how does an instance of this class get persisted? In the context of the entity class, it doesn't really matter. That's the work of the service layer. This class only provides a little bit of information that even indicates that it's supposed to be a data entity:
- The @Entity annotation indicates that this is an entity class. This annotation is meaningful to a number of Java persistence frameworks, including Hibernate.
- The @Table annotation and the @UniqueConstraint annotations it contains are basically advice to persistence frameworks indicating that there should only be a single entry in the table for each subject ID and only a single entry for each combination of source system and record ID.
- The AbstractHibernateEntity class that this entity extends is an NRG framework class that provides a bunch of standard properties that are useful for objects stored in the database. It also makes it so that this object can be managed by the NRG framework entity management classes.
Everything else about storing and retrieving the data in an instance of an entity class is handled at the repository and service implementations described in the next sections.
Repository
The data access object (DAO) or repository (DAO and repository are used basically interchangeably) performs atomic database operations, handles basic framework details like session and state management, and implements specific low-level operations like detailed queries and joins.
Similar to the entity class support, the NRG entity service framework provides a base implementation named org.nrg.framework.orm.hibernate.AbstractHibernateDAO. This includes the following methods (mentions of "ID" here reference the ID value in the AbstractHibernateEntity class, which is a Java long):
Method | Description |
---|---|
create() | Creates a new entry in the database for the submitted data entity instance. |
retrieve() | Retrieves the database entry for the specified ID. |
update() | Updates the entry in the database for the submitted data entity instance. |
saveOrUpdate() | If the instance doesn't have an ID or the ID doesn't exist in the database yet, this creates a new database entry. If the instance ID already exists, this updates the corresponding database entry. |
delete() | Deletes the database entry for the specified ID. |
exists() | Checks whether there's an entry in the database with the specified ID. |
countAll() | Tells you how many entries for the class are stored in the database. |
findByExample() | Returns the entry that matches the example. |
findAll() | Returns a list containing all of the existing entries. |
findAllByExample() | Returns a list containing all of the entries that match the example. |
findByProperties() | Returns a list of entries with property values that match those in the submitted table. |
findByProperty() | Similar to above, but just takes a single property name and value. |
findByUniqueProperty() | Returns an entry that matches a single property name and value. |
By extending this base class in your own repository implementation, you get all of the basic data operations–create, retrieve, update, delete, and a variety of find operations including the ability to find by example and by or more more properties–without writing any more code than just declaring your class, like so:
Scanner repository
@Repository
public class ScannerRepository extends AbstractHibernateDAO<Scanner> {
}
The @Repository annotation indicates that this is a data repository. Notice that the Scanner class is referenced here. Adding the extends clause to the Scanner class makes it an entity that the AbstractHibernateDAO implementation knows how to manage.
Service
The service implementation performs business or domain operations, which combine one or more database operations into a single transaction.
The NRG framework provides a base implementation for service classes as well. For various technical reasons, you should divide your service classes into an interface definition and an actual implementation. The interface definition specifies the operations available through the service.
Scanner service interface
public interface ScannerService extends BaseHibernateService<Scanner> {
Scanner findByScannerId(final String scannerId);
List<Scanner> findByManufacturer(final String manufacturer);
List<Scanner> findByModality(final String modality);
List<Scanner> findByManufacturerAndModality(final String manufacturer, final String modality);
}
Note that this interface extends another interface and that is also configured so that your interface is associated with the Scanner entity. This interface defines some of the basic operations you'd like to be able to perform with this service:
Method | Description |
---|---|
create() | Creates a new entry in the database for the submitted data entity instance. |
retrieve() | Retrieves the database entry for the specified ID. |
update() | Updates the entry in the database for the submitted data entity instance. |
delete() | Deletes the entry in the database for the submitted data entity instance. |
exists() | Checks whether there's an entry in the database with the specified ID. |
getCount() | Tells you how many entries for the class are stored in the database. |
getAll() | Returns a list containing all of the existing entries. |
validate() | Validates that an instance of the entity class conforms to any rules required for the type. |
You might notice that these methods are very similar to the ones on the abstract DAO class. That doesn't mean that implementations extending the abstract service have to be that simple or mirror the repository though, just that the basic operations required from a service are the same as the basic operations found in the DAO class: CRUD and query operations.
The last step is implementing your service class:
Subject mapping service implementation
@Service
public class HibernateScannerService
extends AbstractHibernateEntityService<Scanner, ScannerRepository>
implements ScannerService {
@Transactional
@Override
public Scanner findByScannerId(final String scannerId) {
return getDao().findByUniqueProperty("scannerId", scannerId);
}
@Transactional
@Override
public List<Scanner> findByManufacturer(final String manufacturer) {
return getDao().findByProperty("manufacturer", manufacturer);
}
@Transactional
@Override
public List<Scanner> findByModality(final String modality) {
return getDao().findByProperty("modality", modality);
}
@Transactional
@Override
public List<Scanner> findByManufacturerAndModality(final String manufacturer, final String modality) {
final Map<String, Object> properties = new HashMap<>();
properties.put("manufacturer", manufacturer);
properties.put("modality", modality);
return getDao().findByProperties(properties);
}
}
Important things to notice about this implementation include:
- The @Service annotation indicates that this is a service implementation. Like the @Repository annotation, we'll discuss how this comes into play when we look at component scanning.
- This implementation extends the AbstractHibernateEntityService, which provides implementations for the basic operations defined in the BaseHibernateService interface.
- It also associates itself with both the Scanner entity class and the ScannerRepository class. The service implementation now understands how to work with both of these classes.
- Each method in this implementation is annotated with the @Transactional annotation. It's this annotation that tells the database transaction manager where the business operations begin and end, so that if an error occurs inside that transactional operation, the overall operation can be rolled back.