ORM设计

  1. JDBC 阶段
  2. 设计orm
    1. 表(table)信息
    2. 视图(view)信息
    3. 存储过程
    4. 事务
    5. Java代码和SQL代码同步
    6. 缓存
    7. 分表

本篇介绍ORM该如何设计,笔者并非大牛,如有错误请告知。

JDBC 阶段

Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package github.caliburn1994.test.chapter1;

import java.sql.*;

public class MySQLDemo {

    // JDBC 驱动名及数据库 URL
    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql://localhost:3306/ormdb";

    // 数据库的用户名与密码,需要根据自己的设置
    static final String USER = "root";
    static final String PASS = "123456";

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try {
            // 注册 JDBC 驱动
            Class.forName("com.mysql.jdbc.Driver");

            // 打开链接
            System.out.println("连接数据库...");
            conn = DriverManager.getConnection(DB_URL, USER, PASS);

            // 执行查询
            System.out.println(" 实例化Statement对象...");
            stmt = conn.createStatement();
            String sql;
            sql = "SELECT id, name, psw FROM user";
            ResultSet rs = stmt.executeQuery(sql);

            // 展开结果集数据库
            while (rs.next()) {
                // 通过字段检索
                int id = rs.getInt("id");
                String name = rs.getString("name");
                String paswd = rs.getString("psw");

                // 输出数据
                System.out.print("ID: " + id);
                System.out.print(", 名字 : " + name);
                System.out.print(", 密码 : " + paswd);
                System.out.print("\n");
            }
            // 完成后关闭
            rs.close();
            stmt.close();
            conn.close();
        } catch (SQLException se) {
            // 处理 JDBC 错误
            se.printStackTrace();
        } catch (Exception e) {
            // 处理 Class.forName 错误
            e.printStackTrace();
        } finally {
            // 关闭资源
            try {
                if (stmt != null) stmt.close();
            } catch (SQLException se2) {
            }// 什么都不做
            try {
                if (conn != null) conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            }
        }
        System.out.println("Goodbye!");
    }
}

SQL代码

1
2
3
4
5
6
7
8
9
10
11
create database ormdb;

create table ormdb.user
(
  id int auto_increment
    primary key,
  name varchar(30) null,
  psw int null
)
  engine=InnoDB
;

Java代码做了以下事情

  • 数据库配置
  • 从指定的表,获得数据
  • 将数据存入变量里

SQL代码

  • 定义了数据库和表结构

设计orm

读取数据库配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Orm{
    
    //....
    
    public static void registerDriver(){
        //数据库参数在外部文件里
    }
    
    //通过调用方式传入
    public static void registerDriver(数据库参数){
        
    }
    
    //启动,加载数据库
    public void newOrm(){
        
    }
    
}

调用方存在的问题:

  1. 数据库的表名、属性名可能会更改,一旦更改时候,Java代码中的硬代码也要跟着改变。
  2. bean(容器)中的成员变量的类型需要和数据库属性对应
    • 解决方案:通过Java代码或插件生成SQL代码


表(table)信息

(1)我司的作风:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//表信息
public class TBUser{
	//以对象方式存储表信息
    public static Field id = new Field(属性信息);
    public static Field user = new Field(属性信息);
    public static Field passwd = new Field(属性信息);
    
    public static FK fk = new FK(外键信息);//....
}

//业务类
//通过map,代替struct结构(Java中特指bean)
public class XXBiz{
    
    public void doBiz(){
        //条件语句类型太多
        Map map = Orm.getDb("User")
            		 .get(TBUser.id.name,1);//条件语句
        //手动筛选属性,以达到select id,user form user
        map.remove(TBUser.passwd.name)
        
    
        //插入语句
        Orm.getDb("User").insert(map);
        
        //删除语句
		Orm.getDb("User").delete(map);     
    }    
}

beego做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//反射机制
type User struct {//对应数据库表明
   Id //属性名         int//对应数据库类型
   Name        string
   Profile     *Profile   `orm:"rel(one)"` // OneToOne relation
   Post        []*Post `orm:"reverse(many)"` // 设置一对多的反向关系
}


func main() {
	o := orm.NewOrm()
	o.Using("default") // 默认使用 default,你可以指定为其他数据库

	profile := new(Profile)
	profile.Age = 30

	fmt.Println(o.Insert(profile))//插入
    
    qs := o.QueryTable("user")
    qs.Filter("id", 1) // WHERE id = 1
    qs.Filter("profile__age", 18) // WHERE profile.age = 18

}

从上所示,两者区别:

  1. 区别在于存在默认约定否,前者没有明确的规定,如“Java中的int对应着数据库的什么类型”,而beego则明确规定“golang的int对应着数据库的int类型”。
  2. map类型没有静态检查

所以笔者推荐beego的做法。


视图(view)信息

我司(传统做法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TBUser{
	//以对象方式存储表信息
    public static Field TbUser_id = new Field(属性信息);
    public static Field TbUser_user = new Field(属性信息);
    public static Field TbUser_companyid = new Field(属性信息);
    
    public static FK fk = new FK(外键信息);//....
}

public class TBComany extend{
	//以对象方式存储表信息
    public static Field TBComany_id = new Field(属性信息);
    public static Field TBComany_name = new Field(属性信息); 
}

//方式1, 复制黏贴
public class VwUser_Company_{
    
    public static Field TbUser_id = new Field(属性信息);
    public static Field TbUser_user = new Field(属性信息);
    public static Field TBComany_id = new Field(属性信息);
    public static Field TBComany_name = new Field(属性信息); 
    
}

//组合
public class VwUser_Company {
    public static TBUser tbuser =  new TBUser();
    public static TBcompany tbcompany =  new TBComany();
}


//由此可见,调用时,变量长度会增长。
public class Demo{
    public static void main(String[]arg){
        VwUser_Company.tbuser.TbUser_id;
        VwUser_Company.tbcompany.TBComany_id;
    }    
}

beego

1
2
3
4
5
6
7
//view是虚拟表
type View_User_Company struct {//对应数据库表明
   User_Id          int
   User_Name        string
   Company_Id		int
   Company_Id		int
}

关系数据库常常存在视图(view)的概念,然而使用视图会有什么好处?为什么要创建数据库视图? - 知乎

从上面的答案来看,其实视图和存储过程是身为声明式编程的SQL语言弥补自身的不足而产生的,对应着 「命令式编程」的方法/函数。从这个角度来分析,其实在orm的框架中,我们没有必要引入“视图概念”,直接使用方法/函数进行包裹和封装即可。

需要注意的是 :有些数据库是会对视图进行优化,有些却不会,倘若需要使用带优化的视图,则必须使用SQL语句。


存储过程

为什么阿里巴巴Java开发手册里要求禁止使用存储过程?

从上述讨论上可知,并不建议开发者在SQL脚本里编写存储过程。

笔者观点:

  1. 代码主导权在应用端,则应用可以随时转换数据库;同理,主导权在数据库端,则数据库可以随时更改应用。而常常,代码的主导权在应用端,所以应该在主导权端进行存储过程的编写。
  2. 存储过程和Java的method很类似。


而知乎上有一些观点,笔者也挺认同:

  • SQL的存储过程有点算旧时代的产物,对应的调试应用并不完整,。
  • SQL没有标准化。


传统的调用存储过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        //1.获得session对象
         Session session=HibernateSessionFactory.getSession();
        //2.设置查询过程字符串
        String procName="{Call hib7_test(?,?)}"; 
        //3.创建本地查询对象传入过程查询字符串

        SQLQuery sqlquery=session.createSQLQuery(procName);

        sqlquery.setString(0, "admin");
        sqlquery.setString(1, "admin");
        //5.执行过程返回结果集合回结果集合

        List list =sqlquery.list();
        //6.关闭session对象
        session.close();
        HibernateSessionFactory.closeSession();

参考:Hibernate如何调用存储过程

hibernate的这种调用存储过程的方式,就是将主导权交给SQL脚本了。

然而使用应用层的method对数据库的操作进行封装,从而代替存储过程,这是否可行呢?学习Java高并发秒杀API之高并发优化该课后,我们可以明白:

  • 在高并发(如热点),使用应用端的封装是不可行的,会有性能上的损失。 解决方案:编写Java代码,再将Java代码翻译成SQL代码,以解决性能和跨平台的问题。
  • 非并发情况下,则是可以的。(性能上没有太大追求)

而上述的解决方式,其实就是在做一件事情:使SQL标准化。首先,我们要考虑一下这个的可行度,以及可以达到的程度。另外,也需要考虑debug的难度。

  1. 存储过程的做事情与Java的method差不多,倘若要转换Java代码至SQL语句则必须编写一个代码解释器,工程量非常大。简单的类似DSL的语句很难满足SQL存储过程。(类似重新开发一种新语言)
  2. debug仍旧需要在数据库IDE上进行调试。不过bug数量减少了。

结论:应用使用ORM里的存储过程,只是单纯地调用。


事务

事务的使用如下:

START TRANSACTION;
INSERT INTO test_tab VALUES    (1, '2');
INSERT INTO test_tab VALUES    (1, '3');
COMMIT;

每行都具有分号,也就是可以每句话分开执行,因此开发较为简单。

分别开发以下接口

  1. begin语句的执行。
  2. rollback和commit语句的执行。

又由于数据库驱动以及帮我们开发好相对应的语句,我们只要根据数据库名称进行对应的调用即可。

Java代码和SQL代码同步

beego通过struct信息进行建表

1
2
3
4
5
6
type User struct {//对应数据库表明
   Id //属性名         int//对应数据库类型
   Name        string
   Profile     *Profile   `orm:"rel(one)"` // OneToOne relation
   Post        []*Post `orm:"reverse(many)"` // 设置一对多的反向关系
}

并解析该struct,生成“SQL代码”,也就是”正向工程“ (代码=>映射=>SQL=>数据库)。

而”逆向工程“则是(数据库=>代码)


不管是正向还是逆向工程,都有两种操作。

  1. 插件方式。
  2. 代码自动生成并执行。

后者灵活性相对弱,但是易于使用。另外,减少纯手写的sql语句也有助于减少bug。


缓存

缓存的意义在于让查询更为快,因此很多orm框架都有缓存,比如:hibernate等orm框架均有一级缓存以及二级缓存。而数据库也常常具有一级缓存这些概念,就比如MySQL。

hibernate的一级缓存,用于缓存连续操作中的查询操作。二级缓存,则用于多个应用/服务器同步缓存。

首先要解决的问题

  1. 数据库的缓存和orm的缓存功能上的区别
  2. 缓存的分布式管理

如果架构如下:

orm(server A) ==>nosql(server B)==>数据库(server C/B)

orm所在的应用需要考虑

  • 分布式,使用nosql作为缓存服务器。
  • 非分布式,使用缓存即可。

所以此处必须做接口,对应不同的版本。

nosql与orm自带的二级缓存,功能上有重叠。

数据库的缓存,则是简单的连续查询的缓存。

在Java中的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Orm{
    
    private List cache = new ArraryList();
    
    public void get(){
        
        //该处命中率可优化
        if(第一次查询){
            cache.put(存放查询结果);
            return 查询结果;
        }
        
       return cache.get(...)//从缓存中获取查询结果
        
    }
    
    
    public void delete(){
        //更新缓存
    }
    
}

这里需要注意的:

  • 数据库查查询和缓存查询的接口一致,否则会导致缓存查询的代码难以编写。我司的orm采取了SQL语句拼接的方式,该方式无法适用于缓存的查询,导致了缓存查询需要新编写。

    • 简单的查询,我认为使用这种方式更为适当。每个条件下,都会返回一个容器结果。
    • 而且使用次数少的查询,使用类似我司的SQL的DSL语句更为恰当,如这个

分表

我司对于分表,是采用Java代码进行实现。而这样的坏处

  • Java代码层次需要实现上数据库的功能,也就是重复造轮子。(需要考虑自己的轮子的效率)

好处

  • 跨平台。

笔者不建议自己造轮子。如果该平台没有这个功能,才造,但是优先使用数据库为主。