The primary goal of the Spring Data project is to make it easier to build Spring-powered applications that use data access technologies. Spring Data JDBC offers the popular Repository abstraction based on JDBC.
Spring Data JDBC does not try to be an ORM. It is not a competitor to JPA. Instead it is more of a construction kit for your personal ORM that you can define the way you like or need it.
This means that it does rather little out of the box. But it offers plenty of places where you can put your own logic, or integrate it with the technology of your choice for generating SQL statements.
Spring Data repositories are inspired by the repository as described in the book Domain Driven Design by Eric Evans. One consequence of this is that you should have a repository per Aggregate Root. Aggregate Root is another concept from the same book and describes an entity which controls the lifecycle of other entities which together are an Aggregate. An Aggregate is a subset of your model which is consistent between method calls to your Aggregate Root.
Spring Data JDBC tries its best to encourage modelling your domain along these ideas.
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
In order to use Spring Data JDBC you need the following:
-
An entity with an attribute marked as id using the Spring Data
@Id
annotation.public class Person { @Id Integer id; }
-
A repository
public interface PersonRepository extends CrudRepository<Person, Integer> {}
-
Add
@EnableJdbcRepositories
to your application context configuration. -
Make sure your application context contains a bean of type
DataSource
.
Now you can get an instance of the repository interface injected into your beans and use it:
@Autowired
private PersonRepository repository;
public void someMethod() {
Person person = repository.save(new Person());
}
Properties of the following types are currently supported:
-
all primitive types and their boxed types (
int
,float
,Integer
,Float
…) -
enums get mapped to their name.
-
String
-
java.util.Date
,java.time.LocalDate
,java.time.LocalDateTime
,java.time.LocalTime
and anything your database driver accepts.
-
references to other entities, which will be considered a one-to-one relationship. The table of the referenced entity is expected to have an additional column named like the table of the referencing entity. This name can be changed by implementing
NamingStrategy.getReverseColumnName(JdbcPersistentProperty property)
according to your preferences. -
Set<some entity>
will be considered a one-to-many relationship. The table of the referenced entity is expected to have an additional column named like the table of the referencing entity. This name can be changed by implementingNamingStrategy.getReverseColumnName(JdbcPersistentProperty property)
according to your preferences. -
Map<simple type, some entity>
will be considered a qualified one-to-many relationship. The table of the referenced entity is expected to have two additional columns: One named like the table of the referencing entity for the foreign key and one with the same name and an additional_key
suffix for the map key. This name can be changed by implementingNamingStrategy.getReverseColumnName(JdbcPersistentProperty property)
andNamingStrategy.getKeyColumn(JdbcPersistentProperty property)
according to your preferences. -
List<some entity>
will be mapped like aMap<Integer, some entity>
.
The handling of referenced entities is very limited. Part of this is because this project is still before its first release.
But another reason is the idea of Aggregate Roots as described above. If you reference another entity that entity is by definition part of your Aggregate. So if you remove the reference it will get deleted. This also means references will be 1-1 or 1-n, but not n-1 or n-m.
If you are having n-1 or n-m references you are probably dealing with two separate Aggregates. References between those should be encoded as simple ids, which should map just fine with Spring Data JDBC.
Also the mapping we offer is very limited for a third reason which already was mentioned at the very beginning of the document: this is not an ORM. We will offer ways to plug in your own SQL in various ways. But the default mapping itself will stay limited. If you want highly customizable mappings which support almost everything one can imagine you will probably be much happier with (Spring Data) JPA, which is a very powerful and mature technology.
You can annotate a query method with @Query
to specify a SQL statement to be used for that method.
You can bind method arguments using named parameters in the SQL statement like in the following example:
@Query("SELECT * FROM DUMMYENTITY WHERE name < :upper and name > :lower")
List<DummyEntity> findByNameRange(@Param("lower") String lower, @Param("upper") String upper);
If you compile your sources with the -parameters
compiler flag you can omit the @Param
annotations.
You can configure the RowMapper
to use, using either the @Query(rowMapperClass = ….)
or you can register a RowMapperMap
bean and register RowMapper
per method return type.
@Bean
RowMapperMap rowMappers() {
return new ConfigurableRowMapperMap() //
.register(Person.class, new PersonRowMapper()) //
.register(Address.class, new AddressRowMapper());
}
When determining the RowMapper
to use for a method the following steps are followed based on the return type of the method:
-
If the type is a simple type, no
RowMapper
is used. Instead the query is expected to return a single row with a single column and a conversion to the return type is applied to that value. -
The entity classes in the
RowMapperMap
are iterated until one is found that is a superclass or interface of the return type in question. TheRowMapper
registered for that class is used. Iterating happens in the order of registration, so make sure to register more general types after specific ones.
If applicable, wrapper types like collections or Optional
are unwrapped.
Thus, a return type of Optional<Person>
will use the type Person
in the steps above.
You can mark as a modifying query using the @Modifying
on query method.
@Modifying
@Query("UPDATE DUMMYENTITY SET name = :name WHERE id = :id")
boolean updateName(@Param("id") Long id, @Param("name") String name);
The return types that can be specified are void
, int
(updated record count) and boolean
(whether record was updated).
Spring Data JDBC uses the id to identify entities, but also to determine if an entity is new or already existing in the database.
If the id is null
or of a primitive type having value 0
or 0.0
, the entity is considered new.
If your database has some autoincrement-column for the id-column, the generated value will get set in the entity after inserting it into the database.
There are few ways to tweak this behavior.
If you don’t like the logic to distinguish between new and existing entities you can implement Persistable
with your entity and overwrite isNew()
with your own logic.
One important constraint is that after saving an entity the entity shouldn’t be new anymore.
With autoincrement-columns this happens automatically since the id gets set by Spring Data with the value from the id-column.
If you are not using autoincrement-columns, you can use a BeforeSave
-listener which sets the id of the entity (see below).
If you use the standard implementations of CrudRepository
as provided by Spring Data JDBC, it will expect a certain table structure.
You can tweak that by providing a NamingStrategy
in your application context.
In many cases a DelimiterNamingStrategy
might be good basis for a custom implementation
Spring Data JDBC triggers events which will get published to any matching ApplicationListener
in the application context.
For example, the following listener will get invoked before an Aggregate gets saved.
@Bean
public ApplicationListener<BeforeSaveEvent> timeStampingSaveTime() {
return event -> {
Object entity = event.getEntity();
if (entity instanceof Category) {
Category category = (Category) entity;
category.timeStamp();
}
};
}
Event | When It’s Published |
---|---|
before an aggregate root gets deleted. |
|
after an aggregate root got deleted. |
|
before an aggregate root gets saved, i.e. inserted or updated but after the decision was made if it will get updated or deleted.
The event has a reference to an |
|
after an aggregate root gets saved, i.e. inserted or updated. |
|
after an aggregate root got created from a database |
For each operation in CrudRepository
Spring Data JDBC will execute multiple statements.
If there is a SqlSessionFactory
in the application context, it will be checked if it offers a statement for each step.
If one is found, that statement will be used (including its configured mapping to an entity).
By default, the name of the statement is constructed by concatenating the fully qualified name of the entity type with Mapper.
and a string determining the kind of statement.
E.g. if an instance of org.example.User
is to be inserted, Spring Data JDBC will look for a statement named org.example.UserMapper.insert
.
Upon execution of the statement an instance of [MyBatisContext
] will get passed as an argument which makes various arguments available to the statement.
Name | Purpose | CrudRepository methods which might trigger this statement | Attributes available in the MyBatisContext |
---|---|---|---|
|
Insert for a single entity. This also applies for entities referenced by the aggregate root. |
|
|
|
Update for a single entity. This also applies for entities referenced by the aggregate root. |
|
|
|
Delete a single entity. |
|
|
|
Delete all entities referenced by any aggregate root of the type used as prefix via the given property path. Note that the type used for prefixing the statement name is the name of the aggregate root, not the one of the entity to be deleted. |
|
|
|
Delete all aggregate roots of the type used as the prefix |
|
|
|
Delete all entities referenced by an aggregate root via the given propertyPath |
|
|
|
Select an aggregate root by id |
|
|
|
Select all aggregate roots |
|
|
|
Select a set of aggregate roots by ids |
|
|
|
Select a set of entities that is referenced by another entity. The type of the referencing entity is used for the prefix. The referenced entities type as the suffix. |
All |
|
|
Count the number of aggregate root of the type used as prefix |
|
|
You can customize the namespace part of a statement name using NamespaceStrategy
.
The current MyBatis supported is rather elaborate in that it allows to execute multiple statements for a single method call. But sometimes less is more, and it should be possible to annotate a method with a simple annotation to identify a SQL statement in a MyBatis mapping to be executed.
There is preliminary Spring Boot integration.
Currently you will need to build it locally.
Right now the best source of information is the source code in this repository.
Especially the integration tests (if you are reading this on github, type t
and then IntegrationTests.java
)
We are keeping an eye on the (soon to be created) spring-data-jdbc tag on stackoverflow.
If you think you found a bug, or have a feature request please create a ticket in our issue tracker.
Fast running tests can be executed with a simple
mvn test
This will execute unit tests and integration tests using an in-memory database.
In order to run the integration tests against a specific database you need to have a local Docker installation available, and then execute.
mvn test -Dspring.profiles.active=<databasetype>
This will also execute the unit tests.
Currently the following databasetypes are available:
-
hsql (default, does not require a running database)
-
mysql
-
postgres
-
mariadb
Here are some ways for you to get involved in the community:
-
Get involved with the Spring community by helping out on stackoverflow by responding to questions and joining the debate.
-
Create JIRA tickets for bugs and new features and comment and vote on the ones that you are interested in.
-
Github is for social coding: if you want to write code, we encourage contributions through pull requests from forks of this repository. If you want to contribute code this way, please reference a JIRA ticket as well, covering the specific issue you are addressing.
-
Watch for upcoming articles on Spring by subscribing to spring.io.
Before we accept a non-trivial patch or pull request we will need you to sign the Contributor License Agreement. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. If you forget to do so, you’ll be reminded when you submit a pull request. Active contributors might be asked to join the core team, and given the ability to merge pull requests.