百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Java vs SQL:数据业务场景下谁才是最优选项?

liuian 2025-03-12 16:41 8 浏览

应用中的数据业务通常涉及持久化数据的访问、数据计算和流程处理。数据库中的持久化数据可以用 SQL 计算,存储过程的 loop/if 语句可以进行流程处理,JDBC(含 ODBC)可以让 SQL 和应用集成,所以复杂 SQL(含存储过程)常用于数据业务开发。

但是,复杂 SQL 深度绑定数据库,存在架构上的缺陷,不能满足现代应用的要求。

复杂 SQL(及存储过程)的缺陷

难以扩展

用复杂 SQL 或存储过程实现数据业务时,压力会集中在数据库上。而数据库无法被成熟框架集成,无法利用框架实现高可用性和易扩展性,本身扩展时无论横向还是纵向的成本都很高,不符合现代应用的建设理念。

代码可移植性差

简单 SQL 通用性强,容易在数据库间移植,复杂 SQL 则不然。复杂 SQL 经常用到绑定数据库的特殊语法(含函数),代码就很难移植;存储过程甚至没有统一的标准,互相差异更大,移植也更加困难。

耦合性高

数据业务是为应用服务的,最好是处于应用内部,但存储过程存在于数据库,两者耦合性过高。数据库通常是共享的,还会造成应用间的耦合。

为了弥补复杂 SQL 的缺陷,很多应用开始使用 Java+ 简单 SQL 实现数据业务。主要有两类技术可选择:ORM 和 Stream,ORM 技术以 Hibernate 和 JOOQ 为主;Stream 是 Java8 开始提供的类库,在此基础上又发展出 Kotlin。大量的数据计算和处理压力由应用承担,Java 程序很容易被成熟框架集成,进行低成本的扩展。ORM 和 Stream 负责数据计算,基础 Java 语言负责流程处理,同为 Java 代码,移植性非常好。这种方式的耦合性也很低,数据库仅用作存储,数据业务全部集中于应用,可单独维护,不同应用间的数据业务天然隔离。

相比复杂 SQL,Java+ 简单 SQL 可以得到较好的架构优势,但也带来了新的缺陷。

Java+ 简单 SQL 的缺陷

计算能力弱导致开发困难

Hibernate 的计算能力远不如 SQL,很多简单计算都无法用 Hibernate 描述,包括 from 子查询、涉及行号的计算等。比如 SQL 很容易实现的 from 子查询:

select orderId, m from (select orderId, month(orderDate) m from OrdersEntity) t1

复杂些的计算 Hibernate 更加无法描述,比如 Oracle SQL 用窗口函数计算各组前 3 名

select * from (
select *, row_number() over (partition by Client order by Amount) rn from Orders) T where rn<=3

很多基础的日期函数和字符串函数 Hibernate 都不支持,包括日期增减、求年中第几天、求季度数,以及替换、left 截取、求 ASCII 码等。以日期增加为例:

select date_add(OrderDate,INTERVAL 3 DAY) from OrdersEntity

想实现类似的功能,只有两种办法,引入方言 SQL,或者用 Java 硬编码。前者绑定数据库,代码难以移植,偏离了 ORM 的初衷,后者代码量巨大。

JOOQ 需要程序员先设计好 SQL,再把 SQL 翻译成 JOOQ 代码,最后由引擎把 Java 代码解析成 SQL 去执行,想获得接近方言 SQL 的计算能力,就要大量使用绑定数据库的 JOOQ 函数,但这样并没有解决架构上的缺陷;想弥补架构上的缺陷,就要尽量使用通用的 JOOQ 函数,计算能力又将大幅下降。Java 语法不适合表达 SQL,为了正确表达,JOOQ 经常对函数过度封装,代码比 SQL 复杂,实际的计算能力低于 SQL。

比如,各组前 3 名:

//等价的SQL见前文,绑定Oracle的JOOQ如下
WindowDefinition CA = name("CA").as(partitionBy(ORDERS.CLIENT).orderBy(ORDERS.AMOUNT));
context.select().from(select(ORDERS.ORDERID,ORDERS.CLIENT,ORDERS.SELLERID,ORDERS.AMOUNT,ORDERS.ORDERDATE,rowNumber().over(CA).as("rn")).from(ORDERS).window(CA) ).where(field("rn").lessOrEqual(3)).fetch();


明显要复杂很多。

Stream 提供了流式编程风格、Lambda 语法、集合函数,可以对简单类型(数字、字符串、日期)的集合进行简单计算,但 Stream 是通用的底层工具,在记录集合方面还不够专业,计算能力远低于 SQL。很多基本计算 Stream 实现起来都很困难,比如分组汇总:

//等价的SQL:
select year(OrderDate), sellerid, sum(Amount), count(1) from Orders group by year(OrderDate), sellerid
//Stream:
Calendar cal=Calendar.getInstance();
Map c=Orders.collect(Collectors.groupingBy(
        r->{
            cal.setTime(r.OrderDate);
            return cal.get(Calendar.YEAR)+"_"+r.SellerId;
            },
            Collectors.summarizingDouble(r->{
                return r.Amount;
            })
        )
);
    for(Object sellerid:c.keySet()){
        DoubleSummaryStatistics r =c.get(sellerid);
        String year_sellerid[]=((String)sellerid).split("_");
        System.out.println("group is (year):"+year_sellerid[0]+"\t (sellerid):"+year_sellerid[1]+"\t sum is:"+r.getSum()+"\t count is:"+r.getCount());
    


Kotlin 对 Stream 进行了改进,Lambda 表达式更加简洁,集合函数更加丰富,另外增加了热情集合计算(Eager Evaluation,与 Stream 的惰性集合计算 Lazy evaluation 相对)。但 Kotlin 也是通用的底层工具,设计目标是简单集合的计算,在记录集合方面还不够专业,计算能力依然远低于 SQL。比如基本的分组汇总:

data class Grp(var OrderYear:Int,var SellerId:Int)
data class Agg(var sumAmount: Double,var rowCount:Int)
var result=Orders.groupingBy{Grp(it.OrderDate.year+1900,it.SellerId)}
    .fold(Agg(0.0,0),{
        acc, elem -> Agg(acc.sumAmount + elem.Amount,acc.rowCount+1)
    })
.toSortedMap(compareBy { it. OrderYear}.thenBy { it. SellerId})
result.forEach{println("group fields:${it.key.OrderYear}\t${it.key.SellerId}\t aggregate fields:${it.value.sumAmount}\t${it.value.rowCount}") }


Hibernate、JOOQ、Stream、Kotlin 这些类库之所以计算能力不足,根本原因在于它们的宿主语言是静态的编译型语言,很难支持动态数据结构,表达能力受到极大限制。像 JOOQ 这种勉强支持动态数据结构的类库,必须写成静态代码 + 动态代码(字符串)混合的形式,如 T2.field("continuousdays"),业务数据只要稍显复杂,编码难度就会陡增。SQL 之所以计算能力强,根本原因在于它是动态的解释型语言,天生支持动态数据结构,表达能力的上限较高。

难以热部署导致运维复杂

编译型语言不支持热部署,修改代码后经常需要重新编译并重启应用,系统安全较差,运维复杂度较高。

esProc SPL 解决一切

实现数据业务,还有一个更好的选择:esProc SPL+ 简单 SQL。

esProc SPL 是 Java 下开源的数据处理引擎,基本功能可涵盖数据业务的每个阶段,SPL 本身具有数据计算和流程处理能力,简单 SQL 负责读写数据库,前端 Java 代码通过 JDBC 调用 SPL。

读写数据库。SPL 提供了 query 函数执行 SQL,用来将数据库的查询读为内部的序表(SPL 的结构化数据对象)。

T=db.query("select * from salesR where SellerID=?",10)

SPL 提供 update 函数将序表保存到数据库,SPL 引擎会对比修改前后的数据,自动解析为不同的 SQL DML 语句(insert、delete、update)并执行。比如,原序表为 T,经过增删改之后的序表为 NT, 将变化结果持久化到数据库:

db.update(NT:T,sales;ORDERID)

数据计算。基于序表,SPL 提供了丰富的计算函数。

过滤:T.select(Amount>1000 && Amount<=3000 && like(Client,"*bro*"))

排序:T.sort(-Client,Amount)

去重:T.id(Client)

汇总:T.max(Amount)

关联:join(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))

流程处理。类似 Java 的 for/while/if 和存储过程的 loop/if 语句,SPL 提供了完整的流程控制能力。分支判断语句:


A

B

2


3

if T.AMOUNT>10000

=T.BONUS=T.AMOUNT*0.05

4

else if T.AMOUNT>=5000 && T.AMOUNT<10000

=T.BONUS=T.AMOUNT*0.03

5

else if T.AMOUNT>=2000 && T.AMOUNT<5000

=T.BONUS=T.AMOUNT*0.02

循环语句:


A

B

1

=db=connect("db")


2

=T=db.query@x("select * from sales where SellerID=? order by OrderDate",9)

3

for T

=A3.BONUS=A3.BONUS+A3.AMOUNT*0.01

4


=A3.CLIENT=CONCAT(LEFT(A3.CLIENT,4), "co.,ltd.")

5


 …

JDBC 接口。SPL 编写的数据业务代码可以保存在脚本文件中,Java 通过 JDBC 接口引用脚本文件名,形同调用存储过程。

Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local:// ");
CallableStatement statement = conn.prepareCall("{call InsertSales(?, ?,?,?)}");
statement.setObject(1, d_OrderID);
statement.setObject(2, d_Client);
statement.setObject(3, d_SellerID);
statement.setObject(4, d_Amount);
statement.execute();


除了这些基础能力外,SPL+ 简单 SQL 还能克服复杂 SQL 和 Java+ 简单 SQL 的各种缺陷,扩展成本低、代码可移植性好,耦合性低、计算能力强、支持热部署。

扩展简单

用 SPL 实现数据业务,压力会集中在 SPL 上。作为 Java 类库,SPL 可以无缝被成熟的 Java 框架集成,便于横向扩展。

代码可移植好

SPL+ 简单 SQL 实现数据业务时,代码集中在数据计算和流程处理,这部分由 SPL 实现。SPL 与数据库无关,代码可在数据库间无缝移植。读写数据库由简单 SQL 实现,不涉及方言 SQL,移植起来也很方便。

SPL 的初衷之一就是便于移植,为此提供了许多工具。SPL 鼓励通过数据源名取数,移植时只要修改配置文件,不必修改代码。SPL 支持动态数据源,可通过参数或宏切换不同的数据库,从而进行更方便的移植。SPL 还提供了与具体数据库无关的标准 SQL 语法,使用 sqltranslate 函数可将标准 SQL 转为主流方言 SQL,仍然通过 query 函数执行。

耦合性低

数据库只负责存储,不负责数据业务。数据业务由 SPL+ 简单 SQL 实现,与应用处于同一位置。当数据业务发生变化时,只要修改应用中的代码,不必维护数据库,两者耦合度低。SPL 是普通的 Java 类库,可部署在不同应用中,应用间天然隔离。

计算能力强

SPL 提供了丰富的计算函数,可以用直观简短的代码实现 SQL 式计算:

子查询:Orders.new(OrderId,month(OrderDate):m).new(OrderId,m)

分组汇总: T.groups(year(OrderDate),Client; avg(Amount):amt)

各组前 3 名:Orders.groups(Client;top(3,Amount))

SPL 支持有序计算、集合计算、分步计算、关联计算,适合简化复杂的数据计算,计算能力超过 SQL。比如,最大连续上涨天数:


A

1

=orcl.query@x(select price from stock order by transDate)

2

=t=0,A1.max(t=if(price>price[-1],t+1,0))

再比如,找出司公中与其他人生日相同的员工:


A

1

=mysql5.query(“select * from emp”).group(month(birthday),day(birthday))

2

=A1.select(~.len()>1).conj()

SPL 还提供了更丰富的日期和字符串函数,在数量和功能上超过 Java 计算类库和 SQL。

值得一提的是,为了进一步提高开发效率,SPL 还创造了独特的函数语法。

支持热部署

SPL 是解释型语言,代码以脚本文件的形式外置于 JAVA,无须编译就能执行,脚本修改后立即生效,支持不停机的热部署,适合变化的业务逻辑,运维复杂度低。

SPL 还有其他优点:支持全功能调试,包括断点、单步、进入、执行到光标等;代码通常是脚本文件的形式,可存储于操作系统目录,方便进行团队代码管理;SPL 代码不依赖 JAVA,数据业务和前端代码物理分离,代码耦合性低。

SPL已开源免费,欢迎前往乾学院了解更多!

相关推荐

那些Java架构师必知必会的技术

Java基础#Java对象的内存布局MapStruct解了对象映射的毒周末我把HashMap源码又过了一遍Java7和Java8中的ConcurrentHashMap原理解析Java中自定...

Java内存泄漏最全详解(6大原因及解决方案)

大家好,我是mikechen。内存泄漏是经常出现的线上故障,也是大厂面试经常考察的,下面我就全面来详解内存泄漏以及解决方案@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题...

都说Feign是RPC,没有侵入性,为什么我的代码越来越像 C++

1.概览随着SpringCloud的流行性,Feign已经成为RPC的事实标准,由于其构建与Http协议之上,对请求和返回值缺少规范约束,在日常开发过程中经常由于设计不当对系统造成一...

面试题系列-java后端面试题List 和 Set 的区别

List和Set的区别List,Set都是继承自Collection接口List特点:元素有放入顺序,元素可重复,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(...

一直不理解为什么在重写equals方法时都要重写hashCode方法

为什么在重写equals方法时都要重写hashCode方法呢?首先jdk的要求是这样的,equals与hashcode间的关系:1、如果两个对象相同(即用equals比较返回true),那么它们的ha...

翻了ConcurrentHashMap1.7 和1.8的源码,我总结了它们的主要区别

ConcurrentHashMap思考:HashTable是线程安全的,为什么不推荐使用?HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次...

10 个经典的 Java 集合面试题,看你能否答得上来?

来自:evget.com/article/2014/11/27/21869.html这里有10个经典的Java面试题,也为大家列出了答案。这是Java开发人员面试经常容易遇到的问题,相信你了解和掌握之...

MyBatis3.5.11-从入门到高阶

一.课程介绍MyBatis概述MyBatis基础应用MyBatis高级MyBatis进阶二.MyBatis概述1、为什么需要MyBatis在我们程序中,运行时期产生的数据都是存放在内存中的,那么在内存...

灵魂拷问:如何检查 Java 数组中是否包含某个值?

作者|沉默王二责编|Elle在逛programcreek的时候,我发现了一些专注细节但价值连城的主题。比如说:如何检查Java数组中是否包含某个值?像这类灵魂拷问的主题,非常值得深入地研...

Java后端学习路线是什么?

关于Java后端(SpringBoot为主)学习路线:一、Java基础阶段编程语言基础掌握基本数据类型(如int、double、char等)、变量、常量的定义和使用。理解运算符(算术、关...

Mybatis配置文件XML全貌详解,再不懂我也没招了

一、为什么要使用配置文件试想,如果没有配置文件,我们的应用程序将只能沿着固定的姿态运行,几乎不能做任何动态的调整,那么这不是一套完美的设计,因为我们希望拥有更宽更灵活的操作空间和更多的兼容度,同时也能...

软件性能调优全攻略:从瓶颈定位到工具应用

性能调优是软件测试中的重要环节,旨在提高系统的响应时间、吞吐量、并发能力、资源利用率,并降低系统崩溃或卡顿的风险。通常,性能调优涉及发现性能瓶颈、分析问题根因、优化代码和系统配置等步骤,调优之前需要先...

你还在使用Guava的Lists.newArrayList()吗

Guava说起Guava,做Java开发的应该没人不知道吧,毕竟“google出品,必属精品”。虽然应该没有Spring那样让Javaer无法避开,但是其中很多工具类的封装还是让人欲罢不能。而我们今天...

JDK成长记7:3张图搞懂HashMap底层原理

HashMap基本原理和优缺点HashMap基本原理和优缺点一句话讲,HashMap底层数据结构,JDK1.7数组+单向链表、JDK1.8数组+单向链表+红黑树。HashMap的3个底层原理Hash...

如何深度理解mybatis?

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...