|
| 1 | +image:https://spring.io/badges/spring-data-jdbc/ga.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] |
| 2 | +image:https://spring.io/badges/spring-data-jdbc/snapshot.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] |
| 3 | + |
1 | 4 | = Spring Data JDBC
|
2 | 5 |
|
3 | 6 | The primary goal of the http://projects.spring.io/spring-data[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.
|
4 | 7 |
|
5 |
| -== This is NOT an ORM |
6 |
| - |
7 |
| -Spring Data JDBC does not try to be an ORM. It is not a competitor to JPA. |
8 |
| -Instead it is more of a construction kit for your personal ORM that you can define the way you like or need it. |
9 |
| - |
10 |
| -This means that it does rather little out of the box. |
11 |
| -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. |
12 |
| - |
13 |
| -== The Aggregate Root |
14 |
| - |
15 |
| -Spring Data repositories are inspired by the repository as described in the book Domain Driven Design by Eric Evans. |
16 |
| -One consequence of this is that you should have a repository per Aggregate Root. |
17 |
| -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. |
18 |
| -An Aggregate is a subset of your model which is consistent between method calls to your Aggregate Root. |
19 |
| - |
20 |
| -Spring Data JDBC tries its best to encourage modelling your domain along these ideas. |
21 |
| - |
22 |
| -== Maven Coordinates |
23 |
| - |
24 |
| -[source,xml] |
25 |
| ----- |
26 |
| -<dependency> |
27 |
| - <groupId>org.springframework.data</groupId> |
28 |
| - <artifactId>spring-data-jdbc</artifactId> |
29 |
| - <version>1.0.0.BUILD-SNAPSHOT</version> |
30 |
| -</dependency> |
31 |
| ----- |
| 8 | +It aims at being conceptually easy. |
| 9 | +In order to achieve this it does NOT offer caching, lazy loading, write behind or many other features of JPA. |
| 10 | +This makes Spring Data JDBC a simple, limited, opinionated ORM. |
32 | 11 |
|
33 | 12 | == Features
|
34 | 13 |
|
35 |
| -=== CRUD operations |
36 |
| - |
37 |
| -In order to use Spring Data JDBC you need the following: |
38 |
| - |
39 |
| -1. An entity with an attribute marked as _id_ using the Spring Data https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/annotation/Id.html[`@Id`] annotation. |
40 |
| -+ |
41 |
| -[source,java] |
42 |
| ----- |
43 |
| - public class Person { |
44 |
| - @Id |
45 |
| - Integer id; |
46 |
| - } |
47 |
| ----- |
48 |
| -+ |
49 |
| -1. A repository |
50 |
| -+ |
51 |
| -[source,java] |
52 |
| ----- |
53 |
| -public interface PersonRepository extends CrudRepository<Person, Integer> {} |
54 |
| ----- |
55 |
| -+ |
56 |
| -1. Add `@EnableJdbcRepositories` to your application context configuration. |
57 |
| -1. Make sure your application context contains a bean of type `DataSource`. |
58 |
| - |
59 |
| -Now you can get an instance of the repository interface injected into your beans and use it: |
60 |
| - |
61 |
| -[source,java] |
62 |
| ----- |
63 |
| -@Autowired |
64 |
| -private PersonRepository repository; |
65 |
| -
|
66 |
| -public void someMethod() { |
67 |
| - Person person = repository.save(new Person()); |
68 |
| -} |
69 |
| ----- |
70 |
| - |
71 |
| -==== Supported types in your entity |
72 |
| - |
73 |
| -Properties of the following types are currently supported: |
74 |
| - |
75 |
| -* all primitive types and their boxed types (`int`, `float`, `Integer`, `Float` ...) |
76 |
| - |
77 |
| -* enums get mapped to their name. |
78 |
| - |
79 |
| -* `String` |
80 |
| - |
81 |
| -* `java.util.Date`, `java.time.LocalDate`, `java.time.LocalDateTime`, `java.time.LocalTime` |
82 |
| - |
83 |
| -and anything your database driver accepts. |
84 |
| - |
85 |
| -* references to other entities, which will be considered a one-to-one relationship. |
86 |
| -The table of the referenced entity is expected to have an additional column named like the table of the referencing entity. |
87 |
| -This name can be changed by implementing `NamingStrategy.getReverseColumnName(JdbcPersistentProperty property)` according to your preferences. |
88 |
| - |
89 |
| -* `Set<some entity>` will be considered a one-to-many relationship. |
90 |
| -The table of the referenced entity is expected to have an additional column named like the table of the referencing entity. |
91 |
| -This name can be changed by implementing `NamingStrategy.getReverseColumnName(JdbcPersistentProperty property)` according to your preferences. |
92 |
| - |
93 |
| -* `Map<simple type, some entity>` will be considered a qualified one-to-many relationship. |
94 |
| -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. |
95 |
| -This name can be changed by implementing `NamingStrategy.getReverseColumnName(JdbcPersistentProperty property)` and `NamingStrategy.getKeyColumn(JdbcPersistentProperty property)` according to your preferences. |
96 |
| - |
97 |
| -* `List<some entity>` will be mapped like a `Map<Integer, some entity>`. |
98 |
| - |
99 |
| -The handling of referenced entities is very limited. |
100 |
| -Part of this is because this project is still before its first release. |
101 |
| - |
102 |
| -But another reason is the idea of <<The Aggregate Root,Aggregate Roots>> as described above. |
103 |
| -If you reference another entity that entity is by definition part of your Aggregate. |
104 |
| -So if you remove the reference it will get deleted. |
105 |
| -This also means references will be 1-1 or 1-n, but not n-1 or n-m. |
106 |
| - |
107 |
| -If you are having n-1 or n-m references you are probably dealing with two separate Aggregates. |
108 |
| -References between those should be encoded as simple ids, which should map just fine with Spring Data JDBC. |
109 |
| - |
110 |
| -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. |
111 |
| -We will offer ways to plug in your own SQL in various ways. |
112 |
| -But the default mapping itself will stay limited. |
113 |
| -If you want highly customizable mappings which support almost everything one can imagine you will probably be much happier with (Spring Data) JPA, |
114 |
| -which is a very powerful and mature technology. |
115 |
| - |
116 |
| -=== Query annotation |
117 |
| - |
118 |
| -You can annotate a query method with `@Query` to specify a SQL statement to be used for that method. |
119 |
| -You can bind method arguments using named parameters in the SQL statement like in the following example: |
120 |
| - |
121 |
| -[source,java] |
122 |
| ----- |
123 |
| -@Query("SELECT * FROM DUMMYENTITY WHERE name < :upper and name > :lower") |
124 |
| -List<DummyEntity> findByNameRange(@Param("lower") String lower, @Param("upper") String upper); |
125 |
| ----- |
126 |
| - |
127 |
| -If you compile your sources with the `-parameters` compiler flag you can omit the `@Param` annotations. |
128 |
| - |
129 |
| -==== Custom RowMapper |
130 |
| - |
131 |
| -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. |
132 |
| - |
133 |
| -[source,java] |
134 |
| ----- |
135 |
| -
|
136 |
| -@Bean |
137 |
| -RowMapperMap rowMappers() { |
138 |
| - return new ConfigurableRowMapperMap() // |
139 |
| - .register(Person.class, new PersonRowMapper()) // |
140 |
| - .register(Address.class, new AddressRowMapper()); |
141 |
| -} |
142 |
| -
|
143 |
| ----- |
144 |
| - |
145 |
| -When determining the `RowMapper` to use for a method the following steps are followed based on the return type of the method: |
146 |
| - |
147 |
| -1. If the type is a simple type, no `RowMapper` is used. |
148 |
| - 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. |
149 |
| - |
150 |
| -2. The entity classes in the `RowMapperMap` are iterated until one is found that is a superclass or interface of the return type in question. |
151 |
| - The `RowMapper` registered for that class is used. |
152 |
| - Iterating happens in the order of registration, so make sure to register more general types after specific ones. |
153 |
| - |
154 |
| -If applicable, wrapper types like collections or `Optional` are unwrapped. |
155 |
| -Thus, a return type of `Optional<Person>` will use the type `Person` in the steps above. |
156 |
| - |
157 |
| -==== Modifying query |
158 |
| - |
159 |
| -You can mark as a modifying query using the `@Modifying` on query method. |
160 |
| - |
161 |
| -[source,java] |
162 |
| ----- |
163 |
| -@Modifying |
164 |
| -@Query("UPDATE DUMMYENTITY SET name = :name WHERE id = :id") |
165 |
| -boolean updateName(@Param("id") Long id, @Param("name") String name); |
166 |
| ----- |
167 |
| - |
168 |
| -The return types that can be specified are `void`, `int`(updated record count) and `boolean`(whether record was updated). |
169 |
| - |
170 |
| -=== Id generation |
171 |
| - |
172 |
| -Spring Data JDBC uses the id to identify entities, but also to determine if an entity is new or already existing in the database. |
173 |
| -If the id is `null` or of a primitive type having value `0` or `0.0`, the entity is considered new. |
174 |
| - |
175 |
| -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. |
176 |
| - |
177 |
| -There are few ways to tweak this behavior. |
178 |
| -If you don't like the logic to distinguish between new and existing entities you can implement https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Persistable.html[`Persistable`] with your entity and overwrite `isNew()` with your own logic. |
| 14 | +* Implementation of CRUD methods for Aggregates. |
| 15 | +* `@Query` annotation |
| 16 | +* Support for transparent auditing (created, last changed) |
| 17 | +* Events for persistence events |
| 18 | +* Possibility to integrate custom repository code |
| 19 | +* JavaConfig based repository configuration by introducing `EnableJdbcRepository` |
| 20 | +* Integration with MyBatis |
179 | 21 |
|
180 |
| -One important constraint is that after saving an entity the entity shouldn't be _new_ anymore. |
181 |
| -With autoincrement-columns this happens automatically since the id gets set by Spring Data with the value from the id-column. |
182 |
| -If you are not using autoincrement-columns, you can use a `BeforeSave`-listener which sets the id of the entity (see below). |
183 |
| - |
184 |
| -=== NamingStrategy |
185 |
| - |
186 |
| -If you use the standard implementations of `CrudRepository` as provided by Spring Data JDBC, it will expect a certain table structure. |
187 |
| -You can tweak that by providing a https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/model/NamingStrategy.java[`NamingStrategy`] in your application context. |
188 |
| - |
189 |
| -In many cases a https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/model/DelimiterNamingStrategy.java[`DelimiterNamingStrategy`] |
190 |
| -might be good basis for a custom implementation |
191 |
| - |
192 |
| -=== Events |
193 |
| - |
194 |
| -Spring Data JDBC triggers events which will get published to any matching `ApplicationListener` in the application context. |
195 |
| -For example, the following listener will get invoked before an Aggregate gets saved. |
196 |
| - |
197 |
| -[source,java] |
198 |
| ----- |
199 |
| -@Bean |
200 |
| -public ApplicationListener<BeforeSaveEvent> timeStampingSaveTime() { |
201 |
| -
|
202 |
| - return event -> { |
203 |
| -
|
204 |
| - Object entity = event.getEntity(); |
205 |
| - if (entity instanceof Category) { |
206 |
| - Category category = (Category) entity; |
207 |
| - category.timeStamp(); |
208 |
| - } |
209 |
| - }; |
210 |
| -} |
211 |
| ----- |
212 |
| - |
213 |
| -.Available events |
214 |
| -|=== |
215 |
| -| Event | When It's Published |
216 |
| - |
217 |
| -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDeleteEvent.java[`BeforeDeleteEvent`] |
218 |
| -| before an aggregate root gets deleted. |
219 |
| - |
220 |
| -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDeleteEvent.java[`AfterDeleteEvent`] |
221 |
| -| after an aggregate root got deleted. |
222 |
| - |
223 |
| -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSaveEvent.java[`BeforeSaveEvent`] |
224 |
| -| before an aggregate root gets saved, i.e. inserted or updated but after the decision was made if it will get updated or deleted. |
225 |
| -The event has a reference to an https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/core/conversion/AggregateChange.java[`AggregateChange`] instance. |
226 |
| -The instance can be modified by adding or removing https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/core/conversion/DbAction.java[`DbAction`]s. |
227 |
| - |
228 |
| -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSaveEvent.java[`AfterSaveEvent`] |
229 |
| -| after an aggregate root gets saved, i.e. inserted or updated. |
230 |
| - |
231 |
| -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/AfterLoadEvent.java[`AfterLoadEvent`] |
232 |
| -| after an aggregate root got created from a database `ResultSet` and all it's property set |
233 |
| -|=== |
234 |
| - |
235 |
| - |
236 |
| -=== MyBatis |
237 |
| - |
238 |
| -For each operation in `CrudRepository` Spring Data JDBC will execute multiple statements. |
239 |
| -If there is a https://github.com/mybatis/mybatis-3/blob/master/src/main/java/org/apache/ibatis/session/SqlSessionFactory.java[`SqlSessionFactory`] in the application context, it will be checked if it offers a statement for each step. |
240 |
| -If one is found, that statement will be used (including its configured mapping to an entity). |
241 |
| - |
242 |
| -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. |
243 |
| -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`. |
244 |
| - |
245 |
| -Upon execution of the statement an instance of [`MyBatisContext`] will get passed as an argument which makes various arguments available to the statement. |
246 |
| - |
247 |
| -[cols="default,default,default,asciidoc"] |
248 |
| -|=== |
249 |
| -| Name | Purpose | CrudRepository methods which might trigger this statement | Attributes available in the `MyBatisContext` |
250 |
| - |
251 |
| -| `insert` | Insert for a single entity. This also applies for entities referenced by the aggregate root. | `save`, `saveAll`. | |
252 |
| -`getInstance`: |
253 |
| - the instance to be saved |
254 |
| - |
255 |
| -`getDomainType`: the type of the entity to be saved. |
256 |
| - |
257 |
| -`get(<key>)`: id of the referencing entity, where `<key>` is the name of the back reference column as provided by the `NamingStrategy`. |
258 |
| - |
259 |
| - |
260 |
| -| `update` | Update for a single entity. This also applies for entities referenced by the aggregate root. | `save`, `saveAll`.| |
261 |
| -`getInstance`: the instance to be saved |
262 |
| - |
263 |
| -`getDomainType`: the type of the entity to be saved. |
264 |
| - |
265 |
| -| `delete` | Delete a single entity. | `delete`, `deleteById`.| |
266 |
| -`getId`: the id of the instance to be deleted |
267 |
| - |
268 |
| -`getDomainType`: the type of the entity to be deleted. |
269 |
| - |
270 |
| -| `deleteAll.<propertyPath>` | Delete all entities referenced by any aggregate root of the type used as prefix via the given property path. |
271 |
| -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. | `deleteAll`.| |
272 |
| - |
273 |
| -`getDomainType`: the type of the entities to be deleted. |
274 |
| - |
275 |
| -| `deleteAll` | Delete all aggregate roots of the type used as the prefix | `deleteAll`.| |
276 |
| - |
277 |
| -`getDomainType`: the type of the entities to be deleted. |
278 |
| - |
279 |
| -| `delete.<propertyPath>` | Delete all entities referenced by an aggregate root via the given propertyPath | `deleteById`.| |
280 |
| - |
281 |
| -`getId`: the id of the aggregate root for which referenced entities are to be deleted. |
282 |
| - |
283 |
| -`getDomainType`: the type of the entities to be deleted. |
284 |
| - |
285 |
| - |
286 |
| -| `findById` | Select an aggregate root by id | `findById`.| |
287 |
| - |
288 |
| -`getId`: the id of the entity to load. |
289 |
| - |
290 |
| -`getDomainType`: the type of the entity to load. |
291 |
| - |
292 |
| -| `findAll` | Select all aggregate roots | `findAll`.| |
293 |
| - |
294 |
| -`getDomainType`: the type of the entity to load. |
295 |
| - |
296 |
| -| `findAllById` | Select a set of aggregate roots by ids | `findAllById`.| |
297 |
| - |
298 |
| -`getId`: list of ids of the entities to load. |
299 |
| - |
300 |
| -`getDomainType`: the type of the entity to load. |
301 |
| - |
302 |
| - |
303 |
| -| `findAllByProperty.<propertyName>` | 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 `find*` methods.| |
304 |
| - |
305 |
| -`getId`: the id of the entity referencing the entities to be loaded. |
306 |
| - |
307 |
| -`getDomainType`: the type of the entity to load. |
308 |
| - |
309 |
| -| `count` | Count the number of aggregate root of the type used as prefix | `count` | |
310 |
| - |
311 |
| -`getDomainType` the type of aggregate roots to count. |
312 |
| -|=== |
313 |
| - |
314 |
| -==== NamespaceStrategy |
315 |
| - |
316 |
| -You can customize the namespace part of a statement name using https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mybatis/NamespaceStrategy.java[`NamespaceStrategy`]. |
317 |
| - |
318 |
| -== Features planned for the not too distant future |
319 |
| - |
320 |
| -=== Advanced query annotation support |
321 |
| - |
322 |
| -* projections |
323 |
| -* SpEL expressions |
324 |
| - |
325 |
| -=== MyBatis per method support |
326 |
| - |
327 |
| -The current MyBatis supported is rather elaborate in that it allows to execute multiple statements for a single method call. |
328 |
| -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. |
329 |
| - |
330 |
| -== Spring Boot integration |
331 |
| - |
332 |
| -There is https://github.com/schauder/spring-data-jdbc-boot-starter[preliminary Spring Boot integration]. |
| 22 | +== Getting Help |
333 | 23 |
|
334 |
| -Currently you will need to build it locally. |
| 24 | +If you are new to Spring Data JDBC read the following two articles https://spring.io/blog/2018/09/17/introducing-spring-data-jdbc["Introducing Spring Data JDBC"] and https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates["Spring Data JDBC, References, and Aggregates"] |
335 | 25 |
|
336 |
| -== Getting Help |
| 26 | +There are also examples in the https://github.com/spring-projects/spring-data-examples/tree/master/jdbc[Spring Data Examples] project. |
337 | 27 |
|
338 |
| -Right now the best source of information is the source code in this repository. |
| 28 | +A very good source of information is the source code in this repository. |
339 | 29 | Especially the integration tests (if you are reading this on github, type `t` and then `IntegrationTests.java`)
|
340 | 30 |
|
341 | 31 | We are keeping an eye on the (soon to be created) https://stackoverflow.com/questions/tagged/spring-data-jdbc[spring-data-jdbc tag on stackoverflow].
|
|
0 commit comments