diff --git a/README.adoc b/README.adoc index f479fbc6a..9d18017f5 100644 --- a/README.adoc +++ b/README.adoc @@ -1,341 +1,31 @@ +image:https://spring.io/badges/spring-data-jdbc/ga.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] +image:https://spring.io/badges/spring-data-jdbc/snapshot.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] + = Spring Data JDBC 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. -== This is NOT an ORM - -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. - -== The Aggregate Root - -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. - -== Maven Coordinates - -[source,xml] ----- - - org.springframework.data - spring-data-jdbc - 1.0.0.BUILD-SNAPSHOT - ----- +It aims at being conceptually easy. +In order to achieve this it does NOT offer caching, lazy loading, write behind or many other features of JPA. +This makes Spring Data JDBC a simple, limited, opinionated ORM. == Features -=== CRUD operations - -In order to use Spring Data JDBC you need the following: - -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. -+ -[source,java] ----- - public class Person { - @Id - Integer id; - } ----- -+ -1. A repository -+ -[source,java] ----- -public interface PersonRepository extends CrudRepository {} ----- -+ -1. Add `@EnableJdbcRepositories` to your application context configuration. -1. 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: - -[source,java] ----- -@Autowired -private PersonRepository repository; - -public void someMethod() { - Person person = repository.save(new Person()); -} ----- - -==== Supported types in your entity - -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` 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 implementing `NamingStrategy.getReverseColumnName(JdbcPersistentProperty property)` according to your preferences. - -* `Map` 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 implementing `NamingStrategy.getReverseColumnName(JdbcPersistentProperty property)` and `NamingStrategy.getKeyColumn(JdbcPersistentProperty property)` according to your preferences. - -* `List` will be mapped like a `Map`. - -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 <> 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. - -=== Query annotation - -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: - -[source,java] ----- -@Query("SELECT * FROM DUMMYENTITY WHERE name < :upper and name > :lower") -List 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. - -==== Custom RowMapper - -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. - -[source,java] ----- - -@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: - -1. 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. - -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. - The `RowMapper` 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` will use the type `Person` in the steps above. - -==== Modifying query - -You can mark as a modifying query using the `@Modifying` on query method. - -[source,java] ----- -@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). - -=== Id generation - -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 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. +* Implementation of CRUD methods for Aggregates. +* `@Query` annotation +* Support for transparent auditing (created, last changed) +* Events for persistence events +* Possibility to integrate custom repository code +* JavaConfig based repository configuration by introducing `EnableJdbcRepository` +* Integration with MyBatis -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). - -=== NamingStrategy - -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 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. - -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`] -might be good basis for a custom implementation - -=== Events - -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. - -[source,java] ----- -@Bean -public ApplicationListener timeStampingSaveTime() { - - return event -> { - - Object entity = event.getEntity(); - if (entity instanceof Category) { - Category category = (Category) entity; - category.timeStamp(); - } - }; -} ----- - -.Available events -|=== -| Event | When It's Published - -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDeleteEvent.java[`BeforeDeleteEvent`] -| before an aggregate root gets deleted. - -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDeleteEvent.java[`AfterDeleteEvent`] -| after an aggregate root got deleted. - -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSaveEvent.java[`BeforeSaveEvent`] -| 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 https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/core/conversion/AggregateChange.java[`AggregateChange`] instance. -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. - -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSaveEvent.java[`AfterSaveEvent`] -| after an aggregate root gets saved, i.e. inserted or updated. - -| https://github.com/spring-projects/spring-data-jdbc/blob/master/src/main/java/org/springframework/data/jdbc/mapping/event/AfterLoadEvent.java[`AfterLoadEvent`] -| after an aggregate root got created from a database `ResultSet` and all it's property set -|=== - - -=== MyBatis - -For each operation in `CrudRepository` Spring Data JDBC will execute multiple statements. -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. -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. - -[cols="default,default,default,asciidoc"] -|=== -| Name | Purpose | CrudRepository methods which might trigger this statement | Attributes available in the `MyBatisContext` - -| `insert` | Insert for a single entity. This also applies for entities referenced by the aggregate root. | `save`, `saveAll`. | -`getInstance`: - the instance to be saved - -`getDomainType`: the type of the entity to be saved. - -`get()`: id of the referencing entity, where `` is the name of the back reference column as provided by the `NamingStrategy`. - - -| `update` | Update for a single entity. This also applies for entities referenced by the aggregate root. | `save`, `saveAll`.| -`getInstance`: the instance to be saved - -`getDomainType`: the type of the entity to be saved. - -| `delete` | Delete a single entity. | `delete`, `deleteById`.| -`getId`: the id of the instance to be deleted - -`getDomainType`: the type of the entity to be deleted. - -| `deleteAll.` | 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. | `deleteAll`.| - -`getDomainType`: the type of the entities to be deleted. - -| `deleteAll` | Delete all aggregate roots of the type used as the prefix | `deleteAll`.| - -`getDomainType`: the type of the entities to be deleted. - -| `delete.` | Delete all entities referenced by an aggregate root via the given propertyPath | `deleteById`.| - -`getId`: the id of the aggregate root for which referenced entities are to be deleted. - -`getDomainType`: the type of the entities to be deleted. - - -| `findById` | Select an aggregate root by id | `findById`.| - -`getId`: the id of the entity to load. - -`getDomainType`: the type of the entity to load. - -| `findAll` | Select all aggregate roots | `findAll`.| - -`getDomainType`: the type of the entity to load. - -| `findAllById` | Select a set of aggregate roots by ids | `findAllById`.| - -`getId`: list of ids of the entities to load. - -`getDomainType`: the type of the entity to load. - - -| `findAllByProperty.` | 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.| - -`getId`: the id of the entity referencing the entities to be loaded. - -`getDomainType`: the type of the entity to load. - -| `count` | Count the number of aggregate root of the type used as prefix | `count` | - -`getDomainType` the type of aggregate roots to count. -|=== - -==== NamespaceStrategy - -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`]. - -== Features planned for the not too distant future - -=== Advanced query annotation support - -* projections -* SpEL expressions - -=== MyBatis per method support - -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. - -== Spring Boot integration - -There is https://github.com/schauder/spring-data-jdbc-boot-starter[preliminary Spring Boot integration]. +== Getting Help -Currently you will need to build it locally. +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"] -== Getting Help +There are also examples in the https://github.com/spring-projects/spring-data-examples/tree/master/jdbc[Spring Data Examples] project. -Right now the best source of information is the source code in this repository. +A very good 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) https://stackoverflow.com/questions/tagged/spring-data-jdbc[spring-data-jdbc tag on stackoverflow].