如何实现一个自己的ORM框架

我想,在日常的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类型一样,只是对字符串限制了长度,并且增加了默认约束。 所以根据这种关系,我们可以把stringvarchar进行对应,把numberfloat64进行关联,用以表达出更大范围的数字。

根据这种对应关系,我们可以写出以下的从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!

相关推荐
y先森3 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy3 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿4 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡5 小时前
commitlint校验git提交信息
前端
虾球xz5 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇5 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒6 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员6 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐6 小时前
前端图像处理(一)
前端