V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
waibunleung
V2EX  ›  程序员

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

  •  1
     
  •   waibunleung · 2021-05-19 15:40:17 +08:00 · 2612 次点击
    这是一个创建于 1285 天前的主题,其中的信息可能已经有所发展或是发生改变。

    事情是这样的,在有了 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 再封装的话,怎么样才比较符合认知?

    求各位赐教!

    21 条回复    2021-05-21 11:05:00 +08:00
    ChoateYao
        1
    ChoateYao  
       2021-05-19 15:49:30 +08:00
    拥有 ORM 只需要封装查询,增、删、改不需要封装到数据访问层里面。

    毕竟增、删、改这些业务逻辑比较固定且一般都是面向对象操作。但是查会根据不同的条件查询,这里面变化比较多,所以封装到数据访问层里面方便后期迭代优化。
    no1xsyzy
        2
    no1xsyzy  
       2021-05-19 15:59:26 +08:00
    第二种简直是格林斯潘第十定律……
    waibunleung
        3
    waibunleung  
    OP
       2021-05-19 16:28:09 +08:00
    @no1xsyzy 没太 get 到这个点
    waibunleung
        4
    waibunleung  
    OP
       2021-05-19 16:36:49 +08:00
    @ChoateYao 那删除是直接在 service 层调用?但是我想问的问题不是这个,而是哪种封装更合适一点?
    我觉得,像第一种的那样,我明确告诉你就是用了添加用户姓名的,不满足你的直接再封装一个,这样的界限明确。
    第二种这样,半通用半不通用的风格就比较难受,你说我要扩展的时候,是直接改你的,还是直接封装个新的?而且 新增__insert 这样的映射,有了 orm 还搞一套命令映射,我有点不理解....
    ChoateYao
        5
    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
        6
    waibunleung  
    OP
       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
        7
    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
        8
    KouShuiYu  
       2021-05-19 18:54:25 +08:00
    https://github.com/ckpack/pg-helper
    要不借鉴借鉴我之前写的,单表操作还是挺嗨的🐶
    KouShuiYu
        9
    KouShuiYu  
       2021-05-19 18:59:54 +08:00
    waibunleung
        10
    waibunleung  
    OP
       2021-05-19 19:26:28 +08:00
    @no1xsyzy 感觉有点误解了。这个 xxx 代表的是 user_model 之类的数据访问层的意思,方法的目的是为用户增加昵称,那放在 user_model 里面这个问题不大。
    数据访问分层没有强调降低复杂性,而是强调分层,职责分离
    no1xsyzy
        11
    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
        12
    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
        13
    waibunleung  
    OP
       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
        14
    waibunleung  
    OP
       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
        15
    waibunleung  
    OP
       2021-05-20 10:26:30 +08:00
    @bsg1992 如果说两种都不合适的话,想请教一下,你们项目里面是怎么处理数据获取的逻辑的呢?方便的话希望能提供个简单的 demo,感谢~
    no1xsyzy
        16
    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
        17
    waibunleung  
    OP
       2021-05-20 14:49:21 +08:00
    @no1xsyzy 正确案例 404 了,不知道是不是仓库没开公共权限的原因,看不到(哭
    waibunleung
        18
    waibunleung  
    OP
       2021-05-20 14:55:16 +08:00
    @no1xsyzy 你说的错误案例,这个关系怎么理解呢?另外,感觉你说的错误案例和我的第一个 case 挺像的啊,“把 SQLite 当 kv store,才能这么玩” ,如果没有当成这样,那应该是怎么样玩?我觉得这样玩也没什么问题啊...

    还望能提点一二
    no1xsyzy
        19
    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
        20
    waibunleung  
    OP
       2021-05-21 10:30:16 +08:00
    @no1xsyzy “当然,你也可以把 2b 这两个部分 compose 起来,但这将导致每出现一个略微变动的业务你的 Model 层封装都要添加新的函数或者给函数扩展签名,会导致 Model 层极速膨胀。”

    目前我按照 case1 的写法,是有可能出现 model 层膨胀。但是按照 1a,1b 这种调用,感觉就是 orm 的链式调用,直接在 service 层调用 orm 我觉得不是太好....所以我会偏向于写 2a,2b 这种
    no1xsyzy
        21
    no1xsyzy  
       2021-05-21 11:05:00 +08:00
    Service 层直接用 ORM 有什么不好的呢?

    数据分层的目的是避免底层数据结构变动导致多处代码同时修改,你要在 ORM 上套一层,是因为表结构时刻在变吗?
    如果数据库表的定义不受控制,那没什么问题,但通常不会有这种情况吧。
    或者在 SQL 基础上封装得了(

    (我从每个地方手写 sql 到半自动组装 sql 到每个行动添加一个 Model 层到 ORM 框架这条路重新走了一遍,我是不太想回去)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5615 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 07:07 · PVG 15:07 · LAX 23:07 · JFK 02:07
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.