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

ObjectiveSQL 稳定版本发布 1.4.0

  •  2
     
  •   Braisdom ·
    braisdom · 2020-11-30 11:15:39 +08:00 · 3349 次点击
    这是一个创建于 1464 天前的主题,其中的信息可能已经有所发展或是发生改变。

    你可能喜欢他,也有可能讨厌他,就像 Lombok 这种 Java 编程模式一样,也会被很多人拒绝。只不过这次你遇到的不再是处理简单的 setter 和 getter,而是用这种编程风格来解决 ORM 的简单查询和复杂 SQL 编程。

    经过这段时间各位提交的 Bug 和版本的基本特性,发 1.4.0 稳定版本,主要特性如下:

    项目地址: https://github.com/braisdom/ObjectiveSql

    完整特性如下:

    1 简单查询(首先需要定义一个以 DomainModel Annotation 定义的模型)

    @DomainModel
    public class Member {
        private String no;
        @Queryable
        private String name;
        private Integer gender;
        private String mobile;
        private String otherInfo;
    
        @Relation(relationType = RelationType.HAS_MANY)
        private List<Order> orders;
    }
    

    1.1 数据持久化

    Member.create(newMember);
    Member.create(newMember, true); // Create a member without validating
    Member.create(Member.newInstanceFrom(memberHash));
    Member.create(new Member[]{newMember1, newMember2, newMember3}, false);
    
    Member.update(1L, newMember, true); // Update a member with primary key
    Member.update("name = 'Smith => Jackson'", "name = 'Alice'");
    
    Member.destroy(1L); // Delete a member with primary key
    Member.destroy("name = 'Mary'");
    
    // Execute SQL
    Member.execute(String.format("DELETE FROM %s WHERE name = 'Mary'", Member.TABLE_NAME));
    

    1.2 事务处理

    @Transactional
    public static void makeOrder(Order order, OrderLine... orderLines) throws SQLException {
      Order.create(order, false);
      OrderLine.create(orderLines, false);
    }
    

    1.3 查询与统计

    Member.countAll();
    Member.count("id > ?", 1);
    Member.queryByPrimaryKey(1);
    Member.queryFirst("id = ?", 1);
    Member.query("id > ?", 1);
    Member.queryAll();
    

    1.4 分页查询

    Page page = Page.create(0, 10);// Create a Page instance with current page and page size
    PagedList<Member> members = Member.pagedQueryAll(page, Member.HAS_MANY_ORDERS);
    

    1.5 关联对象查询

    // Querying objects with convenient methods, and it will carry the related objects
    Member.queryAll(Member.HAS_MANY_ORDERS);
    Member.queryByPrimary(1, Member.HAS_MANY_ORDERS);
    Member.queryByName("demo", Member.HAS_MANY_ORDERS);
    

    2 复杂 SQL 查询

    Java 代码:

    // SQL programming with Java syntax without losing the features of SQL syntax
    Order.Table orderTable = Order.asTable();
    Select select = new Select();
    
    select.project(sum(orderTable.amount) / sum(orderTable.quantity) * 100)
            .from(orderTable)
            .where(orderTable.quantity > 30 &&
                orderTable.salesAt.between($("2020-10-10 00:00:00"), $("2020-10-30 23:59:59")))
            .groupBy(orderTable.productId);
    

    生成的 SQL 代码:

    -- SQL syntax is the same as Java syntax
    SELECT ((((SUM(`T0`.`amount` ) / SUM(`T0`.`quantity` ) )) * 100))
    FROM `orders` AS `T0`
    WHERE ((`T0`.`quantity` > 30) AND 
           `T0`.`sales_at` BETWEEN '2020-10-10 00:00:00' AND '2020-10-30 23:59:59')
    GROUP BY `T0`.`product_id`
    
    43 条回复    2022-01-20 22:56:01 +08:00
    PopRain
        1
    PopRain  
       2020-11-30 11:32:31 +08:00
    生成的 SQL 最好是参数化查询,这样效率高且可以避免 SQL 注入
    Braisdom
        2
    Braisdom  
    OP
       2020-11-30 11:35:00 +08:00
    是的,SQL 注入是 ObjectiveSQL 下一阶段的重点特性
    GM
        3
    GM  
       2020-11-30 11:58:58 +08:00
    删除使用 destroy 挺反直觉的,毕竟 sql 里删除就是 delete,大部分人平时用的也都是 delete,建议使用 delete
    替换 destroy 。
    beginor
        4
    beginor  
       2020-11-30 12:25:35 +08:00
    @GM 我也是, 在 destroy 那里愣了一下才明白过来。
    beginor
        5
    beginor  
       2020-11-30 12:28:35 +08:00   ❤️ 1
    持续关注楼主的这个项目, 昨天试用了了一下,确实挺不错的。 不过和 c# 的 linq 比起来, 还是差那么点意思, 主要还是受限于 java 的语法。
    Braisdom
        6
    Braisdom  
    OP
       2020-11-30 12:41:27 +08:00
    @GM
    @beginor
    两位,先陈述一下我的设计思路,问题域分两块:SQL 域和 Java 所解决的业务域,在业务域中存在的是对象行为,创建(create),更新(update),和销毁 /删除(destroy),而 SQL 中通常是 Insert, update, delete 。

    我的设计来自于 Rails 的设计,本质上怎么用问题都不大,只是习惯问题而已
    Braisdom
        7
    Braisdom  
    OP
       2020-11-30 12:42:48 +08:00
    @beginor 感谢支持,下一阶段 ObjectiveSQL 项目的主要工作是:数据迁移(Migration) 和 SQL 注入(SQL Injection)
    chinvo
        8
    chinvo  
       2020-11-30 12:47:41 +08:00 via iPhone   ❤️ 1
    虽然不写 Java,但是还是点个赞

    顺道安利一波 .net

    建议对 .net 还挺留在 framework 2.0 时代认知的人了解一下新时代的 .net (core 、5.0 、6.0):开放、高效、易用
    Braisdom
        9
    Braisdom  
    OP
       2020-11-30 12:48:50 +08:00
    @chinvo .NET 的发展过程值得学习
    chinvo
        10
    chinvo  
       2020-11-30 12:51:33 +08:00 via iPhone
    @Braisdom #9 没拥抱开源之前的 .net 也不能说不好,但是真的用不起,Windows Server 授权、Visual Studio 授权、SQL Server 授权……

    拥抱开源之后和 RHEL 合作,在 RH 系上是商业级的支持力度

    不过在我看来 .Net 最强的地方还是 LinQ 和 EF,写业务逻辑最优选择
    Braisdom
        11
    Braisdom  
    OP
       2020-11-30 12:54:48 +08:00
    @chinvo 我之前没有接触过.NET 的 entity framework,也是最近才看到,

    我设计 ObjectiveSQL 的主要目的是为了解决复杂 SQL 的处理,现有的框架维护动态 SQL 太痛苦,时间一长,项目根本无法维护。

    简单 SQL 现有的框架已经做的很不错了,只是顺带把它做了而已
    chinvo
        12
    chinvo  
       2020-11-30 12:59:10 +08:00 via iPhone
    @Braisdom #11 其实我比较好奇,很多语言都有类似 EF/LinQ 的 Orm,Java 生态圈竟然没有么 😂

    我有了解过 Java 的几种 Orm,有一些是 xml 描述( Java 生态圈好像挺依赖 xml 的),有一些甚至还要写“类 SQL”,给我的直观感受就是难用、反人类
    GM
        13
    GM  
       2020-11-30 13:03:23 +08:00
    @chinvo 推荐个应用框架? ABP 太复杂了,感觉不适合技术能力一般的团队用。
    GM
        14
    GM  
       2020-11-30 13:05:27 +08:00
    @chinvo
    真的没有。
    受制于语言表达能力,很多很好用的功能 Java 里就是无法用正常 Java 代码写出来,只能用代码生成器、动态字节码等别扭的方式来实现。
    Braisdom
        15
    Braisdom  
    OP
       2020-11-30 13:05:36 +08:00   ❤️ 1
    @chinvo 我也有同感,我之前写了很多年的 Java,中途写 Ruby 和 Python,也是最近两年才又写 Java,ORM 又是一个系统无法避免的问题,用起来太痛苦,所以才会写 ObjectiveSQL 这个项目的。

    Java LINQ 也有,但很不好用,只能处理一些相对简单的查询,join 子查询,union,复杂表达式等(多层 case when 或者窗口函数等),这些处理起来很不舒服
    chinvo
        16
    chinvo  
       2020-11-30 13:08:46 +08:00 via iPhone
    @GM #13 个人、小团队,直接裸的 Asp.Net MVC 啊。
    GM
        17
    GM  
       2020-11-30 13:16:31 +08:00
    @chinvo ASP.NET MVC 这个只能做个单体应用,想做个分布式的需要自己手撸各种基础设施功能。
    renyijiu
        18
    renyijiu  
       2020-11-30 13:19:17 +08:00
    有个不太相关的问题,是否支持 grpc protobuf ?
    chinvo
        19
    chinvo  
       2020-11-30 13:21:32 +08:00 via iPhone
    @GM #17 分布式(Actor 模型)用 Orleans,多租户用 MapEndpoint,UoW 其实用不着,因为 EF 本身就是 UoW 模型
    Braisdom
        20
    Braisdom  
    OP
       2020-11-30 13:24:01 +08:00
    @renyijiu protobuf 只是一个协议封装,用在数据传输的,现在比较流行,相比很早的 TLV 灵活很多。rpc 只是远程调用,现在微服务里比较流行
    renyijiu
        21
    renyijiu  
       2020-11-30 13:39:01 +08:00
    @Braisdom #20 可能理解不一致,可以直接映射使用 protobuf 生成的类,而不是再自己定一个 model 层的 class
    DoctorCat
        22
    DoctorCat  
       2020-11-30 14:06:22 +08:00
    想起 10 年前用 SSH 的时候,jdbc template 的封装过程
    GM
        23
    GM  
       2020-11-30 15:00:11 +08:00
    @chinvo Orleans 看起来很不错啊,谢谢。另外,UoW 指的是 Unit of Work 吗?
    Braisdom
        24
    Braisdom  
    OP
       2020-11-30 15:31:34 +08:00
    @renyijiu OK,之前是理解有对,这个问题我之前遇到过,ObjectiveSQL 已经兼容了,

    之前我的一个项目是通过 ProtoBuffer 定义的模型,传输的数据极大,但需要直接存储进数据,如果中间再经过一层转换,性能太差,所以我就在 ObjectiveSQL 中设计了 DomainModelDescriptor,用于描述存储数据类型相关的信息,可以直接通过 ObjectiveSQL 进行数据库操作。

    具体你可以参考: https://github.com/braisdom/ObjectiveSql/blob/master/core/src/main/java/com/github/braisdom/objsql/DomainModelDescriptor.java
    gowk
        25
    gowk  
       2020-11-30 16:24:14 +08:00
    @GM
    @chinvo
    ABP 确实对技术能力要求比较高,一般的团队感觉 hold 不住。我也在考虑,一般的中小型项目,怎么流畅的使用.NET 开发应用,专注于业务,有封装好的 boilerplate 推荐吗(不需要前后端分离)
    chinvo
        26
    chinvo  
       2020-11-30 18:05:05 +08:00 via iPhone
    @GM #23
    @gowk #25

    这毕竟是楼主关于自己作品的帖子,咱们持续在这里这样讨论其他话题有点不礼貌,如果要深入讨论 .net 、abp 等问题咱们可以另开一贴
    renyijiu
        27
    renyijiu  
       2020-11-30 18:59:30 +08:00
    @Braisdom #24 感谢,我看看这块
    beginor
        28
    beginor  
       2020-11-30 19:21:10 +08:00
    @Braisdom 请问 objsql 支持动态查询么? 类似这样的 https://blog.jooq.org/tag/dynamic-sql/
    Braisdom
        29
    Braisdom  
    OP
       2020-11-30 21:37:34 +08:00
    JOOQ 称为动态查询,在 ObjectiveSQL 里称为复杂查询,详细请查询:2 复杂 SQL 查询
    Braisdom
        30
    Braisdom  
    OP
       2020-11-30 21:41:41 +08:00
    @beginor

    Order.Table orderTable = Order.asTable();
    Select select = new Select();

    select.project(sum(orderTable.amount) / sum(orderTable.quantity) * 100)
    .from(orderTable)
    .where(orderTable.quantity > 30 &&
    orderTable.salesAt.between($("2020-10-10 00:00:00"), $("2020-10-30 23:59:59")))
    .groupBy(orderTable.productId);

    你可以看一下上述代码在 Jooq 中如果实现
    beginor
        31
    beginor  
       2020-11-30 22:19:22 +08:00 via Android
    @Braisdom 我说的是类似这种查询

    DSLContext ctx = ...;

    SelectConditionStep<?> c =
    ctx.select(T.A, T.B)
    .from(T)
    .where(T.C.eq(1));

    if (something)
    c = c.and(T.D.eq(2));

    Result<?> result = c.fetch()
    beginor
        32
    beginor  
       2020-11-30 22:20:53 +08:00 via Android
    是的,Java 还真没有好用 linq/lambda 框架
    Braisdom
        33
    Braisdom  
    OP
       2020-11-30 22:29:51 +08:00
    @beginor 给你一段项目的代码(计算一个商品销售的同环比),比较复杂:

    你也可以把 SpringBoot 项目运行起来看效果:

    https://github.com/braisdom/ObjectiveSql/blob/master/examples/springboot-sample/src/main/java/com/github/braisdom/objsql/sample/model/Product.java#L45


    DateTime begin = DateTime.parse(rawBegin + " 00:00:00", DATE_TIME_FORMATTER);
    DateTime end = DateTime.parse(rawEnd + " 23:59:59", DATE_TIME_FORMATTER);

    // Creating dataset of target, last period and same period last year
    Select target = createPeriodSales(rawBegin, rawEnd);
    Select lp = createPeriodSales(minusMonths(begin, 1), minusMonths(end, 1));
    Select sply = createPeriodSales(minusYears(begin, 1), minusYears(end, 1));

    Select select = new Select();
    select.from(target)
    .leftOuterJoin(lp, createLPJoinCondition(target, lp))
    .leftOuterJoin(sply, createSPLYJoinCondition(target, sply));

    // Create calculation expression of last period
    Expression lpAmount = createLPExpr(target, lp, "total_amount");
    Expression lpOrderCount = createLPExpr(target, lp, "order_count");
    Expression lpQuantity = createLPExpr(target, lp, "total_quantity");

    // Create calculation expression of same period last year
    Expression splyAmount = createSPLYExpr(target, sply, "total_amount");
    Expression splyOrderCount = createSPLYExpr(target, sply, "order_count");
    Expression splyQuantity = createSPLYExpr(target, sply, "total_quantity");

    select.project(target.col("barcode"))
    .project(target.col("sales_year"))
    .project(target.col("sales_month"))
    .project(formatMoney(lpAmount).as("amount_lp"))
    .project(formatMoney(lpOrderCount).as("order_count_lp"))
    .project(formatMoney(lpQuantity).as("quantity_lp"))
    .project(formatMoney(splyAmount).as("amount_sply"))
    .project(formatMoney(splyOrderCount).as("order_count_sply"))
    .project(formatMoney(splyQuantity).as("quantity_sply"));

    select.groupBy(target.col("barcode"),
    target.col("sales_year"),
    target.col("sales_month"));

    return select.execute(Product.class);
    beginor
        34
    beginor  
       2020-11-30 22:35:19 +08:00 via Android
    @Braisdom 这段代码绝对算是复杂查询, 但是不是我说的动态查询
    beginor
        35
    beginor  
       2020-11-30 22:37:47 +08:00 via Android
    复杂查询和动态查询,是两个不同的概念,我不质疑 objsql 的复杂查询能力,只是想了解下是否支持动态查询
    Braisdom
        36
    Braisdom  
    OP
       2020-12-01 07:54:54 +08:00
    @beginor 所谓动态查询也就是根据不同的参数,join 不同的表,或者选择不同的条件,因为参与拼接的对象都是变量,本身就是动态的。
    beginor
        37
    beginor  
       2020-12-01 08:36:16 +08:00
    我也贴一个常用的 NHibernate 动态查询示例吧, 不知道在 objsql 下如何实现, 对 java 不熟悉, 不敢妄语。

    ```c#
    public void SearchUser(
    string userName,
    int? age
    ) {
    // 以 NHibernate 的动态查询示例
    ISession session = OpenSession();
    IQueryable<User> query = session.Query<User>();
    // 根据参数动态构建表达式树
    if (userName.IsNotNullOrEmpty()) {
    query = query.Where(user => user.UserName.Contains(userName) )
    }
    if (age.HasValue) {
    query = query.Where(user => user.Age >= age);
    }
    // 可以先根据构造好的表达式树进行 Count 查询
    long userCount = query.LongCount();
    // 也可以继续添加其它表达式,并查询结果
    IList<User> users = query.OrderBy(user => user.Id)
    .Select(user => new User { Id = user.Id, UserName = user.UserName })
    .ToList();
    }
    ```

    PS: 丝毫没有秀 c# 优越感的意思, 我只是好奇是否支持这种动态查询。
    beginor
        38
    beginor  
       2020-12-01 08:42:41 +08:00
    回复的格式有点儿乱, 可以看这个 gist https://gist.github.com/beginor/4bc9bfd25dfd9f488156cf4975b707f6
    Braisdom
        39
    Braisdom  
    OP
       2020-12-01 10:10:01 +08:00
    我也在 gist 里回复了。

    public List<User> searchUser(String name, Integer age) {
    User.Table user = User.asTable();
    Select select = new Select();
    LogicalExpression predicate = new PolynaryExpression(EQ, $("1"), $("1"));

    if(StringUtils.isNotBland(name)) {
    predicate.and(user.name.eq(name));
    }

    if(age > 0) {
    predicate.and(user.age.eq(age));
    }

    return select.orderBy(user.id.asc()).execute(User.class);
    }
    zhangysh1995
        40
    zhangysh1995  
       2020-12-01 14:55:15 +08:00
    性能咋样?看到过论文专门研究 ORM 性能问题的,有些会导致产生的 SQL 很慢?我对这个方向挺有兴趣的,我发个邮件聊聊呗。
    Braisdom
        41
    Braisdom  
    OP
       2020-12-01 15:00:12 +08:00   ❤️ 1
    @zhangysh1995 [email protected]
    ORM 性能问题不是慢 SQL,而是在大规模写和读时的性能,这块我还在优化,和 MyBatis 的性能差不多,但离 JDBC 原始 SQL 还是有差距。
    zhangysh1995
        42
    zhangysh1995  
       2020-12-01 15:01:25 +08:00
    @Braisdom 邮件已发,坐等回复。
    xiaohuya
        43
    xiaohuya  
       2022-01-20 22:56:01 +08:00
    太像 rails 了,赞
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3508 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 04:49 · PVG 12:49 · LAX 20:49 · JFK 23:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.