Spring Data is an umbrella project under the Spring portfolio, designed to provide consistent data access across a number of different data stores including relational and NoSQL Databases, and other types of data stores such as REST (HTTP), search engines, and Hadoop. Under Spring Data, there are subprojects for each specific approach and data store, put together by companies or developers of those technologies. Spring Data significantly simplifies the building of the data layer regardless of the underlying database and persistence technology.
The following table lists a few Spring Data subprojects with a short description of each:
The Spring Data portfolio contains community modules for more data stores that are not covered by the official Spring Data projects. Communities of several very popular open source and proprietary databases are contributing to these projects, which makes Spring Data an excellent source of proven solutions for building the data-access layer of enterprise applications regardless of the underlying data store. Cassandra, Neo4J, Couchbase, and ElasticSearch are some examples of community projects based on Spring Data.
Spring Data standardizes data-access via all its store-specific modules (subprojects) through a consistent API called Spring Data Commons. Spring Data Commons is the foundational specification and a guideline for all Spring Data Modules. All Spring Data subprojects are store-specific implementations of Spring Data Commons.
Spring Data Commons defines the core components and general behaviors of Spring Data modules.
We will examine each of these components, their setup, and usage in the following sections.
org.springframework.data.repository.Repository
is the central interface of Spring Data abstraction. This marker interface is a part of Spring Data Commons and has two specialized extensions, CrudRepository
and PagingAndSortingRepository
.
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { ... }
A repository manages a domain entity (designed as a POJO). CrudRepository
provides CRUD with the following CRUD operations for an entity.
save(One)
, save(List)
find
, findOne
, findAll
delete
, deleteAll
count
exists
PagingAndSortingRepository
adds pagination and sorting features over CrudRepository
. It has the following two methods:
Page<T> findAll(Pageable)
Iterable<T> findAll(Sort)
Now is time to jump ahead and discuss the technology and store-specific modules of Spring Data. We are covering Spring Data JPA and Spring Data MongoDB to illustrate two totally different worlds in the database universe: relational and NoSQL. When we use a specific implementation, we use an implementation-specific repository but your method interfaces remain the same; hence, theoretically, a switch from a specific Spring Data implementation to another would not affect your client programs (service, controller, or test cases).
Spring Data JPA is the JPA (Java Persistence Architecture)-based implementation of Spring Data, dealing with object-relational data access. For a developer, most of the programming is based on what is described in Spring Data Commons, whereas Spring Data JPA allows for some extra customizations specific to relational SQL and JPA. The main difference is in the repository setup and the query optimization using the @Query
annotation.
Enabling Spring Data JPA in your project is a simple two-step process:
spring-data-jpa
dependency to your maven/gradle
build file.In Maven, you can add a spring-data-jpa
dependency as shown in the following code:
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>${spring-data-jpa.version}</version> </dependency>
You can enable JPA repositories, as shown in the following line, if you are using XML:
<jpa:repositories base-package="com.taskify.dao" />
In the case of Java configuration, you just annotate to enable JPA repositories.
@Configuration @ComponentScan(basePackages = {"com.taskify"}) @EnableJpaRepositories(basePackages = "com.taskify.dao") public class JpaConfiguration { ... }
After enabling JPA repositories, Spring scans the given package for Java classes annotated with @Repository
, and creates fully-featured proxy objects ready to be used. These are your DAO, where you just define the methods, Spring gives you proxy-based implementations at runtime. See a simple example:
public interface TaskDAO extends JpaRepository<Task, Long>{ List<Task> findByAssigneeId(Long assigneeId); List<Task> findByAssigneeUserName(String userName); }
Spring generates smart implementations that actually perform the required database operations for these methods inside the proxy implementation, looking at the method names and arguments.
MongoDB is one of the most popular document-oriented NoSQL databases. It stores data in BSON (Binary JSON) format, allowing you to store an entire complex object in nested structures, avoiding the need to break data into a lot of relational tables. Its nested object structure maps directly to object-oriented data structures and eliminates the need for any object-relational mapping, as is the case with JPA/Hibernate.
Spring Data MongoDB is the Spring Data module for MongoDB. It allows Java objects to be mapped directly into MongoDB documents. It also provides a comprehensive API and infrastructural support for connecting to MongoDB and manipulating its document collections.
Spring Data MongoDB can be enabled with the following steps:
spring-data-mongodb
to your build file (maven/gradle
).mongoTemplate
Spring Bean to your Spring metadata.Adding the spring-data-mongodb
dependency with Maven should look like this:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.framework.version}</version> </dependency>
You can register a Mongo instance in your XML metadata, as shown in the following line:
<mongo:mongo host="192.168.36.10" port="27017" />
This Mongo instance is a proxy of your actual MongoDB instance running on a server.
A simplistic mongoTemplate
looks like the listing given in the following code:
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate"> <constructor-arg ref="mongo" /> <constructor-arg name="databaseName" value="Taskify" /> </bean>
MongoRepository is the MongoDB-specific repository for Spring Data MongoDB. It looks very similar to JpaRepository
. Take a look at a sample MongoRepository
class:
public interface TaskDAO extends MongoRepository<Task, String>{ List<Task> findByAssigneeId(String assigneeId); @Query("{ 'status' : 'Complete' }") List<Task> findCompletedTasks(); @Query(value = "{ 'status' : 'Open', assignee.id: ?0 }") List<Task> findOpenTasksByAssigneeId(String assigneeId); ... }
Data-driven applications often design domain objects as entities and then persist them into databases either as relational tables or document structures of key-value pairs at runtime. Spring Data deals with domain entities like any other persistence framework. In order to illustrate the usage of a repository, we will refer to the following three related entities, designed as Plain Old Java Objects (POJOs) in your program.
The following are the Java representations. The first one is annotated for JPA and the other two for MongoDB. JPA entities are annotated with @Entity
. Columns are mapped against each field. Remember that, instead of annotations, you can use XML-based mapping for JPA entities too. XML mapping offers several benefits including centralized control and maintainability. This example uses annotations for simplicity, assuming that the reader is already familiar with JPA or Hibernate mappings.
@Entity @Table(name = "TBL_USER", uniqueConstraints = @UniqueConstraint(name = "UK_USER_USERNAME", columnNames = {"USER_NAME" }) ) public class User { @Id @SequenceGenerator(name = "SEQ_USER", sequenceName = "SEQ_USER", allocationSize = 1, initialValue=1001) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_USER") private Long id; @Column(name = "NAME", length = 200) private String name; @Column(name = "USER_NAME", length = 25) private String userName; @Column(name = "PASSWORD", length = 20) private String password; @Column(name = "DOB") @Temporal(TemporalType.TIMESTAMP) private Date dateOfBirth; @ManyToOne(optional = true) @JoinColumn(name = "FILE_ID", referencedColumnName = "ID") private File profileImage; public User() {} public User(Long id, String name, String userName, String password, Date dateOfBirth) { super(); this.id = id; this.name = name; this.userName = userName; this.password = password; this.dateOfBirth = dateOfBirth; } public Long getId() { return id; } ... }
The following is the task entity, annotated as a MongoDB document. Mongo entities are annotated with @Document
. It requires an ID field, either annotated with @Id
or with the name id
.
@Document(collection = "tasks") public class Task { @Idprivate String id; private String name; private int priority; private String status; private User createdBy; private Date createdDate; private User assignee; private Date completedDate; private String comments; public Task() {} ... }
The file entity is annotated as a JPA entity.
@Entity @Table(name = "TBL_FILE") public class File { @Id @SequenceGenerator(name = "SEQ_FILE", sequenceName = "SEQ_FILE", allocationSize = 1) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_FILE") private Long id; @Column(name = "FILE_NAME", length = 200) private String fileName; ... }
In addition to the declared query (find
, count
, delete
, remove
, and exists
) methods at interface level, CrudRepository
supports declared queries using the @Query
annotation methods with any name, which helps to derive the actual SQL queries from the SpEL (Spring Expression Language) expression given as a parameter. Of these two query deriving options, Spring Data adopts one based on the following query lookup strategies:
Query Lookup Strategy |
Description |
---|---|
|
Generates module-specific queries from the method name. |
|
Uses a query declared by an annotation or some other means. |
|
This strategy combines the first two. This is the default strategy. |
The query lookup strategy is normally set while enabling JPA repositories.
<jpa:repositories base-package="com.taskify.dao" query-lookup-strategy="create-if-not-found"/>
The query generation strategy (CREATE
) works around the properties of the entity, including their dependencies, in a nested direction. As a developer, you define method names based on a specific format that can be interpreted and realized by Spring Data. The general structure of the query method is shown here:
[return Type] [queryType][limitKeyword]By[criteria][OrderBy][sortDirection]
return type
can be the entity <T>
itself (in the case of a unique result), a list <T>
, a stream <T>
, page <T>
, primitive numbers, Java wrapper types, void, future <T>
, CompletableFuture<T>
, ListenableFuture<T>
, and so on. The last three are for Spring's asynchronous method execution and should be annotated with @Async
.queryType
can be find
, read
, query
, count
, exists
, delete
, and so on.limitKeyword
supports distinct
, First[resultSize]
, and Top[resultSize]
. An example is First5
.criteria
is built by combining one or more property expressions (using camel-casing) with standard operators such as Or
, And
, Between
, GreaterThan
, LessThan
, IsNull
, StartsWith
, and Exists
. Criteria can be suffixed by IgnoreCase
or AllIgnoreCase
, to apply case insensitivity.OrderBy
is used as it is, suffixed by property expressions.sortDirection
can be either of Asc
or Desc
. This is used only with OrderBy
.Let's see some examples for better clarity. The following sample code illustrates how to construct query (or delete) methods so that Spring Data can generate the actual SQL query at runtime.
public interface UserDAO extends JpaRepository<User, Long> { // Returns unique user with given user-name User findByUserName(String userName); // Returns a paginated list of users whose name starts with // given value Page<User> findByNameStartsWith(String name, Pageable pageable); // Returns first 5 users whose name starts with given value, // order by name descending List<User> findTop5ByNameStartsWithOrderByNameDesc(String name); // Returns number of users whose birth date is before the given // value Long countUsersDateOfBirthLessThan(Date dob); // Deletes the User of given id void deleteById(Long userId); // Asynchronously returns a list of users whose name contains // the given value @Async Future<List<User>> findByNameContains(String name); }
The preceding example showing JpaRepository
and MongoRepository
works in the same way; you just need to extend from it, without changing the method signatures. You have seen the constraining query and filter methods traversing root-level properties of the entity, combining operators appropriately. Besides root-level properties, you can traverse and filter by nested properties as well, to define query constraints, in other words, limiting the result. Take a look at the following example:
public interface TaskDAO extends MongoRepository<Task, String>{ List<Task> findByAssigneeId(Long assigneeId); List<Task> findByAssigneeUserName(String userName); }
The methods listed in the preceding example are traversing nested properties of the task entity:
findByAssigneeId = task.assignee.id
findByAssigneeUserName = task.assignee.userName
You can traverse into any level of nested elements of your entity, depending on how complex your entity and requirements are.
Besides the autogeneration of queries based on method names as demonstrated in the previous section, Spring Data allows you to declare queries for entities locally, directly in the repository itself, over the method names. You declare the query using SpEL, and Spring Data interprets it at runtime and (the proxy repository) generates the queries for you. This is an implementation of the query resolution strategy: USE_DECLARED_QUERY
.
Let's take a look at some self-explanatory examples:
public interface TaskDAO extends JpaRepository<Task, Long>{ @Query("select t from Task t where status = 'Open'") List<Task> findOpenTasks(); @Query("select t from Task t where status = 'Complete'") List<Task> findCompletedTasks(); @Query("select count(t) from Task t where status = 'Open'") int findAllOpenTasksCount(); @Query("select count(t) from Task t where status = 'Complete'") int findAllCompletedTasksCount(); @Query("select t from Task t where status = 'Open' and assignee.id = ?1") List<Task> findOpenTasksByAssigneeId(Long assigneeId); @Query("select t from Task t where status = 'Open' and assignee.userName = ?1") List<Task> findOpenTasksByAssigneeUserName(String userName); @Query("select t from Task t where status = 'Complete' and assignee.id = ?1") List<Task> findCompletedTasksByAssigneeId(Long assigneeId); @Query("select t from Task t where status = 'Complete' and assignee.userName = ?1") List<Task> findCompletedTasksByAssigneeUserName(String userName); }
You can see from the preceding example that we can traverse into nested properties to constrain the queries, in the criteria part of it. You can also have both query generation strategies (CREATE
and USE_DECLARED_QUERY
) in the same repository.
The preceding example was based on Spring Data JPA; the Spring Data MongoDB equivalent is given in the following code. You can see how the @Query
annotation values differ in comparison to the MongoDB structure.
public interface TaskDAO extends MongoRepository<Task, String>{ @Query("{ 'status' : 'Open' }") List<Task> findOpenTasks(); @Query("{ 'status' : 'Complete' }") List<Task> findCompletedTasks(); @Query(value = "{ 'status' : 'Open' }", count = true) int findAllOpenTasksCount(); @Query(value = "{ 'status' : 'Complete' }", count = true) int findAllCompletedTasksCount(); @Query(value = "{ 'status' : 'Open', assignee.id: ?0 }") List<Task> findOpenTasksByAssigneeId(String assigneeId); @Query(value = "{ 'status' : 'Open', assignee.userName: ?0 }") List<Task> findOpenTasksByAssigneeUserName(String userName); @Query(value = "{ 'status' : 'Complete', assignee.id: ?0 }") List<Task> findCompletedTasksByAssigneeId(String assigneeId); @Query(value = "{ 'status' : 'Open', assignee.userName: ?0 }") List<Task> findCompletedTasksByAssigneeUserName(String userName); }
Spring Data provides a smart extension called SpringDataWebSupport
to Spring MVC applications, integrating a few productivity components automatically if you enable it. It primarily resolves domain entities as Pageable
and Sort
instances with request-mapping controller methods directly from request parameters, if you are using Spring Data repository programming model for data access.
You need to enable SpringDataWebSupport
for your project before you can use the features. You can annotate @EnableSpringDataWebSupport
, as shown in the following code, if you are using a Java configuration:
@Configuration @EnableWebMvc @ComponentScan(basePackages = {"com.taskify"}) @EnableSpringDataWebSupport @EnableJpaRepositories(basePackages = "com.taskify.dao") public class ApplicationConfiguration { ... }
In the case of XML metadata, you can register SpringDataWebConfiguration
as a Spring bean, as shown in the following code:
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
Once you set up SpringDataWebSupport
, you can start using Spring Data entities as request arguments with request-mapping methods, as shown in the following code:
@RestController @RequestMapping("/api/v1/user") @CrossOrigin public class UserController { @RequestMapping(path = "/{id}", method = RequestMethod.GET) public User getUser(@PathVariable("id") User user) { return user; } ... }
In the preceding method, you can see that Spring Data loads the User
entity data using UserRepository
transparently for you. Similarly, you can accept Pageable
and Sort
instances against JSON or XML post requests. Wise usage of the SpringDataWebSupport
extension makes your code cleaner and more maintainable.
Tracking data modifications is a critical feature of serious business applications. Administrators and managers are anxious to know when and who changed certain business information saved in the database. Spring Data provides smart and easy methods for auditing data entities transparently. Spring Data ships the following meaningful annotations for capturing modified user and time data entities in the system:
A typical JPA entity should look like the following code:
@Entity @Table(name = "tbl_task") public class Task { @Id private Long id; ... @ManyToOne(optional = true) @JoinColumn(name = "CREATED_USER_ID", referencedColumnName = "ID") @CreatedBy private User createdBy; @Column(name = "CREATED_DATE") @Temporal(TemporalType.TIMESTAMP) @CreatedDate private Date createdDate; @ManyToOne(optional = true) @JoinColumn(name = "MODIFIED_USER_ID", referencedColumnName = "ID") @LastModifiedBy private User modifiedBy; @Column(name = "MODIFIED_DATE") @Temporal(TemporalType.TIMESTAMP) @LastModifiedDate private Date modifiedDate; ... }
If you are using XML instead of annotations to map your entities, you can either implement an auditable interface, which forces you to implement the audit metadata fields, or extend AbstractAuditable
, a convenient base class provided by Spring Data.
Since you are recording the information of the user who is creating and modifying entities, you need to help Spring Data to capture that user information from the context. You need to register a bean that implements AuditAware<T>
, where T
is the same type of field that you annotated with @CreatedBy
and @LastModifiedBy
. Take a look at the following example:
@Component public class SpringDataAuditHelper implements AuditorAware<User> { ... @Override public User getCurrentAuditor() { // Return the current user from the context somehow. } }
If you are using Spring Security for authentication, then the getCurrentAuditor
method should get and return the user from the SecurityContextHolder
class, as follows:
@Component public class SpringDataAuditHelper implements AuditorAware<User> { ... @Override public User getCurrentAuditor() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { return null; } return ((User) authentication.getPrincipal()).getUser(); } }
Now your auditing infrastructure is ready, any modification you make in your auditable entities will be tracked transparently by Spring Data.
So far you have mastered the mighty Spring Data and you know how to create elegant and clean yet really powerful data access layers with Spring Data repositories, so now it is time to think about how to ensure the data integrity and reliability of your application. Spring Transaction is the answer; let's explore it in the next section.