Active Record 是什么?也许很多做 Java 的朋友并没有听说过这个概念,但它确实很早就已经出现了。
确切地说,应该是在 2003 年,由世界大师级人物 Martin Fowler(马丁 · 福勒)在他写的一本叫做《企业应用架构模式》书里就描述过这个模式。不可否认,马丁是软件架构的泰斗,他写的每本书,我都买过,虽然很多内容我还看不懂,但每次阅读都有新的认识,虽然这些文字已经很陈旧了。
如果您想了解关于 Active Record 的权威定义,可以点击下面的维基百科地址:
当然,如果您想听到更通俗易懂的言语,我可以试着描述一下:
- 它是面向领域对象的设计模式
- 它为每个领域对象提供一组 CRUD 方法
以上提到的 领域对象
实际上就是我们经常说的 Entity
(实体)。
Active Record 模式最早是在 Ruby on Rails(RoR)里取得了最佳实践,然后其它开发语言开始效仿,比如:PHP、Python 等,当然 Java 也不例外。
这几天我收集了几款基于 Java 的 Active Record 开源项目,这些项目都非常优秀,让我收获良多、受益匪浅!所以我忍不住想与大家分享一下我的学习心得与体会。
需要申明的是:本文仅代表个人看法,本人仅站在使用者的角度来体验这些产品,并非对技术架构与实现进行评价,若大家有不同见解,欢迎讨论!
下面的这些 Active Record 框架的排名不分先后,但我还是想给出一个“推荐指数”(满分是 5 颗星),当然这仅代表我个人的观点。
为了描述方便,以下将 Active Record 简称为 AR
。
JFinal(国产)
JFinal 是国内最流行的轻量级 Java Web 框架之一,作者深厚的功力,让我敬佩万分!今天也是我第一次评价 JFinal,当然仅作为一名用户,对该框架的 AR 使用方面发表一点个人观点。
我们不妨先看看在 JFinal 中是怎样使用 AR 的吧:
// 创建name属性为James,age属性为25的User对象并添加到数据库new User().set("name", "James").set("age", 25).save();// 删除id值为25的UserUser.dao.deleteById(25);// 查询id值为25的User将其name属性改为James并更新到数据库User.dao.findById(25).set("name", "James").update();// 查询id值为25的user, 且仅仅取name与age两个字段的值User user = User.dao.findById(25, "name, age");// 获取user的name属性String userName = user.getStr("name");// 获取user的age属性Integer userAge = user.getInt("age");// 查询所有年龄大于18岁的userListusers = User.dao.find("select * from user where age > 18");// 分页查询sex为1并且年龄大于18的user,当前页号为1,每页10个userPage userPage = User.dao.paginate(1, 10, "select *", "from user where sex=? and age>?", 1, 18);
可见,代码还是非常精辟的,面向 User 实体,通过访问该实体的 dao
成员变量来调用相关 AR 方法,非常不错!
尤其是这种链式方法,简直太棒了!
new User().set("name", "James").set("age", 25).save();
此外,该框架还提供了一个 Db + Record
的开发模式,无需编写任何的实体就能完成数据库操作。我很喜欢,很赞!
最新的 1.6 版本也支持了多数据源,也就是说,在 AR 中可以同时使用 MySQL 与 Oracle 数据库,这是两个不同的数据源。这个特性也非常好!相信一定很受欢迎。
但作为用户而言,我还是想提几点在编码过程中一点点太完美的地方,当然只是吹毛求疵了。
首先,以上代码中的 set 方法里的 key 是 User 的属性名,假如需要将 name 重命名,那么势必会修改很多相关的代码,否则如果有一个漏掉了,就容易出现 Bug,也就是说,当重构时会有风险。
然后,我们来看看以下这行代码:
User.dao.findById(25)
我认为,如果能这样写会更加精简:
User.find(25)
也就是说,把 dao
去掉,并且简化方法名,默认就是根据 id 来获取实体对象。
下面我大致总结一下:
亮点:
- 链式方法调用,例如:new User().set("name", "James").set("age", 25).save()
- 无需编写实体即可操作数据库,例如:提供 Record、Db 这些实用类
- 支持多数据源,例如:在 find 时可指定 mysql 或 oracle
瑕疵:
- 通过字符串来操作实体属性,当重构时会有风险,例如:user.set("name", "James")
- API 不够简洁,例如:User.dao.findById(25) 方法,不妨可简化为:User.find(25)
- 分页方法过于繁琐,例如:将 select 语句划分为两段,形成了两个参数
参考:
- (官网)
- (文档)
- (ActiveRecord 开发示例)
- (Db + Record 模式示例)
推荐指数:★ ★ ★
ar4j
这个框架是一个老外写的,我对它并不是太熟悉,只是从文档上大致学习了一下。
比较有特色的是,实体类无需扩展任何类,只需实现一个该框架提供的名为 ActiveRecord<T> 的泛型接口,我们需要将具体的 Entity 类型太填充这个泛型。就像这样:
public abstract class Primary implements ActiveRecord{ ...}
这里定义了一个名为 Primary
的实体类,实现了 ActiveRecord<Primary>
接口,此外,将该类设置为 abstract
的,因为无需在代码中 new 这个对象。
下面我们看看该框架是如何实现 find 操作的:
IActiveRecordFactory factory = NamedSingletonActiveRecordFactory.getFactory();Primary instance = factory.getActiveRecord(Primary.class);instance.getContext().setDataSource(dataSource);MapfirstParameters = new HashMap ();firstParameters.put("code", "FIRST_PRIMARY");Collection firstResults = instance.find(firstParameters);
首先,我们得初始化一个 IActiveRecordFactory
工厂(接口),然后使用该工厂去获取相应的 AR 实例,也就是这里的 Primary
实体对象了。
然后,通过构造一个 Map,来填充查询条件,貌似这里的每个查询条件都是 and
的关系。
最后,携带那个 Map 参数来调用 Primary
实体对象的 find
方法,从而获取相应的集合对象。
这种方式感觉有些保守,从代码上来看,写得太多,不太优雅。说实话,我个人不太喜欢。
需要提醒大家的是,该框架在 2010 年就停止维护了,考虑使用该框架的同学需要想想了。
不管怎么说,还是要总结一下的:
亮点:
- 实体无需扩展任何类,只需实现一个 ActiveRecord<T> 接口
- 支持自定义数据类型转换
- 方便与 Spring 集成
瑕疵:
- 创建 Record 对象不太直接,需要借助 Factory 类
- find 方法的参数过于繁琐,针对查询条件与分页参数
- 不支持 OneToMany 等特性
参考:
- (Google Code)
- (文档)
推荐指数:★ ★
jActiveRecord(国产)
这个框架我是在 OSC Git 上发现的,当初是被它的 readme 文档所吸引,写得非常简洁,新手能在较短的时间内入门。
该框架为用户提供了三个实用类,分别是:DB
、Table
、Record
,可想而知,这三个类分别对应:数据库、表、记录,这样的定义方式让人非常容易接受,至少学过关系型数据库的同学们都知道这三个概念。
我们再来简单看看它的用法:
连接数据库:
DB sqlite3 = DB.open("jdbc:sqlite::memory:");
添加:
Table Zombie = sqlite3.active("zombies");Zombie.create("name:", "Ash", "graveyard:", "Glen Haven Memorial Cemetery");Zombie.create("name", "Bob", "graveyard", "Chapel Hill Cemetery");Zombie.create("graveyard", "My Fathers Basement", "name", "Jim");
查询:
Record jim = Zombie.find(3);int id = jim.get("id");String name = jim.get("name");Timestamp createdAt = jim.get("created_at");
更新:
Record jim = Zombie.find(3);jim.set("graveyard", "Benny Hills Memorial").save();jim.update("graveyard:", "Benny Hills Memorial");
删除:
Zombie.find(1).destroy();Zombie.delete(Zombie.find(1));
关联:
Zombie.hasMany("tweets").by("zombie_id");Tweet.belongsTo("zombie").by("zombie_id").in("zombies");Record jim = Zombie.find(3);Table jimTweets = jim.get("tweets");for (Record tweet : jimTweets.all()) { ...}
可见,代码可读性还是挺高的,而且也非常容易理解,这说明作者在 API 的设计上还是花了点功夫的,非常不错!
还支持多种实体之间的关联,绝对是有一定技术含量的,所以这一定是该框架的一大亮点。
该框架非常活跃,从 OSC Git 上观察到,最近作者提交过代码。
正所谓“人无完人”,对于框架也不例外,下面便是我对该框架的总结:
亮点:
- 仅提供 DB、Table、Record 三个实用类,就能操作数据库
- 支持多种数据库:MySQL、PostgreSQL、HyperSQL、SQLite
- 支持一对多、多对一、多对多等关联
瑕疵:
- 不支持 Oracle 数据库,需要自行扩展
- 通过字符串来操作实体属性,当重构时会有风险
- 需要通过 API 的手工方式来建立关联,不具备自动方式
参考:
- (OSC Git)
推荐指数:★ ★ ★ ★
etmvc(国产)
其实我很早就关注过 etmvc 框架,因为它是一款轻量级 Java Web 开发框架。在该框架中,我学到了很多宝贵的经验,非常感谢作者的贡献与分享!
说起该框架的作者,也许大家非常熟悉,他就是著名的 jQuery Easy UI 的创始人,能把 Java 与 JS 玩得如此棒的人,我都非常地佩服,膜拜一下!
需要的是,对于 AR 而言,只是该框架中的一部分,但这部分绝对给它增加了不少分数。
还是先看看具体的使用方法吧:
首先,我们需要定义一个实体类:
@Table(name="users")public class User extends ActiveRecordBase{ @Id private Integer id; @Column private String name; @Column private String addr; @Column private String email; @Column private String remark; //get,set...}
需要注意的是,User
实体必须继承框架提供的 ActiveRecordBase
父类,这样才能拥有相关的 AR 方法。
然后,我们再来看看具体的 CRUD 操作:
插入:
User user = new User();user.setName("name1");user.setAddr("addr1");user.setEmail("name1@gmail.com");user.save();
更新:
User user = User.find(User.class, 3);user.setRemark("user remark");user.save();
删除:
User user = User.find(User.class, 3);user.destroy();
查询:
Listusers = User.findAll(User.class);for(User user: users){ System.out.println(user.getName());}
多条件查询:
Listusers = User.findAll(User.class, "addr like ?", new Object[]{"%百花路%"});for(User user: users){ System.out.println(user.getName());}
但貌似与 JFinal 存在类似的问题,那就是 find
方法用起来有些繁琐,比如:
User user = User.find(User.class, 3);
如果能这样写就更加漂亮了:
User user = User.find(3);
也就是说,find
方法里的第一个参数 User.class
是多余的,因为 find
方法已经是 User
类中的 static 方法了,是有办法获取 User.class
的。
与 JFinal 相同,多数据源在该框架中也是支持的。
此外,还提供了编程式事务,但框架自身没有提供声明式事务的支持。
有点特色的还有,可以在实体类上配置注解,来支持多种关联,相信用过 Hibernate 的同学一定不会感到陌生。
这个框架目前也不再维护了,最新版本的发布时间是 2009 年 12 月,虽然该框架真的非常优秀,但我还是要建议大家谨慎使用。
来对它总结一下吧:
亮点:
- 支持多数据源
- 使用类似注解来配置关联,支持一对多、一对一、多对一等
- 支持回调方法
瑕疵:
- find 方法不够简洁,例如:User.find(User.class, 3),不妨可简化为:User.find(3)
- 不支持多对多关联
- 仅支持编程式事务控制,不支持声明式或基于注解的配置
参考:
- (Google Code)
- (ORM-ActiveRecord 基础)
- (ActiveRecord 中同时访问多个数据库)
- (ActiveRecord 中的关联)
- (ActiveRecord 中集成 Spring)
- (ActiveRecord 中使用事务)
- (ActiveRecord 中的数据类型映射)
- (ActiveRecord 中的回调方法)
推荐指数:★ ★ ★ ★
ActiveJDBC
说起 ActiveJDBC,想必有些人听说过,因为该框架是最早开始用 Java 实现 AR 模式的,可以号称 Java 界里第一个吃 AR 这只螃蟹的人。
因为做得比较久了,项目得到了很好的沉淀,功能相当之多,建议大家可以看看它的官方文档。
有点特色的是,无需编写任何的实体属性,而只需继承一个框架提供的 Model
类即可。下面是一个实例类:
public class Person extends Model {}
该框架可以自动扫描数据库的表结构,通过字节码增强的方式来修改实体类的 class 文件。
尤其是这样的链式方法,我个人是非常喜欢的:
Listpeople = Person.where("name = ?", "John");Person aJohn = people.get(0);String johnsLastName = aJohn.get("last_name");Paging through dataList people = Employee.where("department = ? and hire_date > ? ", "IT", hireDate) .offset(21) .limit(10) .orderBy("hire_date asc");
无需担心不同数据库的分页 SQL 语句的差异性了。
该框架的功能较多,特点也非常多,我还是简单总结一下吧:
亮点:
- 无需编写任何的实体属性,只需继承一个 Model 父类,由框架来扫描数据库表结构,生成实体属性与关联
- 提供链式的 find 方法调用,尤其是编写分页代码时非常优雅
- 可使用注解定义缓存
瑕疵:
- 通过字符串来操作实体属性,当重构时会有风险
- 在实体上进行执行 SQL 查询,有些不太合适
- 对于多对多关联,需要为中间表编写对应的实体类(Hibernate 不需要这样做)
参考:
- (Google Code,已废弃)
- (官网)
推荐指数:★ ★ ★ ★
ActiveObjects
我是在 Google 里得知 ActiveObjects 这个开源项目的,貌似以前火了一段时间,至少在互联网上可以找到关于它的博文。
但让我非常理解的是,为何该框架官网也打不开?貌似作者不再维护了。更让我惊讶的是,我在 Atlassian 的开源项目中看到了该框架。可以推论出,该框架已经纳入 Atlassian 的体系架构了。
不知道算不算亮点,该框架竟然将实体定义为接口,而不是类。下面是一个实体接口:
public interface Company extends Entity { // ...}
这个接口需要继承框架提供的 Entity
接口。那么实体属性如何定义呢?
public interface Company extends Entity { public String getName(); public void setName(String name); public String getTickerSymbol(); public void setTickerSymbol(String tickerSymbol);}
看到以上这样的代码,我第一反应是,这些代码需要手写了,因为 IDE 几乎没办法自动生成这些 getter/setter 方法。
我们再来看看具体怎么用?
EntityManager manager = new EntityManager("jdbc:mysql://localhost/test", "user", "password");Company[] companies = manager.find(Company.class);
首先,我们需要创建一个 EntityManager
对象,然后,通过该对象去 find
出相应的实体数组。
这类风格好不好呢?反正我个人是不喜欢的。
说实话,我很少关注 Atlassian 的开源项目,他们的商业产品,比如:JIRA、Confluence 等,我还是非常喜爱的(尤其是破解版)。
亮点:
- 将实体定义为接口,自身继承一个名为 Entity 的接口
- 通过基于 Query 类的链式方法生成 SQL 语句
- 支持自定义 ORM 映射规则
瑕疵:
- 在实体接口中没有属性,只有属性的 getter/setter 方法,无法通过 IDE 自动生成
- 查询操作均通过 EntityManager 来完成,与 ActiveRecord 的传统风格不太一致
- 项目已归入 Atlassian 名下,成为 Atlassian Platform Common Components 的一部分
参考:
- (官网,无法访问)
- (博文,英文)
- (博文,中文)
- (文档)
推荐指数:★ ★
Ebean
Ebean 是一位朋友推荐给我的,曾经听说过,但并没有关注过,一直认为它是一个小众框架。当我静下心来学习之后,才发现该框架真所谓“麻雀虽小,五脏俱全”啊!
有点类似于精简版的 Hibernate,它基于 JPA 接口,提供了根据实体自动生成 DDL 的功能,可以借助 Spring 强大的事务管理机制。总而言之,该框架我很喜欢!
要使用该框架,必须提供一个 ebean.properties
配置文件,做简单的配置即可使用,相应的配置请参考官方文档(见下面的参考部分)。
我们看看如何来插入一条记录?
ESimple e = new ESimple(); e.setName("test"); e.setDescription("something"); Ebean.save(e);
通过 Ebean
这个实用类就能完成,这里不太像其他 AR 框架那样,直接在实体 ESimple
上调用 save
方法。不知道这算不算严格意义上的 AR 呢?
对于查询而言,同样也是面向 Ebean
类的:
Listlist = Ebean.find(Order.class).findList();
以上代码貌似还可以再简洁一下,比如:
Listlist = Ebean.findList(Order.class);
这样是不是更好呢?我认为没必要先调用 find
方法。
该项目最近(2014 年 4 月)才发布了 3.3.1 版本,可见还有是非常有生命力的。
稍微归纳一下吧:
亮点:
- 基于 JPA 规范,依赖于 persistence.jar
- 可以通过实体生成 DDL,类似于 Hibernate
- 支持类似 Spring 的事务传播行为,并且能与 Spring 无缝集成
瑕疵:
- find 方法不够简洁,例如:Ebean.find(Order.class).findList(),不妨可简化为:Ebean.findList(Order.class)
- 链式风格的查询条件不太适合复杂情况,对于复杂的情况需要使用类似 SQL 的查询语言
- 通过字符串硬编码的方式来连接查询,当重构时会有风险
参考:
- (官网)
- (文档)
- (快速入门)
推荐指数:★ ★ ★ ★
jOOQ
jOOQ 同样也是一位朋友推荐的,我花了一点时间学习了一下。发现学习成本不高,还是比较容易上手的。官网做得很漂亮,文档也非常丰富!
使用该框架,我们需要先定义数据库表结构,然后就是配置,最后该框架提供了一个代码生成器,我们只需通过 java 命令就能运行该代码生成器,最终为我们生成实体类 Java 源码。与 Hibernate 的使用过程正好相反,Hibernate 要求我们先定义实体类,然后通过实体类来生成数据库表结构。
对于用户而言,只需使用该框架提供的 API 即可完成底层的 SQL 操作,而无需编写具体的 SQL 代码。就像这样:
DSLContext create = DSL.using(conn, SQLDialect.MYSQL);Resultresult = create.select().from(AUTHOR).fetch();
我们必须提供一个数据库连接,也就是以上代码中的 conn
参数。第二行代码将执行一条 MySQL 的 SQL 语句:
select * from author;
需要补充说明的是,以上代码中的 AUTHOR
其实是来自于 test.generated.Tables.*
包,所以我们需要使用静态导入。此外,DSL
是来自于 org.jooq.impl.DSL.*
包的,同样需要静态导入。
import static test.generated.Tables.*;import static org.jooq.impl.DSL.*;
下面要做的就是遍历 result
对象了:
for (Record r : result) { Integer id = r.getValue(AUTHOR.ID); String firstName = r.getValue(AUTHOR.FIRST_NAME); String lastName = r.getValue(AUTHOR.LAST_NAME); System.out.println("ID: " + id + " first name: " + firstName + " last name: " + lastName);}
看起来貌似挺简单的,实际好不好用呢?那就需要在进一步的实践中慢慢体会了。
还有更复杂的 SQL 语句,都可以通过 Java API 的方式来表达,如下图(来自于官网):
需要指出的是,该框架仅提供基于 Java 代码的 SQL 操作,并没有提供事务管理框架,所以还是需要借助像 Spring 这样的框架来实现。
亮点:
- 可读取配置扫描数据库,自动生成 Java 代码(与 Hibernate 正好相反)
- 使用链式方法生成对应方言的 SQL 语句
- 代码生成器可以定制
瑕疵:
- 开发人员需要熟知表结构与之间的关系
- 未提供事务控制,但可集成 Spring 的事务
- 对于复杂 SQL 查询需要编写大量的 Java 代码
参考:
- (官网)
- (文档)
推荐指数:★ ★ ★ ★
其它
当然,基于 Java 的 AR 框架还有很多,下面补充几个,大家有空可以了解一下:
- ActiveJPA:(GitHub)
- Siena:(官网)