一个超轻量级的 ORM 框架

时间:2022-05-17 20:40:09

在上集里,我与大家分享了有关“数据访问层”的相关解决方案。这里是上集的链接:

http://my.oschina.net/huangyong/blog/265378

数据访问层说得专业一点就是 DAO(Data Access Object)层,在 Java 里是通过封装 JDBC 接口来实现的,实现起来难度并不太大,开源第三方包也不少,Smart 目前就选择的是 Apache Commons 的 DbUtils 代码库做为实现的,当然,现在您也可以轻松使用其它第三方库来取代。

与 DAO 关联性比较大的就是 ORM(Object Relationship Mapping,对象关系映射),Hibernate 就是一款市面上最为流行的 ORM 开源框架之一。Smart 坚持走自己的路线,放弃了沉重的 Hibernate,选择了一种最简单、最直接、最粗暴的方式来实现 ORM。在本集里,我将与大家分享 Smart 关于 ORM 的那点事儿。

开始!

ORM 映射规则

可以肯定的是,Hibernate 将 ORM 的思想发挥到了极致,而且充分运用了 DSM(Domain-Specific Modeling,面向领域建模)这种开发模式。也就是说,这种模式建议我们,从业务需求中找到所涉及的实体(Entity),实体包含的属性(Property),以及实体与实体之间的关系,对这些实体及其关系进行建模。

由于有了 ORM,我们无须使用针对某种数据库来编写特定的 SQL 语句,因此 Hibernate 还提供了一个类似 SQL 的查询语句,名为 HQL(Hibernate Query Language),开发者只需面向 Entity 就写出简短 HQL 语句,Hibernate 就能自动根据 HQL 来生成具体的 SQL 并执行数据库操作,最终返回相应的结果。

Smart 也吸收了 Hibernate 的这套 ORM 思想,只是想做一个超轻量级的 ORM 框架。我们不妨先从 Entity 开始说吧。

这里有一个描述“产品”实体,名为 Product,代码如下:

<!-- lang: java -->
@Entity
public class Product {

private long id;
private long productTypeId;
private String productName;
private String productCode;

...

// getter/setter
}

我们只用了一个 Entity 注解来标示 Product 是一个实体类,而该实体中所有的 Property 都使用 Java 的驼峰式命名规范来编写,最后是每个 Property 所对应的 getter/setter 方法。

那么,Entity 及其 Property 是怎样映射数据库中的表(Table)及其列(Column)的呢?我们还是以 Product 实体为例进行说明。

  • Product 实体 => product 表
  • id 属性 => id 列
  • productTypeId 属性 => product_type_id 列
  • productName 属性 => product_name 列
  • productCode 属性 => product_code 列

相信大家已经看出了一些规律,一句话:将 Entity 的“驼峰式”转换为 Table 的“下划线式”,该规则对于实体名与属性名都有效。

当然,这个映射规则是 Smart 默认行为,如果您的数据库并非这类下划线风格的,那么就需要使用如下两个注解来自定义映射规则了。

  • Table 注解:用于映射具体的表名,例如:@Table("t_product")
  • Column 注解:用于映射具体的列名,例如:@Column("c_product_name")

可见,Smart 的 ORM 规则还是比较简单的,没有像 Hibernate 那样,需要开发者使用一大堆的注解。此外,在 Smart 中使用了“贫血模型”,而 Hibernate 可以支持“充血模型”。也就是说,Smart 没有使用 Hibernate 的 OneToMany、ManyToOne、ManyToMany 等映射规则,Smart 只是将一个 Entity 与一张 Table 进行了简单映射。

因篇幅有限,以上 ORM 规则的具体实现,请大家自行阅读源码,见 EntityHelper 类。

有了 ORM 映射规则,我们想面向 Entity 实现查询与更新,简直轻而易举,由于 Smart 也参考了 Hibernate 的 HQL 语句,提供了一个更简单的 ORM 操作方法。

ORM 操作方法

在 Smart 中,有个名为 DataSet 的类,该类中提供了大量的 static 方法,我们非常方便地就能调用这些方法。

DataSet 具体代码请参考 Smart 源码,这里仅用一个非常具有代表性的方法进行说明。

<!-- lang: java -->
/**
* 提供与实体相关的数据库操作
*
*
@author huangyong
*
@since 1.0
*/

public class DataSet {

/**
* 查询单条数据,并转为相应类型的实体
*/

public static <T> T select(Class<T> entityClass, String condition, Object... params) {
condition = transferCondition(condition);
String sql = SqlHelper.generateSelectSql(entityClass, condition, "");
return DatabaseHelper.queryEntity(entityClass, sql, params);
}
...
}

我们先来看 select 方法的参数:

  • Class<T> entityClass:一个基于泛型的实体类
  • String condition:一个基于实体的查询条件
  • Object... params:查询条件的动态参数

怎么用呢?

比如:通过产品编号查询产品,我们可以这样写:

<!-- lang: java -->
Product product = DataSet.select(Product.class, "productCode = ?", productCode);

其中,entityClass 是一个具体的实体类名,condition 是一个面向实体的查询条件(注意:不是 SQL 语句的 where 条件),最后的动态参数 productCode 对应 condition 中的 ?,从左向右进行对应。

如果您用过 Hibernate,这样的写法一定不会感到陌生。

下面我们看看以上 select 方法的具体实现,分以下三步:

  1. 首先,将查询条件进行转换,这里需要转换的是,将 productCode 转换为 product_code
  2. 然后,借助 SqlHelper 来生成具体的 SQL 语句。
  3. 最后,通过 DatabaseHelper 执行 SQL 语句并返回相应的结果。

可见,以上三个步骤中,最难实现的应该是第一步,也就是,如何将 Property 转换为 Column?说白了就是,先找出 condition 中 Property,然后将“驼峰式”的 Property 转换为“下滑线式”的 Column。

这里只是一个很简单的查询条件,并没有一定的通用性,所以不妨设想一个更为复杂的查询条件:

productTypeId = ? and productName like '%productName%'

这个查询条件就有点意思了,注意 productName 可以是属性名也可以是查询条件中的属性值。我们的目标是,提取出查询条件中真正的属性名,而不是在属性值中,与属性名风格相同的字符串。

那么怎样才能找到所有的属性名呢?

有两个方法可以实现,一是解析字符串,二是通过正则表达式进行匹配。

我们不妨使用正则表达式来实现该功能吧,因为使用这种方式将更容易维护,代码量也会更少一些,性能或许也会更好一点。

那么第一个问题就是,写一个正则表达式来匹配属性名。我们不妨仔细心来,分析一下属性名的命名规则与出现在查询条件中的相对位置。不难得到以下两个特征:

  1. 属性名是“驼峰式”命名的,由字母、数字、下划线构成,且不能以数字开头。
  2. 属性名在操作符左边,这里的操作符不限于 = 与 like,此外还包括 !=<>>>=<<= 等,总之,凡是 SQL 有的,这里都要有。
  3. 属性名与操作符之间可能会有零个或多个空格。

有以上三点特征,我们就可以编写一个正则表达式来匹配需要的属性名了。

编写正则表达式绝非是一件轻松的事情,对于复杂的情况,推荐大家使用这款工具:

http://regex101.com/

我们只需要把需要匹配的字符串贴进去,然后试图写正则表达式,该工具就可以自动匹配出我们想要的字符,如下图所示:

一个超轻量级的 ORM 框架

该工具还有保存正则表达式的功能,以便分享与传播。大家不妨点击以下这个链接试试看:

http://regex101.com/r/aW9eL8

当我们得到了这个正则表达式:([a-z_]+([A-Z]+[a-z|0-9|_]+)+)\s+(=|!=|<>|>|>=|<|<=|like)\s+,下面要做的就是编写一段 Java 代码,使用这个正则表达式来实现我们想要的功能。

<!-- lang: java -->
private static String transferCondition(String condition) {
StringBuffer buffer = new StringBuffer();
if (StringUtil.isNotEmpty(condition)) {
String regex = "([a-z_]+([A-Z]+[a-z|0-9|_]+)+)\\s+(=|!=|<>|>|>=|<|<=|like)\\s+";
Matcher matcher = Pattern.compile(regex).matcher(condition.trim());
while (matcher.find()) {
String column = StringUtil.camelhumpToUnderline(matcher.group(1));
String operator = matcher.group(3);
matcher.appendReplacement(buffer, column);
buffer.append(" ").append(operator).append(" ");
}
matcher.appendTail(buffer);
}
return buffer.toString();
}

不妨写一个 main 方法测试一把:

<!-- lang: java -->
public static void main(String[] args) {
String condition= "productTypeId = ? and productName like '%productName%'";
System.out.println(DataSet.transferCondition(condition));
}

输出:

<!-- lang: sql -->
product_type_id = ? and product_name like '%productName%'

没错,这样的结果才是我们想要的。

以上便是我想与大家分享的有关 Smart ORM 的相关精华,当然,还有很多更有价值的特性,需要您通过阅读源码来了解。我随时等待大家的宝贵建议!

Smart - 轻量级 Java Web 开发框架


[补充]:以下是我的小伙伴 快枪手 的解决方案,他将查询条件进行了抽象,封装了一个 Conditions 对象,通过该对象来生成查询条件,大家觉得他的方案如何呢?

<!-- lang: java -->
public class Conditions {

private List<Condition> conditionList = new ArrayList<Condition>();

public Conditions condition(String name, String operator, String value) {
return append(name, operator, value);
}

public Conditions and() {
return append("and");
}

public Conditions or() {
return append("or");
}

public Conditions left() {
return append("(");
}

public Conditions right() {
return append(")");
}

@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
for (Condition condition : conditionList) {
String name = condition.getName().trim();
String operator = condition.getOperator().trim();
String value = condition.getValue().trim();
if (StringUtil.isNotEmpty(name)) {
builder.append(StringUtil.camelhumpToUnderline(name));
}
if (StringUtil.isNotEmpty(operator)) {
builder.append(" ").append(operator.trim()).append(" ");
}
if (StringUtil.isNotEmpty(value)) {
if (value.contains("?")) {
builder.append(value);
} else {
builder.append("'").append(value).append("'");
}
}
}
return builder.toString().trim().replaceAll("\\s{2}+", " ");
}

private Conditions append(String name, String operator, String value) {
Condition condition = new Condition(name, operator, value);
conditionList.add(condition);
return this;
}

private Conditions append(String operator) {
return append("", operator, "");
}

private class Condition {

private String name;
private String operator;
private String value;

private Condition(String name, String operator, String value) {
this.name = name;
this.operator = operator;
this.value = value;
}

public String getName() {
return name;
}

public String getOperator() {
return operator;
}

public String getValue() {
return value;
}
}
}

如何生成查询条件?

<!-- lang: java -->
String condition = new Conditions()
.condition("fizzBuzz", "=", "1")
.and()
.left()
.condition("buzzWhizz", "=", "2")
.or()
.condition("fizzWhizz", "=", "3")
.right()
.toString();
System.out.println(condition);

相当于将以下查询条件:

<!-- lang: sql -->
fizzBuzz = '1' and (buzzWhizz = '2' or fizzWhizz = '3')

通过链式方法调用转换为:

<!-- lang: sql -->
fizz_buzz = '1' and (buzz_whizz = '2' or fizz_whizz = '3')