带着 orm 封装的疑问,我又来了~!

2021-05-19 15:40:17 +08:00
 waibunleung

事情是这样的,在有了 orm 之后,在封装一层数据访问层,我觉得是没有大问题的。但是问题是怎么进行封装比较合适呢? 下面是两个 case (伪代码):
case1:

// 新增用户昵称
function addName(user_id, name){
    // 获取数据库链接
    db = _get_db()
    if (not db){
    	return error
    }

    add_time = time()

    insert_row = {
        user_id = user_id,
        name = name,
        status = this.status_using,
        add_time = add_time,
        updated_time = add_time,
    }
    insert_id, err = db.add(table, insert_row)
    if (err) {
        LOG("添加姓名失败" ,insert_row ,insert_id ,err)
        return error
    }

    return true
}

// service 层调用
res = xxx.addName(12, '拜拜你条尾')

code ...

case2:

function db_option(handlearr){
    is_ok = false
    if (len(handlearr) <= 0) {
        return is_ok
    }

    db = _get_db()
    if (not db){
    	return is_ok
    }

    for (index, option in handlearr) {
        is_ok = true
        if (option.cmd== '__delete') {
            res = db.delete(option.table_name, option.data)
            if (res <= 0) {
                LOG("delete data err where:",json_encode(option.data))
                is_ok = false
            }
        }
        if (option.cmd== '__insert'){
            res = db.add(option.table_name, option.data)
            if (not res) {
                LOG("insert data failed:",json_encode(option.data))
                is_ok = false
            }
        }
    }
    return is_ok
}

// service 层调用
handlearr = [
    {cmd = '__insert',table_name = 't_user', data = userinfo},  
    {cmd = '__delete',table_name = 't_device', data = {id = deviceinfo.id}}
]

res = db_option(handlearr)

code ...

1.想知道怎样的封装才是比较合适的?个人认为第一种就够了,第二种的话好像慢慢会封装成另一个 orm 的感觉,而且隐藏的细节更多了.... 2.dao 层对 orm 再封装的话,怎么样才比较符合认知?

求各位赐教!

2581 次点击
所在节点    程序员
21 条回复
ChoateYao
2021-05-19 15:49:30 +08:00
拥有 ORM 只需要封装查询,增、删、改不需要封装到数据访问层里面。

毕竟增、删、改这些业务逻辑比较固定且一般都是面向对象操作。但是查会根据不同的条件查询,这里面变化比较多,所以封装到数据访问层里面方便后期迭代优化。
no1xsyzy
2021-05-19 15:59:26 +08:00
第二种简直是格林斯潘第十定律……
waibunleung
2021-05-19 16:28:09 +08:00
@no1xsyzy 没太 get 到这个点
waibunleung
2021-05-19 16:36:49 +08:00
@ChoateYao 那删除是直接在 service 层调用?但是我想问的问题不是这个,而是哪种封装更合适一点?
我觉得,像第一种的那样,我明确告诉你就是用了添加用户姓名的,不满足你的直接再封装一个,这样的界限明确。
第二种这样,半通用半不通用的风格就比较难受,你说我要扩展的时候,是直接改你的,还是直接封装个新的?而且 新增__insert 这样的映射,有了 orm 还搞一套命令映射,我有点不理解....
ChoateYao
2021-05-19 16:55:12 +08:00
@waibunleung 抛弃第二种,第一种的话跟你直接用 ORM 的对象操作也一样,只不过是分散到 services 里面。

比如
function create(name, age) {
user = new User()
user.setName(name)
user.setAge(age)
user.save()
}

function delete(id) {
user = UserDAO.findById(id)
if (!user) {
return error
}
user.delete()
}


class UserDAO {
function findById(id) {
return UserORM.find('id = %d', id);
}
}
waibunleung
2021-05-19 17:07:40 +08:00
@ChoateYao 你这例子我感觉跟第一种都是一样的,dao 本来就是一个分层收口的作用,强调一个分层,你现在 dao 类也是基于 orm 封装了一层给外面用,我只是少了 dao 这个类,直接用了 orm 而已。例如你的 delete(id)函数我就变成了:

function delete(id) {
user = UserORM.find({id = id})
if (!user) {
return error
}
user.delete()
}
no1xsyzy
2021-05-19 18:25:06 +08:00
我是说,你的第二种,基本就是重新实现了一个不完整的 lisp
(db-do (insert 't_user userinfo)
(delete 't_device `([id ,id])))

就目前来看,你的两个重新封装都是多余的。
1. 你的 addName 并没有明确地与 xxx 产生关系,没必要将它放在 xxx 里。
最基本的,应当至少产生硬绑定 user.addSomething(...){ db.add({user_id:user.id, ...}) } 才有必要写在 user 这里。
2. 它其实根本没有降低任何复杂性,甚至干扰了表达力弱的类型系统的静态分析工作。
KouShuiYu
2021-05-19 18:54:25 +08:00
https://github.com/ckpack/pg-helper
要不借鉴借鉴我之前写的,单表操作还是挺嗨的🐶
KouShuiYu
2021-05-19 18:59:54 +08:00
waibunleung
2021-05-19 19:26:28 +08:00
@no1xsyzy 感觉有点误解了。这个 xxx 代表的是 user_model 之类的数据访问层的意思,方法的目的是为用户增加昵称,那放在 user_model 里面这个问题不大。
数据访问分层没有强调降低复杂性,而是强调分层,职责分离
no1xsyzy
2021-05-20 00:08:33 +08:00
@waibunleung 我既然是 ORM,那应当已经做到 User(id=12).addName("john") 甚至是 User(id=12).name += ["john"] 这样了,你这样 xxx.addName 反而是改回去了。

职责分离和分层感觉相互正交,一个是横切一个是纵切。
大约还是得从 MVC 架构开始重新看一遍。
bsg1992
2021-05-20 09:45:37 +08:00
正如 11L 说的那样,你这样的封装 没有啥意义。
ORM 可以做到
dbContext.XXXTable.where(x=>x.age>=18 && x.sex =1).group(x=>x.school).sum(x=>x. score)
这样的查询你如何封装呢?按照目前的做法 ,无非就是在 DAO 层 增加一个与之对应的接口。
如果想做到业务查询能映射到数据库或者其他的数据源或者 ORM,你就得引入规约模式,还得需要吧规约转换成其他 library 写法。还得提供每个不同的 library 提供转换 provider 。
既然引入了规约是不是还得包装一个 unit of work
你做到这里时,你回发现你所做的这一切 ORM 都给你实现好了。

要做到业务查询和 ORM 之类的库解耦,这个绝对考验设计能力,绝对不是你这样简单的封装就解决的问题
waibunleung
2021-05-20 10:18:17 +08:00
@bsg1992
"按照目前的做法 ,无非就是在 DAO 层 增加一个与之对应的接口。"
第一种写法不就是在做这样的事情吗? orm 是获取数据的,将获取数据的逻辑抽出来放在 dao 层,至于这个获取数据的逻辑函数,里面是通过 orm 获取,还是通过原始 sql 获取,都是可以的。

并不是说要做 orm 的封装,而是要把数据获取的逻辑抽离到 dao 层,然后才是我真正的问题,选择了这样抽离逻辑,第一种和第二种写法哪种更合适一点?

”dbContext.XXXTable.where(x=>x.age>=18 && x.sex =1).group(x=>x.school).sum(x=>x. score)
这样的查询你如何封装呢?”
按我的理解,就按照查询意图去封装,这个例子就是想获取 sum,那就在所谓的 dao 层弄个 getxxxSum(age, score)这样的方法
waibunleung
2021-05-20 10:25:58 +08:00
@no1xsyzy orm 是可以做到 User(id=12).addName("john") ,然而不想在 service 层直接调用(虽然也可以)User(id=12).addName("john") ,所以才将 它变成了 user_model.addName(),这样 service 层都不需要关心他是关系型数据库还是 nosql 添加的,还可在函数里面做一些额外的操作。这是我抽离数据获取逻辑到一个函数的原因。

然后才是我真正的问题,既然我选择了抽离,第一种和第二种哪种合适?如果都不合适,那想请教一下,你们项目里面是怎么处理数据获取的逻辑的呢?方便的话希望能提供个简单的 demo,感谢~
waibunleung
2021-05-20 10:26:30 +08:00
@bsg1992 如果说两种都不合适的话,想请教一下,你们项目里面是怎么处理数据获取的逻辑的呢?方便的话希望能提供个简单的 demo,感谢~
no1xsyzy
2021-05-20 12:40:45 +08:00
@waibunleung User(id=12).addName("john") 也不需要关注这是关系型还是 NoSQL
倒不如说最早就是模仿 NoSQL 的写法操作 RDBMS 。

你要进行额外操作应当是「面向切面编程」的课题,不过即便不是典型的 AOT 框架,也大多包含了方便的包装方法。

错误案例:
https://github.com/no1xsyzy/bgmtinygrail/blob/master/src/bgmtinygrail/db/accounts.py
主要是没有一丁点「关系」,纯粹就是把 SQLite 当 kv store,才能这么玩(虽然 CharacterStrategy 那边有 ForeignKey,但可以看到基本没有任何效果)

相对正常的:
https://github.com/no1xsyzy/great/blob/master/src/great/hierarchy.py
虽然很裸,但逻辑没有被覆盖,可以很明显地从对象语法中看出关系的存在。
waibunleung
2021-05-20 14:49:21 +08:00
@no1xsyzy 正确案例 404 了,不知道是不是仓库没开公共权限的原因,看不到(哭
waibunleung
2021-05-20 14:55:16 +08:00
@no1xsyzy 你说的错误案例,这个关系怎么理解呢?另外,感觉你说的错误案例和我的第一个 case 挺像的啊,“把 SQLite 当 kv store,才能这么玩” ,如果没有当成这样,那应该是怎么样玩?我觉得这样玩也没什么问题啊...

还望能提点一二
no1xsyzy
2021-05-21 09:24:24 +08:00
@waibunleung 确实是没开 public ……
还是拿经典的 article comment 模型,参考如下

1a. Article.query(Article.id=article_id).one().addComment(new Comment(...))
1b. Article.query(Article.reference_key=refkey).one().addComment(new Comment(...))

2a. addCommentForArticle(article_id, ...)
2b. article_id = findArticleIdFromRefKey(refkey) ;addCommentForArticle(article_id, ...)

比如你的业务目前发展到写出了 a 项,你需要扩充 b 项的代码,首先就导致了形态的不一致。
当然,你也可以把 2b 这两个部分 compose 起来,但这将导致每出现一个略微变动的业务你的 Model 层封装都要添加新的函数或者给函数扩展签名,会导致 Model 层极速膨胀。
waibunleung
2021-05-21 10:30:16 +08:00
@no1xsyzy “当然,你也可以把 2b 这两个部分 compose 起来,但这将导致每出现一个略微变动的业务你的 Model 层封装都要添加新的函数或者给函数扩展签名,会导致 Model 层极速膨胀。”

目前我按照 case1 的写法,是有可能出现 model 层膨胀。但是按照 1a,1b 这种调用,感觉就是 orm 的链式调用,直接在 service 层调用 orm 我觉得不是太好....所以我会偏向于写 2a,2b 这种

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/777921

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX