Spring Data JDBC通用DAO实现–迄今为止最轻量的ORM
我很高兴宣布Spring Data JDBC存储库项目的第一个版本。 这个开放源代码库的目的是为基于Spring框架中 JdbcTemplate
关系数据库提供通用,轻量且易于使用的DAO实现,与项目的Spring Data 框架兼容。
设计目标
- 轻巧,快速且开销低。 只有少数几个类, 没有XML,注释,反射
- 这不是成熟的ORM 。 没有关系处理,延迟加载,脏检查,缓存
- 在几秒钟内实现CRUD
- 对于JPA过大的小型应用程序
- 在需要简单性或考虑将来迁移到JPA时使用
- 对数据库方言差异的最小化支持(例如,透明的结果分页)
特征
每个DAO为以下内容提供内置支持:
- 通过
RowMapper
抽象到域对象/从域对象映射 - 生成的和用户定义的主键
- 提取生成的**
- 复合(多列)主键
- 不变的领域对象
- 分页(请求结果子集)
- 按几列排序(与数据库无关)
- 对多对一关系的可选支持
- 支持的数据库(连续测试):
- 的MySQL
- PostgreSQL的
- H2
- 数据库
- 德比
- …很可能是其他大多数
- 通过
SqlGenerator
类可以轻松扩展到其他数据库方言。 - 通过ID轻松检索记录
API
与Spring Data PagingAndSortingRepository
抽象兼容, 所有这些方法都为您实现 :
public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { T save(T entity); Iterable<T> save(Iterable<? extends T> entities); T findOne(ID id); boolean exists(ID id); Iterable<T> findAll(); long count(); void delete(ID id); void delete(T entity); void delete(Iterable<? extends T> entities); void deleteAll(); Iterable<T> findAll(Sort sort); Page<T> findAll(Pageable pageable); }
还完全支持Pageable
和Sort
参数,这意味着您可以通过任意属性免费获得分页和排序 。 例如,假设您有userRepository
扩展了PagingAndSortingRepository<User, String>
接口(由库为您实现),并且在应用某种排序后,您请求了USERS
表的第5页,每页10个:
Page<User> page = userRepository.findAll( new PageRequest( 5, 10, new Sort( new Order(DESC, "reputation"), new Order(ASC, "user_name") ) ) );
Spring Data JDBC存储库库会将此调用转换为(PostgreSQL语法):
SELECT * FROM USERS ORDER BY reputation DESC, user_name ASC LIMIT 50 OFFSET 10
…甚至(Derby语法):
SELECT * FROM ( SELECT ROW_NUMBER() OVER () AS ROW_NUM, t.* FROM ( SELECT * FROM USERS ORDER BY reputation DESC, user_name ASC ) AS t ) AS a WHERE ROW_NUM BETWEEN 51 AND 60
无论使用哪个数据库,都将获得Page<User>
对象作为回报(您仍然必须自己提供RowMapper<User>
才能将其从ResultSet
转换为域对象。如果您还不了解Spring Data项目,则Page<T>
是一个很棒的抽象,不仅封装了List<User>
,而且还提供了元数据,例如记录总数,我们当前所在的页面等。
使用理由
- 由于将来您的代码将仅依赖于Spring Data Commons伞项目中的
PagingAndSortingRepository
和CrudRepository
定义的方法,PagingAndSortingRepository
您可以自由地从JdbcRepository
实现(从该项目)切换到:JpaRepository
,MongoRepository
,GemfireRepository
或GraphRepository
。 它们都实现相同的通用API。 当然,不要指望从JDBC切换到JPA或MongoDB就像切换导入的JAR依赖项一样简单-但是至少您可以通过使用相同的DAO API最小化影响。 - 您需要一个快速,简单的JDBC包装器库。 JPA甚至MyBatis都不过分
- 如果需要,您想完全控制生成的SQL
- 您想使用对象,但是不需要延迟加载,关系处理,多级缓存,脏检查……您需要CRUD等等
- 您想干吗
- 您已经在使用Spring甚至
JdbcTemplate
,但仍然觉得手工工作过多 - 您的数据库表很少
入门
有关更多示例和工作代码,请不要忘记检查项目测试 。
先决条件
Maven坐标:
<dependency> <groupId>com.blogspot.nurkiewicz</groupId> <artifactId>jdbcrepository</artifactId> <version>0.1</version> </dependency>
不幸的是,该项目尚未在Maven中央存储库中 。 目前,您可以通过克隆将库安装在本地存储库中:
$ git clone git://github.com/nurkiewicz/spring-data-jdbc-repository.git $ git checkout 0.1 $ mvn javadoc:jar source:jar install
为了启动您的项目,必须存在DataSource
bean并启用事务管理。 这是最小的MySQL配置:
@EnableTransactionManagement @Configuration public class MinimalConfig { @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } @Bean public DataSource dataSource() { MysqlConnectionPoolDataSource ds = new MysqlConnectionPoolDataSource(); ds.setUser("user"); ds.setPassword("secret"); ds.setDatabaseName("db_name"); return ds; } }
具有自动生成的**的实体
假设您有一个具有自动生成的**(MySQL语法)的数据库表:
CREATE TABLE COMMENTS ( id INT AUTO_INCREMENT, user_name varchar(256), contents varchar(1000), created_time TIMESTAMP NOT NULL, PRIMARY KEY (id) );
首先,您需要创建到该表的域对象User
映射(就像在任何其他ORM中一样):
public class Comment implements Persistable<Integer> { private Integer id; private String userName; private String contents; private Date createdTime; @Override public Integer getId() { return id; } @Override public boolean isNew() { return id == null; } //getters/setters/constructors/... }
除了标准的Java样板之外,您还应该注意实现Persistable<Integer>
,其中Integer
是主键的类型。 Persistable<T>
是一个来自Spring Data项目的接口,这是我们对您的域对象的唯一要求。
最后,我们准备创建CommentRepository
DAO:
@Repository public class CommentRepository extends JdbcRepository<Comment, Integer> { public CommentRepository() { super(ROW_MAPPER, ROW_UNMAPPER, "COMMENTS"); } public static final RowMapper<Comment> ROW_MAPPER = //see below private static final RowUnmapper<Comment> ROW_UNMAPPER = //see below @Override protected Comment postCreate(Comment entity, Number generatedId) { entity.setId(generatedId.intValue()); return entity; } }
首先,我们使用@Repository
批注标记DAO bean。 它启用持久性异常转换。 通过CLASSPATH扫描也可以发现此类带注释的bean。
如您所见,我们扩展了JdbcRepository<Comment, Integer>
,它是该库的中心类,提供了所有PagingAndSortingRepository
方法的实现。 它的构造函数具有三个必需的依赖项: RowMapper
, RowUnmapper
和表名。 您也可以提供ID列名,否则使用默认的"id"
。
如果您曾经使用过Spring的JdbcTemplate
,则应该熟悉RowMapper
界面。 我们需要以某种方式将ResultSet
列提取到一个对象中。 毕竟,我们不想使用原始的JDBC结果。 这很简单:
public static final RowMapper<Comment> ROW_MAPPER = new RowMapper<Comment>() { @Override public Comment mapRow(ResultSet rs, int rowNum) throws SQLException { return new Comment( rs.getInt("id"), rs.getString("user_name"), rs.getString("contents"), rs.getTimestamp("created_time") ); } };
RowUnmapper
来自此库,它本质上与RowMapper
相反:接收一个对象并将其转换为Map
。 库稍后使用此映射来构造SQL CREATE
/ UPDATE
查询:
private static final RowUnmapper<Comment> ROW_UNMAPPER = new RowUnmapper<Comment>() { @Override public Map<String, Object> mapColumns(Comment comment) { Map<String, Object> mapping = new LinkedHashMap<String, Object>(); mapping.put("id", comment.getId()); mapping.put("user_name", comment.getUserName()); mapping.put("contents", comment.getContents()); mapping.put("created_time", new java.sql.Timestamp(comment.getCreatedTime().getTime())); return mapping; } };
如果您从不更新数据库表(仅读取插入在其他位置的一些参考数据),则可以跳过RowUnmapper
参数或使用MissingRowUnmapper
。
最后一个难题是postCreate()
回调方法,该方法在插入对象后调用。 您可以使用它来检索生成的主键并更新域对象(如果域对象是不可变的,则返回新的主键)。 如果不需要它,就不要重写postCreate()
。 根据此示例,检查JdbcRepositoryGeneratedKeyTest
以获取有效的代码。
到目前为止,您可能会觉得与JPA或Hibernate相比,有很多手工工作。 但是,众所周知,各种JPA实现和其他ORM框架都会引入大量开销并显示一些学习曲线。 这个微小的库有意让用户承担一些责任,以避免复杂的映射,反射,注释……并非总是需要的所有隐式性。 该项目无意替代成熟稳定的ORM框架。 相反,它试图填补原始JDBC和ORM之间的利基,其中简单性和低开销是关键特征。
具有手动分配的**的实体
在此示例中,我们将看到如何处理具有用户定义的主键的实体。 让我们从数据库模型开始:
CREATE TABLE USERS ( user_name varchar(255), date_of_birth TIMESTAMP NOT NULL, enabled BIT(1) NOT NULL, PRIMARY KEY (user_name) );
…和User
域模型:
public class User implements Persistable<String> { private transient boolean persisted; private String userName; private Date dateOfBirth; private boolean enabled; @Override public String getId() { return userName; } @Override public boolean isNew() { return !persisted; } public User withPersisted(boolean persisted) { this.persisted = persisted; return this; } //getters/setters/constructors/... }
注意,添加了特殊的persisted
瞬态标志。 来自Spring Data项目的CrudRepository.save()
合同要求一个实体知道它是否已经保存( isNew()
)方法–没有单独的create()
和update()
方法。 对于自动生成的键,实现isNew()
很简单(请参见上面的Comment
),但是在这种情况下,我们需要一个额外的瞬态字段。 如果您讨厌这种解决方法,并且只插入数据而从不更新,则始终可以从isNew()
返回true
。
最后是我们的DAO, UserRepository
bean:
@Repository public class UserRepository extends JdbcRepository<User, String> { public UserRepository() { super(ROW_MAPPER, ROW_UNMAPPER, "USERS", "user_name"); } public static final RowMapper<User> ROW_MAPPER = //... public static final RowUnmapper<User> ROW_UNMAPPER = //... @Override protected User postUpdate(User entity) { return entity.withPersisted(true); } @Override protected User postCreate(User entity, Number generatedId) { return entity.withPersisted(true); } }
"USERS"
和"user_name"
参数指定表名称和主键列名称。 我将保留mapper和unmapper的详细信息(请参阅源代码 )。 但是请注意postUpdate()
和postCreate()
方法。 它们确保一旦对象被持久保存,就设置了persisted
标志,以便随后对save()
调用将更新现有实体,而不是尝试重新插入它。
根据此示例,检查JdbcRepositoryManualKeyTest
以获得有效的代码。
复合主键
我们还支持复合主键(由几列组成的主键)。 以该表为例:
CREATE TABLE BOARDING_PASS ( flight_no VARCHAR(8) NOT NULL, seq_no INT NOT NULL, passenger VARCHAR(1000), seat CHAR(3), PRIMARY KEY (flight_no, seq_no) );
我希望您注意到Peristable<T>
的主键类型:
public class BoardingPass implements Persistable<Object[]> { private transient boolean persisted; private String flightNo; private int seqNo; private String passenger; private String seat; @Override public Object[] getId() { return pk(flightNo, seqNo); } @Override public boolean isNew() { return !persisted; } //getters/setters/constructors/... }
不幸的是,我们不支持将所有ID值封装在一个对象中的小数值类(就像JPA使用@IdClass
),因此您必须使用Object[]
数组。 定义DAO类类似于我们已经看到的内容:
public class BoardingPassRepository extends JdbcRepository<BoardingPass, Object[]> { public BoardingPassRepository() { this("BOARDING_PASS"); } public BoardingPassRepository(String tableName) { super(MAPPER, UNMAPPER, new TableDescription(tableName, null, "flight_no", "seq_no") ); } public static final RowMapper<BoardingPass> ROW_MAPPER = //... public static final RowUnmapper<BoardingPass> UNMAPPER = //... }
需要注意的两件事:我们扩展了JdbcRepository<BoardingPass, Object[]>
并且按预期提供了两个ID列名称: "flight_no", "seq_no"
。 我们通过提供由Object[]
包裹的flight_no
和seq_no
(必须seq_no
顺序)值来查询此类DAO:
BoardingPass pass = repository.findOne(new Object[] {"FOO-1022", 42});
毫无疑问,这在实践中很麻烦,因此我们提供了微小的辅助方法,您可以静态导入:
import static com.blogspot.nurkiewicz.jdbcrepository.JdbcRepository.pk; //... BoardingPass foundFlight = repository.findOne(pk("FOO-1022", 42));
根据此示例,检查JdbcRepositoryCompoundPkTest
以获取工作代码。
交易次数
该库与事务管理完全正交。 每个存储库的每种方法都需要运行事务,具体取决于您进行设置。 通常,您将@Transactional
放在服务层上(称为DAO bean)。 我不建议将@Transactional
放在每个DAO bean上 。
快取
Spring Data JDBC存储库库不提供任何缓存抽象或支持。 但是, 在Spring中使用缓存抽象将@Cacheable
层添加@Cacheable
DAO或服务之上非常简单。 另请参见: Spring中的@Cacheable开销 。
会费
..总是欢迎。 不要犹豫, 提交错误报告并提出请求 。 现在最大的缺失功能是对MSSQL和Oracle数据库的支持。 如果有人可以看一下,那就太好了。
测试中
该库已使用Travis( )。 测试套件包括265个测试 (53个不同的测试,每个测试针对5个不同的数据库运行:MySQL,PostgreSQL,H2,HSQLDB和Derby。
在填写错误报告或提交新功能时,请尝试包括支持测试用例。 每个拉取请求都会在单独的分支上自动进行测试。
建造
分叉后, 正式的存储库构建就像运行一样简单:
$ mvn install
您将在JUnit测试执行过程中注意到大量异常。 这个是正常的。 一些测试是针对仅在Travis CI服务器上可用的MySQL和PostgreSQL运行的。 当这些数据库服务器不可用时,只需跳过整个测试:
结果:
Tests run: 265, Failures: 0, Errors: 0, Skipped: 106
异常堆栈跟踪来自根AbstractIntegrationTest
。
设计
库仅包含少数几个类,如下图所示:
JdbcRepository
是实现所有PagingAndSortingRepository
方法的最重要的类。 每个用户存储库都必须扩展此类。 同样,每个这样的存储库必须至少实现RowMapper
和RowUnmapper
(仅当您要修改表数据时)。
SQL生成委托给SqlGenerator
。 PostgreSqlGenerator.
和DerbySqlGenerator
用于与标准生成器不DerbySqlGenerator
的数据库。
执照
该项目是在Apache许可的 2.0版下发布的 (与Spring框架相同)。
参考: NoBlogDefFound博客中JCG合作伙伴 Tomasz Nurkiewicz 为程序员提供的概率分布 。