本篇介绍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(){
}
}
调用方存在的问题:
- 数据库的表名、属性名可能会更改,一旦更改时候,Java代码中的硬代码也要跟着改变。
- 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
}
从上所示,两者区别:
- 区别在于存在默认约定否,前者没有明确的规定,如“Java中的int对应着数据库的什么类型”,而beego则明确规定“golang的int对应着数据库的int类型”。
- 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语句。
存储过程
从上述讨论上可知,并不建议开发者在SQL脚本里编写存储过程。
笔者观点:
- 代码主导权在应用端,则应用可以随时转换数据库;同理,主导权在数据库端,则数据库可以随时更改应用。而常常,代码的主导权在应用端,所以应该在主导权端进行存储过程的编写。
- 存储过程和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的这种调用存储过程的方式,就是将主导权交给SQL脚本了。
然而使用应用层的method对数据库的操作进行封装,从而代替存储过程,这是否可行呢?学习Java高并发秒杀API之高并发优化该课后,我们可以明白:
- 在高并发(如热点),使用应用端的封装是不可行的,会有性能上的损失。 解决方案:编写Java代码,再将Java代码翻译成SQL代码,以解决性能和跨平台的问题。
- 非并发情况下,则是可以的。(性能上没有太大追求)
而上述的解决方式,其实就是在做一件事情:使SQL标准化。首先,我们要考虑一下这个的可行度,以及可以达到的程度。另外,也需要考虑debug的难度。
- 存储过程的做事情与Java的method差不多,倘若要转换Java代码至SQL语句则必须编写一个代码解释器,工程量非常大。简单的类似DSL的语句很难满足SQL存储过程。(类似重新开发一种新语言)
- debug仍旧需要在数据库IDE上进行调试。不过bug数量减少了。
结论:应用使用ORM里的存储过程,只是单纯地调用。
事务
事务的使用如下:
START TRANSACTION;
INSERT INTO test_tab VALUES (1, '2');
INSERT INTO test_tab VALUES (1, '3');
COMMIT;
每行都具有分号,也就是可以每句话分开执行,因此开发较为简单。
分别开发以下接口
- begin语句的执行。
- 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=>数据库)。
而”逆向工程“则是(数据库=>代码)
不管是正向还是逆向工程,都有两种操作。
- 插件方式。
- 代码自动生成并执行。
后者灵活性相对弱,但是易于使用。另外,减少纯手写的sql语句也有助于减少bug。
缓存
缓存的意义在于让查询更为快,因此很多orm框架都有缓存,比如:hibernate等orm框架均有一级缓存以及二级缓存。而数据库也常常具有一级缓存这些概念,就比如MySQL。
hibernate的一级缓存,用于缓存连续操作中的查询操作。二级缓存,则用于多个应用/服务器同步缓存。
首先要解决的问题
- 数据库的缓存和orm的缓存功能上的区别
- 缓存的分布式管理
如果架构如下:
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语句拼接的方式,该方式无法适用于缓存的查询,导致了缓存查询需要新编写。
分表
我司对于分表,是采用Java代码进行实现。而这样的坏处
- Java代码层次需要实现上数据库的功能,也就是重复造轮子。(需要考虑自己的轮子的效率)
好处
- 跨平台。
笔者不建议自己造轮子。如果该平台没有这个功能,才造,但是优先使用数据库为主。