文档章节

一个超轻量级的 ORM 框架

黄勇
 黄勇
发布于 2014/05/20 00:01
字数 2409
阅读 7622
收藏 83

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

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/

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

在此输入图片描述

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

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')

© 著作权归作者所有

黄勇

黄勇

粉丝 6624
博文 121
码字总数 216155
作品 1
浦东
CTO(技术副总裁)
私信 提问
加载中

评论(26)

吕兵阳
吕兵阳
我觉得这种写法可维护性很差,特别是复杂的sql,其实我们只需要的是类似hibernate里面的单对象操作,和sqlquery类的api那样最好。
libinjareo
libinjareo
利用泛型,是不是可以实现链式调用?
Xsank
Xsank
这个 left 和 right还是有点繁琐
黄勇
黄勇 博主

引用来自“onlybest”的评论

List<SysBrowser> sbList = DataSet.selectListWithConditionAndSort(
        SysBrowser.class, "createName =?", "updateDate desc",
        new Object[] {createName});中如果createName带下划线,不能执行并且控制台无打印sql,为什么?
因为框架中定义得是实体的属性,并非数据表的字段。
o
onlybest
List<SysBrowser> sbList = DataSet.selectListWithConditionAndSort(
        SysBrowser.class, "createName =?", "updateDate desc",
        new Object[] {createName});中如果createName带下划线,不能执行并且控制台无打印sql,为什么?
一条大河波浪宽
一条大河波浪宽
恩恩,就是这样的~嘿嘿
黄勇
黄勇 博主

引用来自“水牛叔叔”的评论

快枪手的方式比较保守,用起来比较繁琐,但是感觉会比较稳定安全(如果用上动态参数就更佳了)。
博主的方式比较清新,也是我喜欢的方式。但是要求正则表达式一定要写好,否则很容易出问题。
ps:sql语句里还有个“is”操作符(比如:username is null),正则表达式里应该加上去
水牛叔叔
水牛叔叔
快枪手的方式比较保守,用起来比较繁琐,但是感觉会比较稳定安全(如果用上动态参数就更佳了)。
博主的方式比较清新,也是我喜欢的方式。但是要求正则表达式一定要写好,否则很容易出问题。
ps:sql语句里还有个“is”操作符(比如:username is null),正则表达式里应该加上去
水牛叔叔
水牛叔叔

引用来自“成熟的毛毛虫”的评论

首先支持一个,非常感谢作者为开源做贡献,总体来说清晰易懂,约定大于配置,Entity映射规则是个不错的改进
1.没有onetomany等这些规则,怎样做业务中关系的映射?举个例子,学生与课程是多对多关系,在smart框架里面怎么建这两个实体
2.有没有属性的验证,比如@NotNull
3.sql防注入问题,可以用正则处理掉单引号,分号,注释号
暂时想到这么多......
对于第一个问题我觉得:很显然吧smart用错场合了
水牛叔叔
水牛叔叔

引用来自“黄勇”的评论

引用来自“linapex”的评论

其实我在想,如果碰到了, 日期问题,如何进行查询。各大数据库的日期查询方式不一样。
在 oracle 中需要 to_date ,并且 日期时间格式 HH24:mi:ss 需要注意
在 mssql mysql 中则统一
@黄勇

我个人的经验是:日期或时间一律使用 String 或 long 类型。
我也这样觉得,日后想要以什么格式显示都行。
PDF.NET SOD 框架 5.5.4.0507 发布

菜鸟:怎么使用EF框架啊?遇到麻烦了,救命! 老鸟:试试SOD开发框架! 一直使用EF并且老是遇到麻烦?何不解放自己并且试试SOD框架呢! 它是简单的,并且容易使用的,轻量级的框架。 SOD 不仅...

独行族妖侠
2016/05/20
1K
4
超轻量级PHP建站框架--Idea-framework

Idea framework 超轻量级框架 完全基于OOP思想、MVC架构开发 采用PDO,摒弃ORM,兼容PHP7 支持三种路由模式,类库扩展方便 一套简单、灵活、轻便、惹人喜爱的国产PHP框架 Idea 是为 PHP 开发...

放空的岛
2016/05/11
1K
0
利用 Composer 一步一步构建自己的 PHP 框架(四)——使用 ORM

本教程示例代码见 https://github.com/johnlui/My-First-Framework-based-on-Composer 回顾 经过前三篇文章 基础准备 、 构建路由 和 设计 MVC ,我们已经得到了一个结构比较完整的 MVC 架构...

peasant
2016/06/03
48
0
针对PostgreSQL的最佳Java ORM框架

  【IT168 评论】Java生态系统提供了大量的ORM框架(水平各有不同)。虽然ORM大部分性能问题是由开发者自己引起的,但以只读方式使用ORM是不值得的。在此,我推荐一款ORM,该框架允许使用Pos...

it168网站
2017/01/18
0
0
Java持久层框架--restSQL

restSQL 是一个超轻量级的数据访问层,专为 HTTP 客户端提供,同时也是一个持久层框架,可嵌入任何 Java 应用。 restSQL 是: an SQL generator with Java and HTTP APIs uses a simple RES...

匿名
2011/07/20
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

CSS盒子模型

一、什么叫框模型 页面元素皆为框(盒子) 定义了元素框处理元素内容,内边距,外边距以及边框的计算方式 二、外边距 围绕在元素边框外的空白距离(元素与元素之间的距离) 语法:margin,定...

wytao1995
今天
4
0
Replugin借助“UI进程”来快速释放Dex

public static boolean preload(PluginInfo pi) { if (pi == null) { return false; } // 借助“UI进程”来快速释放Dex(见PluginFastInstallProviderProxy的说明) return PluginFastInsta......

Gemini-Lin
今天
4
0
Hibernate 5 的模块/包(modules/artifacts)

Hibernate 的功能被拆分成一系列的模块/包(modules/artifacts),其目的是为了对依赖进行独立(模块化)。 模块名称 说明 hibernate-core 这个是 Hibernate 的主要(main (core))模块。定义...

honeymoose
今天
4
0
精华帖

第一章 jQuery简介 jQuery是一个JavaScript库 jQuery具备简洁的语法和跨平台的兼容性 简化了JavaScript的操作。 在页面中引入jQuery jQuery是一个JavaScript脚本库,不需要特别的安装,只需要...

流川偑
今天
7
0
语音对话英语翻译在线翻译成中文哪个方法好用

想要进行将中文翻译成英文,或者将英文翻译成中文的操作,其实有一个非常简单的工具就能够帮助完成将语音进行翻译转换的软件。 在应用市场或者百度手机助手等各大应用渠道里面就能够找到一款...

401恶户
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部