我想,在日常的web开发过程中,我们除了接触一些快速构建web应用的框架,另外一个就是和数据库快速操作的框架,这类框架统称为ORM(object relationship mapping) 即对象关系映射,之所以称为映射,是因为对象关系表达的是数据库的表关系,但不是实际的表。比如可能字段在代码中是驼峰命名法,但是实际到了数据库当中就变成了snake_case
。ORM是作为代码和数据居中的转换工具,将我们的代码转换为可执行的SQL语句。让我们可以专注于代码。不用过多的关注于sql语句以及避免手动编写导致的语法错误和安全问题。
大部分的ORM的核心利用的是语言中的反射技巧,即在运行过程中,动态的获取对象的字段,字段值等其他信息。 比如JS当中的反射我们一直在使用,但是并没有很清楚的知道自己在使用反射,像下面这段代码:
ts
class User{
name:string
age:number
phone:string
}
Object.keys(new User())
我们可以使用keys
直接获取一个对象当中的字段,并且可以使用直接的属性访问的形式获取字段的值,比如下面的代码:
ts
let user = new User()
Object.keys(user).forEach(key=>{
let value = user[key as keyof User]
})
至于获取到了值该怎么操作,那就是个人选择了。可以获取字段,可以获取值,已经能做到很多事情了。 关于语言的特性已经介绍完毕,该介绍一下sql语句了。我们先从创建表开始吧。毕竟这类似于我们之前的定义User
类。
sql
create table user(
id int primary key auto_increment,
name varchar(100) not null default "",
age int not null default 0,
phone varchar(20) not null default ""
)
这段SQL创建表的语句和之前的定义的User有很多相似之处,但是也有很多不同,比如字段增加了一个id
,数据类型是MYSQL
的数据类型。但是数据类型大致上和我们之前定义的User类型一样,只是对字符串限制了长度,并且增加了默认约束。 所以根据这种关系,我们可以把string
和varchar
进行对应,把number
和float64
进行关联,用以表达出更大范围的数字。
根据这种对应关系,我们可以写出以下的从JS对象到sql建表语句的代码,
ts
function isString(val) {
return typeof val === "string"
}
function isUndefined(val) {
return typeof val === "undefined"
}
function GenColumn(val) {
let ColumnExpression: string[] = []
switch (typeof val) {
case "string":
ColumnExpression.push("varchar(255)")
break;
case "number":
ColumnExpression.push("int")
break;
}
if (!isUndefined(val)) { // 这说明有默认值
ColumnExpression.push("not null")
let _val;
if(isString(val)){
_val = ['"',val,'"'].join('')
}else{
_val = val
}
ColumnExpression.push(`default ${_val}`);
}
return ColumnExpression
}
function Create(target: object) {
let tableExpression: string[] = []
Object.keys(target).forEach(columnName => {
let value = (target as any)[columnName]
tableExpression.push(
[
columnName,
...GenColumn(value)
].join(" ")
);
})
return tableExpression.join(",")
}
最终输出的内容则是附带字段名和字段定义的类型
java
name varchar(255) not null default "",
age int not null default 0,
phone varchar(255) not null default ""
这段代码原本格式是没有换行的,为了方便阅读,我给每个逗号后增加了换行符。现在这段生成的SQL代码和最终我们要执行的创建表的语句有些出入。所以我们还需要添加一些代码使其更加接近我们要执行的SQL语句。
接下来我们需要获取表名,这个也很简单,在完成获取表名后需要组装整个语句,使其能够成为合法的可执行SQL语句。
ts
function Create(target: object) {
let tableExpression: string[] = []
let tableName = target.constructor.name
Object.keys(target).forEach(columnName => {
let value = (target as any)[columnName]
tableExpression.push(
[
columnName,
...GenColumn(value)
].join(" ")
);
})
return `create table ${tableName}( ${tableExpression.join(",")})`
}
最终这段代码生成的SQL语句如下:
ts
class User {
name:string = ""
age = 0
phone = ""
}
console.log(Create(new User()))
// create table User( name varchar(255) not null default "",
// age int not null default 0,
// phone varchar(255) not null default "")
接下来该实现一下其他的SQL语句生成了。我们常用到的除了创建表 还有查询语句 select 和 update语句,这些语句都有固定的模式,比如select
sql
select <列名> from <表名>
ts
function Select(val:any){
let cols = Object.keys(val)
let table = val.constructor.name
return [
"select",
"(",
cols.join(","),
")",
"from",
table
].join(" ")
}
这段代码的输出如下:
sql
select ( name,age,phone ) from User
上面的代码只是作为一个简要的实现,select除了select还有一些条件子句,比如where,like等。
update 也有一些固定模式,比如:
bash
update <tablename> set column1=value,column2=value2,column3=value3 where id=value4
后面的where语句不是当前需要解决的问题,只需要专注于前面的部分,update关键字后接着就是表名,然后就是字段和值的集合。
经过前面的代码,想必读者已经有思路如何实现生成update的TS代码了。至于其他的编程语言,比如go又或者Java。只需要问自己"我该如何获取字段?" "我该如何获取字段类型?" "我该如何获取表名?",再加上一些反射的知识,想必就很容易找到思路。至于如何让SQL语句run起来,这部分的责任在于各种驱动身上,而不是ORM框架身上。比如JAVA使用JDBC接口和各种驱动通信,输入SQL语句,经过SQL服务器执行后将结果返回回来。就是这么一个过程。ORM的责任是生成各种优化后的SQL语句,然后将语句送给各类数据库驱动,完成语句的执行。
对于TS版本的ORM,我已经编写过用以验证可行性的简单的代码,仓库如下:
祝你们玩的愉快, have fun!