For the latest stable version, please use Spring Data Relational 3.4.0! |
Persisting Entities
R2dbcEntityTemplate
is the central entrypoint for Spring Data R2DBC.
It provides direct entity-oriented methods and a more narrow, fluent interface for typical ad-hoc use-cases, such as querying, inserting, updating, and deleting data.
The entry points (insert()
, select()
, update()
, and others) follow a natural naming schema based on the operation to be run.
Moving on from the entry point, the API is designed to offer only context-dependent methods that lead to a terminating method that creates and runs a SQL statement.
Spring Data R2DBC uses a R2dbcDialect
abstraction to determine bind markers, pagination support and the data types natively supported by the underlying driver.
All terminal methods return always a Publisher type that represents the desired operation.
The actual statements are sent to the database upon subscription.
|
Methods for Inserting and Updating Entities
There are several convenient methods on R2dbcEntityTemplate
for saving and inserting your objects.
To have more fine-grained control over the conversion process, you can register Spring converters with R2dbcCustomConversions
— for example Converter<Person, OutboundRow>
and Converter<Row, Person>
.
The simple case of using the save operation is to save a POJO. In this case, the table name is determined by name (not fully qualified) of the class. You may also call the save operation with a specific collection name. You can use mapping metadata to override the collection in which to store the object.
When inserting or saving, if the Id
property is not set, the assumption is that its value will be auto-generated by the database.
Consequently, for auto-generation the type of the Id
property or field in your class must be a Long
, or Integer
.
The following example shows how to insert a row and retrieving its contents:
R2dbcEntityTemplate
Person person = new Person("John", "Doe");
Mono<Person> saved = template.insert(person);
Mono<Person> loaded = template.selectOne(query(where("firstname").is("John")),
Person.class);
The following insert and update operations are available:
A similar set of insert operations is also available:
-
Mono<T>
insert(T objectToSave)
: Insert the object to the default table. -
Mono<T>
update(T objectToSave)
: Insert the object to the default table.
Table names can be customized by using the fluent API.
Selecting Data
The select(…)
and selectOne(…)
methods on R2dbcEntityTemplate
are used to select data from a table.
Both methods take a Query
object that defines the field projection, the WHERE
clause, the ORDER BY
clause and limit/offset pagination.
Limit/offset functionality is transparent to the application regardless of the underlying database.
This functionality is supported by the R2dbcDialect
abstraction to cater for differences between the individual SQL flavors.
R2dbcEntityTemplate
Flux<Person> loaded = template.select(query(where("firstname").is("John")),
Person.class);
Fluent API
This section explains the fluent API usage. Consider the following simple query:
Flux<Person> people = template.select(Person.class) (1)
.all(); (2)
1 | Using Person with the select(…) method maps tabular results on Person result objects. |
2 | Fetching all() rows returns a Flux<Person> without limiting results. |
The following example declares a more complex query that specifies the table name by name, a WHERE
condition, and an ORDER BY
clause:
Mono<Person> first = template.select(Person.class) (1)
.from("other_person")
.matching(query(where("firstname").is("John") (2)
.and("lastname").in("Doe", "White"))
.sort(by(desc("id")))) (3)
.one(); (4)
1 | Selecting from a table by name returns row results using the given domain type. |
2 | The issued query declares a WHERE condition on firstname and lastname columns to filter results. |
3 | Results can be ordered by individual column names, resulting in an ORDER BY clause. |
4 | Selecting the one result fetches only a single row.
This way of consuming rows expects the query to return exactly a single result.
Mono emits a IncorrectResultSizeDataAccessException if the query yields more than a single result. |
You can directly apply Projections to results by providing the target type via select(Class<?>) .
|
You can switch between retrieving a single entity and retrieving multiple entities through the following terminating methods:
-
first()
: Consume only the first row, returning aMono
. The returnedMono
completes without emitting an object if the query returns no results. -
one()
: Consume exactly one row, returning aMono
. The returnedMono
completes without emitting an object if the query returns no results. If the query returns more than one row,Mono
completes exceptionally emittingIncorrectResultSizeDataAccessException
. -
all()
: Consume all returned rows returning aFlux
. -
count()
: Apply a count projection returningMono<Long>
. -
exists()
: Return whether the query yields any rows by returningMono<Boolean>
.
You can use the select()
entry point to express your SELECT
queries.
The resulting SELECT
queries support the commonly used clauses (WHERE
and ORDER BY
) and support pagination.
The fluent API style let you chain together multiple methods while having easy-to-understand code.
To improve readability, you can use static imports that let you avoid using the 'new' keyword for creating Criteria
instances.
Methods for the Criteria Class
The Criteria
class provides the following methods, all of which correspond to SQL operators:
-
Criteria
and(String column)
: Adds a chainedCriteria
with the specifiedproperty
to the currentCriteria
and returns the newly created one. -
Criteria
or(String column)
: Adds a chainedCriteria
with the specifiedproperty
to the currentCriteria
and returns the newly created one. -
Criteria
greaterThan(Object o)
: Creates a criterion by using the>
operator. -
Criteria
greaterThanOrEquals(Object o)
: Creates a criterion by using the>=
operator. -
Criteria
in(Object… o)
: Creates a criterion by using theIN
operator for a varargs argument. -
Criteria
in(Collection<?> collection)
: Creates a criterion by using theIN
operator using a collection. -
Criteria
is(Object o)
: Creates a criterion by using column matching (property = value
). -
Criteria
isNull()
: Creates a criterion by using theIS NULL
operator. -
Criteria
isNotNull()
: Creates a criterion by using theIS NOT NULL
operator. -
Criteria
lessThan(Object o)
: Creates a criterion by using the<
operator. -
Criteria
lessThanOrEquals(Object o)
: Creates a criterion by using the⇐
operator. -
Criteria
like(Object o)
: Creates a criterion by using theLIKE
operator without escape character processing. -
Criteria
not(Object o)
: Creates a criterion by using the!=
operator. -
Criteria
notIn(Object… o)
: Creates a criterion by using theNOT IN
operator for a varargs argument. -
Criteria
notIn(Collection<?> collection)
: Creates a criterion by using theNOT IN
operator using a collection.
You can use Criteria
with SELECT
, UPDATE
, and DELETE
queries.
Inserting Data
You can use the insert()
entry point to insert data.
Consider the following simple typed insert operation:
Mono<Person> insert = template.insert(Person.class) (1)
.using(new Person("John", "Doe")); (2)
1 | Using Person with the into(…) method sets the INTO table, based on mapping metadata.
It also prepares the insert statement to accept Person objects for inserting. |
2 | Provide a scalar Person object.
Alternatively, you can supply a Publisher to run a stream of INSERT statements.
This method extracts all non-null values and inserts them. |
Updating Data
You can use the update()
entry point to update rows.
Updating data starts by specifying the table to update by accepting Update
specifying assignments.
It also accepts Query
to create a WHERE
clause.
Consider the following simple typed update operation:
Person modified = …
Mono<Long> update = template.update(Person.class) (1)
.inTable("other_table") (2)
.matching(query(where("firstname").is("John"))) (3)
.apply(update("age", 42)); (4)
1 | Update Person objects and apply mapping based on mapping metadata. |
2 | Set a different table name by calling the inTable(…) method. |
3 | Specify a query that translates into a WHERE clause. |
4 | Apply the Update object.
Set in this case age to 42 and return the number of affected rows. |
Deleting Data
You can use the delete()
entry point to delete rows.
Removing data starts with a specification of the table to delete from and, optionally, accepts a Criteria
to create a WHERE
clause.
Consider the following simple insert operation:
Mono<Long> delete = template.delete(Person.class) (1)
.from("other_table") (2)
.matching(query(where("firstname").is("John"))) (3)
.all(); (4)
1 | Delete Person objects and apply mapping based on mapping metadata. |
2 | Set a different table name by calling the from(…) method. |
3 | Specify a query that translates into a WHERE clause. |
4 | Apply the delete operation and return the number of affected rows. |
Using Repositories, saving an entity can be performed with the ReactiveCrudRepository.save(…)
method.
If the entity is new, this results in an insert for the entity.
If the entity is not new, it gets updated. Note that whether an instance is new is part of the instance’s state.
This approach has some obvious downsides. If only few of the referenced entities have been actually changed, the deletion and insertion is wasteful. While this process could and probably will be improved, there are certain limitations to what Spring Data R2DBC can offer. It does not know the previous state of an aggregate. So any update process always has to take whatever it finds in the database and make sure it converts it to whatever is the state of the entity passed to the save method. |
ID Generation
Spring Data uses the identifer property to identify entities.
The ID of an entity must be annotated with Spring Data’s @Id
annotation.
When your database has an auto-increment column for the ID column, the generated value gets set in the entity after inserting it into the database.
Spring Data does not attempt to insert values of identifier columns when the entity is new and the identifier value defaults to its initial value.
That is 0
for primitive types and null
if the identifier property uses a numeric wrapper type such as Long
.
Entity State Detection explains in detail the strategies to detect whether an entity is new or whether it is expected to exist in your database.
One important constraint is that, after saving an entity, the entity must not be new anymore. Note that whether an entity is new is part of the entity’s state. With auto-increment columns, this happens automatically, because the ID gets set by Spring Data with the value from the ID column.
Optimistic Locking
Spring Data supports optimistic locking by means of a numeric attribute that is annotated with
@Version
on the aggregate root.
Whenever Spring Data saves an aggregate with such a version attribute two things happen:
-
The update statement for the aggregate root will contain a where clause checking that the version stored in the database is actually unchanged.
-
If this isn’t the case an
OptimisticLockingFailureException
will be thrown.
Also, the version attribute gets increased both in the entity and in the database so a concurrent action will notice the change and throw an OptimisticLockingFailureException
if applicable as described above.
This process also applies to inserting new aggregates, where a null
or 0
version indicates a new instance and the increased instance afterwards marks the instance as not new anymore, making this work rather nicely with cases where the id is generated during object construction for example when UUIDs are used.
During deletes the version check also applies but no version is increased.
@Table
class Person {
@Id Long id;
String firstname;
String lastname;
@Version Long version;
}
R2dbcEntityTemplate template = …;
Mono<Person> daenerys = template.insert(new Person("Daenerys")); (1)
Person other = template.select(Person.class)
.matching(query(where("id").is(daenerys.getId())))
.first().block(); (2)
daenerys.setLastname("Targaryen");
template.update(daenerys); (3)
template.update(other).subscribe(); // emits OptimisticLockingFailureException (4)
1 | Initially insert row. version is set to 0 . |
2 | Load the just inserted row. version is still 0 . |
3 | Update the row with version = 0 .Set the lastname and bump version to 1 . |
4 | Try to update the previously loaded row that still has version = 0 .The operation fails with an OptimisticLockingFailureException , as the current version is 1 . |