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

我用 Golang 写了个增删查改框架 GoooQo,正在尝试做一些推广,欢迎给点意见

  •  
  •   f0rb · 70 天前 · 1723 次点击
    这是一个创建于 70 天前的主题,其中的信息可能已经有所发展或是发生改变。

    GoooQo 是一个基于 OQM ( Object Query-Language Mapping )技术的框架。

    OQM 技术与传统的 ORM ( Object Relational Mapping )技术的最大区别是,OQM 技术提出通过对象直接构造各种增删查改语句。项目目前还是一个 MVP ,已经完成了基于字段后缀推导查询条件的功能,例如通过字段名称ageGt推导出对应的查询条件为age > ?。并且围绕数据库的增删查改接口构建了一个简单的 RESTFul 模块提供 Web 的 API 。

    目前打算通过撰写文章和 Demo ,尝试在掘金、HelloGitHub 、Hacker News 、Reddit 、Product Hunt 等平台进行一些推广。

    掘金新发的文章今天在反馈页吐槽了一下展现数的问题,编辑提了点修改意见后就给推荐上了首页,到现在有 6000 多的展现量了。其他的,HN 的帖子太容易沉了,Reddit 感觉很容易被 downvotes 然后被屏蔽,所以来请教下大家有没有什么好的推广方法。

    掘金文章: https://juejin.cn/post/7406900130764881972

    GitHub 地址: https://github.com/doytowin/goooqo

    Demo 地址: https://github.com/doytowin/goooqo-demo

    30 条回复    2024-09-20 22:21:45 +08:00
    wkong
        1
    wkong  
       70 天前
    👍
    f0rb
        2
    f0rb  
    OP
       70 天前
    我感觉国外一些开发一看到 ORM 几个字母就 PTSD 了,也不看内容,一上来就攻击你
    bunny189
        3
    bunny189  
       70 天前
    非常好的
    VVVYGD
        4
    VVVYGD  
       70 天前
    非常好, 之前我也写过一个类似的,不过用于 document 数据库 https://github.com/ddx2x/crossgate
    matrix1010
        5
    matrix1010  
       70 天前
    Go 的数据库查询一般分 2 个流派: 代码生成和 SQL builder. 代码生成很相当于你这个的强化版,缺点在于修改 schema 要重跑而且代码多。另一方面 SQL builder 的好处则是简洁,适合简单的 CRUD 。

    代码生成的 type 一般是完全安全的,而 SQL builder 的安全性会低一些,比如`func (c *Cond) In(field string, value ...interface{}) `. 但相比你这个完全手写还是更安全些。
    f0rb
        6
    f0rb  
    OP
       70 天前
    @matrix1010 我这里一张表只需要编写实体对象和查询对象两个结构体,根据查询对象字段的名称生成查询条件,生成方式有两种,一种是通过反射读取字段信息,生成查询条件,一种是通过 gen 包为查询对象生成一堆 if 来拼接查询条件,比如这个 https://github.com/doytowin/goooqo/blob/main/main/user_query_builder.go 。 除了这两个结构体和两行构建代码,其他代码全部都封装了。
    没明白你这里说的完全手写是什么意思。

    我这里的代码生成是基于查询对象生成 if 构建语句,而不是从表生成实体对象。
    f0rb
        7
    f0rb  
    OP
       70 天前
    @VVVYGD 我这里面也有个 MongoDB 的模块,这次文章里没提,用的是代码生成的方法,比如为 QtyGt 生成这样的代码 if q.QtyGt != nil {
    d = append(d, D{{"qty", D{{"$gt", q.QtyGt}}}})
    }
    完整的在这里:
    https://github.com/doytowin/goooqo/blob/main/main/inventory_query_builder.go
    f0rb
        8
    f0rb  
    OP
       70 天前
    @VVVYGD 我看到你这个是 rust 写的,我是先用 Java 写了一个,然后写专利的时候写了个 Go 的,来验证 OQM 这个 idea 是不是能适用所有语言
    wwhontheway
        9
    wwhontheway  
       69 天前
    请教一个问题,对于文档数据库,有没有类似 go-gorm/gen 的工具,可以生成 sql
    wwhontheway
        11
    wwhontheway  
       69 天前
    wwhontheway
        12
    wwhontheway  
       69 天前
    @wwhontheway gorm/gen 只有一个 model struct 就可以了
    f0rb
        13
    f0rb  
    OP
       69 天前
    @wwhontheway intentory.go 里就包含了增删查改的全部代码

    Gorm 只能基于 model 构造比较符是’=‘的查询条件:

    db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;

    非=的查询条件需要手搓:

    db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
    // SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

    而使用 GoooQo ,你只需要定义好查询对象,
    type UserQuery struct {
    PageQuery
    NameNe *string
    AgeGt *int
    }

    然后基于查询参数构造实例就好了
    userDataAccess.Query(ctx, &UserQuery{NameNe: PStr("f0rb"), AgeGt: PInt(20)})

    这就是 OQM 技术中通过后缀来推导字段对应的查询条件。查询对象专注于构建查询子句,符合单一职责原则。

    好处就是,前端向后端传递查询参数时,可以通过反射自动构建查询对象的实例。而当需求变更需要增减查询条件时,只需要在查询对象里增减字段即可。
    f0rb
        14
    f0rb  
    OP
       69 天前
    @wwhontheway 传统的 ORM 概念里,使用实体构建一些使用等号为比较符的查询条件,使得实体对象额外承担了构建查询子句的责任,显然不符合单一职责原则。

    实体对象的字段只能对应一个列名,如 age ,但是查询子句可能会包含同一列的多个查询条件,如 age > ? and age < ?,这就是使得实体对象无法同时兼顾列名的映射和查询条件的映射。

    而单独使用查询对象构建查询子句就不存在这样的限制,例如
    type UserQuery struct {
    PageQuery
    Name *string
    NameNe *string
    Age *int
    AgeLt *int
    AgeGt *int
    }

    注意这里字段的类型都是指针,在构建查询子句时,只为不为 nil 的字段构建查询条件和参数。
    VVVYGD
        15
    VVVYGD  
       69 天前
    @f0rb 是的,我用 rust 写的并加入了查询谓词解析,例如查询 ab=123 & abc='123' 这样的,你可以参考里面的 yacc 在 go 里使用。
    VVVYGD
        18
    VVVYGD  
       69 天前
    @f0rb 汗,按个回车就回复了,我这里抽象 Condition<Filter> ,然后把 ab=123 & abc='123' 这种语法转成 mongo db 的 filter, 实现了大部份的逻辑,我小团队业务中用还是挺好用的,你可以看看。
    f0rb
        19
    f0rb  
    OP
       68 天前
    @VVVYGD 我看了一下例子,是用 format!("a > 1");转成 ("a":"$gt",1) ? 这还是要拼字符串吧?
    我这儿的方案是把列名和谓词合二为一,把原来需要 3 个元素的查询条件变成了两个,变成键值对的形式,
    比如 aGt = 1 ,既能表示为 a > 1 ,也能表示为("a":"$gt",1) ,相当于直接把 OO 编程语言翻译成不同数据库的查询语言了,不需要额外去拼接字符串
    f0rb
        20
    f0rb  
    OP
       68 天前
    @VVVYGD 我这个方案是重构来的,一开始也是拼字符串,不过是在 Java 的注解里,比如这样
    public class UserQuery {
    @QueryField(and = "username Like CONCAT('%', ?, '%')")
    private String usernameLike;
    }
    这种写法遇到 IN 查询的时候有个问题,就是不能提前知道数组里元素的个数来确定占位符的个数
    后面发现,字段名称里面已经包含列名和谓词两个要素了,剩下的占位符直接根据赋值去确定
    然后就确定了这种通过后缀来推导查询条件的方案。
    因为通过重构把重复的代码都进行了封装,所以代码量是最少的
    VVVYGD
        21
    VVVYGD  
       56 天前
    差不多吧, a > 1 会转换成 yacc 语法解析成 field(a, gt,1). ->. mongo 的实现者转换成 真实的 {$gt:{a:1}}
    VVVYGD
        22
    VVVYGD  
       56 天前
    @f0rb 例如 :a=1&&b=2||c=1&&b=2&&abc='abc21' 转换成的就是
    MongoFilter(Document({"$or": Array([Document({"$and": Array([Document({"a": Document({"$eq": Int64(1)})}), Document({"b": Document({"$eq": Int64(2)})})])}), Document({"$and": Array([Document({"c": Document({"$eq": Int64(1)})}), Document({"$and": Array([Document({"b": Document({"$eq": Int64(2)})}), Document({"abc": Document({"$eq": String("abc21")})})])})])})])})


    参考 https://github.com/ddx2x/crossgate/blob/master/src/store/condition.rs 测试样例
    f0rb
        23
    f0rb  
    OP
       56 天前
    @VVVYGD 不一样哦,a > 1 对应 field(a, gt,1) 需要 3 个参数,而 aGt 只需要一个参数,值是跟着参数传进来
    前后端传参也不一样的方式也不一样,aGt=1 对应 json 是{"aGt":1}, 或者是?aGt=1&b=2, 不太清楚你这个前端参数是怎么传的,我这也有个构造或语句的例子: https://juejin.cn/post/7409608924054700047
    f0rb
        24
    f0rb  
    OP
       56 天前
    @VVVYGD 而且使用字符串不利于 IDE 的代码检查还有代码重构和复用
    VVVYGD
        25
    VVVYGD  
       55 天前
    @f0rb 其实就是类似 SQL 解析,这里 https://github.com/ddx2x/crossgate/blob/master/condition/src/cond.rs
    https://github.com/ddx2x/crossgate/blob/master/condition/src/cond.y 将语法树解析为 Expr ,然后将解析的 Expr 转换成 mongo 的谓词条件 https://github.com/ddx2x/crossgate/blob/master/src/store/mongo/filter.rs ,当然其实这个过程直接转换成 mongo SDK 的对象 doc. 例如原本 sql:select username ,amount from users where local="abc" and(xx=1 or
    b =2 and c=3) 我这边的使用写法是 new_condition().withTable("users").fields(["username","amount"]).wheres(#b" local="abc" && (xx=1 || b=2 && c=3)")
    VVVYGD
        26
    VVVYGD  
       55 天前
    @f0rb 因为这个是转换成 Expr 的,在 rust 里是可以做前置检查的。
    f0rb
        27
    f0rb  
    OP
       55 天前
    @VVVYGD 所以你这里每条查询都需要构造一次类似这样的条件是吧:#b" local="abc" && (xx=1 || b=2 && c=3)",那你这种怎么重用了,到处都是字符串吗?你这里不过是把 AND 换成&&,OR 换成||了,别的和 SQL 有什么区别呢?你并没有造出个新轮子,只是给旧轮子涂了层新漆。

    你要考虑到做一个信息系统, 前端都会有多个输入框由用户填写查询条件进行搜索,而多个查询条件是可以任意组合的,只能靠 if 语句判断用户是否填写了某个条件来决定是否拼接对应的条件,不知道这种场景你是怎么处理的。

    pub enum MongoOp {
    Eq,
    Gt,
    Gte,
    Lt,
    Lte,
    Ne,
    Like,
    NotLike, // { item: { $not: { $regex: "^p.*" } } }
    In,
    NotIn,
    }

    我看你这里都定义了这些谓词了,你直接把列名和他们拼接成 ageGt 这样,再解析成条件岂不是方便多了。
    f0rb
        28
    f0rb  
    OP
       55 天前
    @VVVYGD "其实就是类似 SQL 解析"
    看懂了,我做的东西和你不一样,
    你做的是 SQL 转 MongoDB
    我做的是对象转 SQL 或者 MongoDB 或者其他 NoSQL 语言
    VVVYGD
        29
    VVVYGD  
       47 天前
    @f0rb 是的,这里用了一种类 SQL 的方式去转成 mongo 的查询,也可以实现其他的的 nosql 查询,虽然我也不太明确还有哪些语法,哈哈。
    f0rb
        30
    f0rb  
    OP
       45 天前
    @VVVYGD 是啊,因为所有的查询本质上都是两个值进行比较嘛,只不过不同的语言用的语法不一样
    我这里是把比较语法集成到面向对象语言的字段名称上了
    一般人有点不理解这个思路,所以感觉好难推广
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   987 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 22:12 · PVG 06:12 · LAX 14:12 · JFK 17:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.