Java真正智慧的ORM框架,融合JPA功能和最佳的sql编写及查询模式、独创的缓存翻译、最优化的分页、并提供无限层级分组汇总、同比环比、行列转换、树形排序汇总、sql自适配不同数据库、分库分表、多租户、数据加解密、脱敏以及面向复杂业务和大规模数据分析等痛点、难点问题项目实践经验分享的一站式解决方案!
https://github.com/sagframe/sqltoy-online-doc/blob/master/docs/sqltoy/search.md
<dependency>
<groupId>com.sagframe</groupId>
<artifactId>sagacity-sqltoy-spring-starter</artifactId>
<!-- solon 适配版本 <artifactId>sagacity-sqltoy-solon-plugin</artifactId> -->
<!-- 传统spring项目 <artifactId>sagacity-sqltoy-spring</artifactId> -->
<!-- 单纯sqltoy <artifactId>sagacity-sqltoy</artifactId> -->
<!-- jdk8 对应的版本号为:5.6.33.jre8 -->
<version>5.6.33</version>
</dependency>
sqltoy-orm是JPA和超强查询的融合体,是简单业务、大型SaaS化多租户ERP、大数据分析多种类型项目实践过程的总结和分享。
//三步曲:1、quickvo生成pojo,2、完成yml配置;3、service中注入dao(无需自定义各种dao)
@Autowired
LightDao lightDao;
StaffInfoVO staffInfo = new StaffInfoVO();
//保存
lightDao.save(staffInfo);
//删除
lightDao.delete(new StaffInfoVO("S2007"));
//public Long update(Serializable entity, String... forceUpdateProps);
// 这里对photo 属性进行强制修改,其他为null自动会跳过
lightDao.update(staffInfo, "photo");
//深度修改,不管是否null全部字段修改
lightDao.updateDeeply(staffInfo);
List<StaffInfoVO> staffList = new ArrayList<StaffInfoVO>();
StaffInfoVO staffInfo = new StaffInfoVO();
StaffInfoVO staffInfo1 = new StaffInfoVO();
staffList.add(staffInfo);
staffList.add(staffInfo1);
//批量保存或修改
lightDao.saveOrUpdateAll(staffList);
//批量保存
lightDao.saveAll(staffList);
...............
lightDao.loadByIds(StaffInfoVO.class,"S2007")
//唯一性验证
lightDao.isUnique(staffInfo, "staffCode");
/**
* @todo 通过对象传参数,简化paramName[],paramValue[] 模式传参
* @param <T>
* @param sqlOrNamedSql 可以是具体sql也可以是对应xml中的sqlId
* @param entity 通过对象传参数,并按对象类型返回结果
*/
public <T extends Serializable> List<T> findBySql(final String sqlOrNamedSql, final T entity);
public Page<StaffInfoVO> findStaff(Page<StaffInfoVO> pageModel, StaffInfoVO staffInfoVO) {
// sql可以直接在代码中编写,复杂sql建议在xml中定义
// 单表entity查询场景下sql字段可以写成java类的属性名称
return findPageEntity(pageModel,StaffInfoVO.class, EntityQuery.create()
.where("#[staffName like :staffName]#[and createTime>=:beginDate]#[and createTime<=:endDate]")
.values(staffInfoVO)
// 字典缓存必须要设置cacheType
// 单表对象查询需设置keyColumn构成select keyColumn as column模式
.translates(new Translate("dictKeyName").setColumn("sexTypeName").setCacheType("SEX_TYPE")
.setKeyColumn("sexType"))
.translates(new Translate("organIdName").setColumn("organName").setKeyColumn("organId")));
}
//演示代码中非直接sql模式设置条件模式进行记录修改
public Long updateByQuery() {
return lightDao.updateByQuery(StaffInfoVO.class,
EntityUpdate.create().set("createBy", "S0001")
.where("staffName like ?").values("张"));
}
//代码中非直接sql模式设置条件模式进行记录删除
lightDao.deleteByQuery(StaffInfoVO.class, EntityQuery.create().where("status=?").values(0));
//1、 条件值处理跟具体sql分离
//2、 将条件值前置通过filters 定义的通用方法加工规整(大多数是不需要额外处理的)
<sql id="show_case">
<filters>
<!-- 参数statusAry只要包含-1(代表全部)则将statusAry设置为null不参与条件检索 -->
<eq params="statusAry" value="-1" />
</filters>
<value><![CDATA[
select *
from sqltoy_device_order_info t
where #[t.status in (:statusAry)]
#[and t.ORDER_ID=:orderId]
#[and t.ORGAN_ID in (:authedOrganIds)]
#[and t.STAFF_ID in (:staffIds)]
#[and t.TRANS_DATE>=:beginAndEndDate[0]]
#[and t.TRANS_DATE<:beginAndEndDate[1]]
]]>
</value>
</sql>
<select id="show_case" resultMap="BaseResultMap">
select *
from sqltoy_device_order_info t
<where>
<if test="statusAry!=null">
and t.status in
<foreach collection="status" item="statusAry" separator="," open="(" close=")">
#{status}
</foreach>
</if>
<if test="orderId!=null">
and t.ORDER_ID=#{orderId}
</if>
<if test="authedOrganIds!=null">
and t.ORGAN_ID in
<foreach collection="authedOrganIds" item="order_id" separator="," open="(" close=")">
#{order_id}
</foreach>
</if>
<if test="staffIds!=null">
and t.STAFF_ID in
<foreach collection="staffIds" item="staff_id" separator="," open="(" close=")">
#{staff_id}
</foreach>
</if>
<if test="beginDate!=null">
and t.TRANS_DATE>=#{beginDate}
</if>
<if test="endDate!=null">
and t.TRANS_DATE<#{endDate}
</if>
</where>
</select>
假设sql语句如下
select *
from sqltoy_device_order_info t
where #[t.ORGAN_ID in (:authedOrganIds)]
#[and t.TRANS_DATE>=:beginDate]
#[and t.TRANS_DATE<:endDate]
java调用过程:
lightDao.find(sql, MapKit.keys("authedOrganIds","beginDate", "endDate").values(authedOrganIdAry,beginDate,null), DeviceOrderInfoVO.class);
最终执行的sql是这样的:
select *
from sqltoy_device_order_info t
where t.ORDER_ID=?
and t.ORGAN_ID in (?,?,?)
and t.TRANS_DATE>=?
然后通过: pst.set(index,value) 设置条件值,不存在将条件直接作为字符串拼接为sql的一部分
<!-- 快速分页和分页优化演示 -->
<sql id="sqltoy_fastPage">
<!-- 分页优化器,通过缓存实现查询条件一致的情况下在一定时间周期内缓存总记录数量,从而无需每次查询总记录数量 -->
<!-- parallel:是否并行查询总记录数和单页数据,当alive-max=1 时关闭缓存优化 -->
<!-- alive-max:最大存放多少个不同查询条件的总记录量; alive-seconds:查询条件记录量存活时长(比如120秒,超过阀值则重新查询) -->
<page-optimize parallel="true" alive-max="100" alive-seconds="120" />
<value><![CDATA[
select t1.*,t2.ORGAN_NAME
-- @fast() 实现先分页取10条(具体数量由pageSize确定),然后再关联
from @fast(select t.*
from sqltoy_staff_info t
where t.STATUS=1
#[and t.STAFF_NAME like :staffName]
order by t.ENTRY_DATE desc
) t1
left join sqltoy_organ_info t2 on t1.organ_id=t2.ORGAN_ID
]]>
</value>
<!-- 这里为极特殊情况下提供了自定义count-sql来实现极致性能优化 -->
<!-- <count-sql></count-sql> -->
</sql>
/**
* 基于对象传参数模式
*/
public void findPageByEntity() {
Page pageModel = new Page();
StaffInfoVO staffVO = new StaffInfoVO();
// 作为查询条件传参数
staffVO.setStaffName("陈");
// 使用了分页优化器
// 第一次调用:执行count 和 取记录两次查询
Page result = lightDao.findPage(pageModel, "sqltoy_fastPage", staffVO);
System.err.println(JSON.toJSONString(result));
// 第二次调用:过滤条件一致,则不会再次执行count查询
//设置为第二页
pageModel.setPageNo(2);
result = lightDao.findPage(pageModel, "sqltoy_fastPage", staffVO);
System.err.println(JSON.toJSONString(result));
}
//支持对象属性注解模式进行缓存翻译
@Translate(cacheName = "dictKeyName", cacheType = "DEVICE_TYPE", keyField = "deviceType")
private String deviceTypeName;
@Translate(cacheName = "staffIdName", keyField = "staffId")
private String staffName;
<sql id="sqltoy_order_search">
<!-- 缓存翻译设备类型
cache:具体的缓存定义的名称,
cache-type:一般针对数据字典,提供一个分类条件过滤
columns:sql中的查询字段名称,可以逗号分隔对多个字段进行翻译
cache-indexs:缓存数据名称对应的列,不填则默认为第二列(从0开始,1则表示第二列),
例如缓存的数据结构是:key、name、fullName,则第三列表示全称
-->
<translate cache="dictKeyName" cache-type="DEVICE_TYPE" columns="deviceTypeName" cache-indexs="1"/>
<!-- 员工名称翻译,如果同一个缓存则可以同时对几个字段进行翻译 -->
<translate cache="staffIdName" columns="staffName,createName" />
<filters>
<!-- 反向利用缓存通过名称匹配出id用于精确查询 -->
<cache-arg cache-name="staffIdNameCache" param="staffName" alias-name="staffIds"/>
</filters>
<value>
<![CDATA[
select ORDER_ID,
DEVICE_TYPE,
DEVICE_TYPE deviceTypeName,-- 设备分类名称
STAFF_ID,
STAFF_ID staffName, -- 员工姓名
ORGAN_ID,
CREATE_BY,
CREATE_BY createName -- 创建人名称
from sqltoy_device_order_info t
where #[t.ORDER_ID=:orderId]
#[and t.STAFF_ID in (:staffIds)]
]]>
</value>
</sql>
// parallQuery 面向查询(不要用于事务操作过程中),sqltoy提供强大的方法,但是否恰当使用需要使用者做合理的判断
/**
* @TODO 并行查询并返回一维List,有几个查询List中就包含几个结果对象,paramNames和paramValues是全部sql的条件参数的合集
* @param parallQueryList
* @param paramNames
* @param paramValues
*/
public <T> List<QueryResult<T>> parallQuery(List<ParallQuery> parallQueryList, String[] paramNames,
Object[] paramValues);
//定义参数
String[] paramNames = new String[] { "userId", "defaultRoles", "deployId", "authObjType" };
Object[] paramValues = new Object[] { userId, defaultRoles, GlobalConstants.DEPLOY_ID,
SagacityConstants.TempAuthObjType.GROUP };
// 使用并行查询同时执行2个sql,条件参数是2个查询的合集
List<QueryResult<TreeModel>> list = super.parallQuery(
Arrays.asList(
ParallQuery.create().sql("webframe_searchAllModuleMenus").resultType(TreeModel.class),
ParallQuery.create().sql("webframe_searchAllUserReports").resultType(TreeModel.class)),
paramNames, paramValues);
# 开启sqltoy默认的函数自适配转换函数
spring.sqltoy.functionConverts=default
# 如在mysql场景下同时测试其他类型数据库,验证sql适配不同数据库,主要用于产品化软件
spring.sqltoy.redoDataSources[0]=pgdb
# 也可以自定义函数来实现替换Nvl
# spring.sqltoy.functionConverts=default,com.yourpackage.Nvl
# 启用框架自带Nvl、Instr
# spring.sqltoy.functionConverts=Nvl,Instr
# 启用自定义Nvl、Instr
# spring.sqltoy.functionConverts=com.yourpackage.Nvl,com.yourpackage.Instr
<sql id="sqltoy_showcase">
<value>
<![CDATA[
select * from sqltoy_user_log t
where t.user_id=:userId
]]>
</value>
</sql>
<!-- sqlId_数据库方言(小写) -->
<sql id="sqltoy_showcase_mysql">
<value>
<![CDATA[
select * from sqltoy_user_log t
where t.user_id=:userId
]]>
</value>
</sql>
品类 | 销售月份 | 销售笔数 | 销售数量(吨) | 销售金额(万元) |
---|---|---|---|---|
苹果 | 2019年5月 | 12 | 2000 | 2400 |
苹果 | 2019年4月 | 11 | 1900 | 2600 |
苹果 | 2019年3月 | 13 | 2000 | 2500 |
香蕉 | 2019年5月 | 10 | 2000 | 2000 |
香蕉 | 2019年4月 | 12 | 2400 | 2700 |
香蕉 | 2019年3月 | 13 | 2300 | 2700 |
<!-- 行转列 -->
<sql id="pivot_case">
<value>
<![CDATA[
select t.fruit_name,t.order_month,t.sale_count,t.sale_quantity,t.total_amt
from sqltoy_fruit_order t
order by t.fruit_name ,t.order_month
]]>
</value>
<!-- 行转列,将order_month作为分类横向标题,从sale_count列到total_amt 三个指标旋转成行 -->
<pivot start-column="sale_count" end-column="total_amt" group-columns="fruit_name" category-columns="order_month" />
</sql>
品类 | 2019年3月 | 2019年4月 | 2019年5月 | ||||||
---|---|---|---|---|---|---|---|---|---|
笔数 | 数量 | 总金额 | 笔数 | 数量 | 总金额 | 笔数 | 数量 | 总金额 | |
香蕉 | 13 | 2300 | 2700 | 12 | 2400 | 2700 | 10 | 2000 | 2000 |
苹果 | 13 | 2000 | 2500 | 11 | 1900 | 2600 | 12 | 2000 | 2400 |
<sql id="group_summary_case">
<value>
<![CDATA[
select t.fruit_name,t.order_month,t.sale_count,t.sale_quantity,t.total_amt
from sqltoy_fruit_order t
order by t.fruit_name ,t.order_month
]]>
</value>
<!-- reverse 是否反向 -->
<summary columns="sale_count,sale_quantity,total_amt" reverse="true">
<!-- 层级顺序保持从高到低 -->
<global sum-label="总计" label-column="fruit_name" />
<!-- order-column: 分组排序列(对同分组进行排序),order-with-sum:默认为true,order-way:desc/asc -->
<group group-column="fruit_name" sum-label="小计" label-column="fruit_name" />
</summary>
</sql>
品类 | 销售月份 | 销售笔数 | 销售数量(吨) | 销售金额(万元) |
---|---|---|---|---|
总计 | 71 | 12600 | 14900 | |
小计 | 36 | 5900 | 7500 | |
苹果 | 2019年5月 | 12 | 2000 | 2400 |
苹果 | 2019年4月 | 11 | 1900 | 2600 |
苹果 | 2019年3月 | 13 | 2000 | 2500 |
小计 | 35 | 6700 | 7400 | |
香蕉 | 2019年5月 | 10 | 2000 | 2000 |
香蕉 | 2019年4月 | 12 | 2400 | 2700 |
香蕉 | 2019年3月 | 13 | 2300 | 2700 |
<!-- 列与列环比演示 -->
<sql id="cols_relative_case">
<value>
<![CDATA[
select t.fruit_name,t.order_month,t.sale_count,t.sale_amt,t.total_amt
from sqltoy_fruit_order t
order by t.fruit_name ,t.order_month
]]>
</value>
<!-- 数据旋转,行转列,将order_month 按列显示,每个月份下面有三个指标 -->
<pivot start-column="sale_count" end-column="total_amt" group-columns="fruit_name" category-columns="order_month" />
<!-- 列与列之间进行环比计算 -->
<cols-chain-relative group-size="3" relative-indexs="1,2" start-column="1" format="#.00%" />
</sql>
品类 | 2019年3月 | 2019年4月 | 2019年5月 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
笔数 | 数量 | 比上月 | 总金额 | 比上月 | 笔数 | 数量 | 比上月 | 总金额 | 比上月 | 笔数 | 数量 | 比上月 | 总金额 | 比上月 | |
香蕉 | 13 | 2300 | 2700 | 12 | 2400 | 4.30% | 2700 | 0.00% | 10 | 2000 | -16.70% | 2000 | -26.00% | ||
苹果 | 13 | 2000 | 2500 | 11 | 1900 | -5.10% | 2600 | 4.00% | 12 | 2000 | 5.20% | 2400 | -7.70% |
<!-- 树排序、汇总 -->
<sql id="treeTable_sort_sum">
<value>
<![CDATA[
select t.area_code,t.pid_area,sale_cnt from sqltoy_area_sales t
]]>
</value>
<!-- 组织树形上下归属结构,同时将底层节点值逐层汇总到父节点上,并且对同层级按照降序排列 -->
<tree-sort id-column="area_code" pid-column="pid_area" sum-columns="sale_cnt" level-order-column="sale_cnt" order-way="desc"/>
</sql>
地区 | 归属地区 | 销售量 |
---|---|---|
上海 | 中国 | 300 |
松江 | 上海 | 120 |
杨浦 | 上海 | 116 |
浦东 | 上海 | 64 |
江苏 | 中国 | 270 |
南京 | 江苏 | 110 |
苏州 | 江苏 | 90 |
无锡 | 江苏 | 70 |
sql参见quickstart项目:com/sqltoy/quickstart/sqltoy-quickstart.sql.xml 文件
<!-- 演示分库 -->
<sql id="qstart_db_sharding_case">
<sharding-datasource strategy="hashDataSource" params="userId" />
<value>
<![CDATA[
select * from sqltoy_user_log t
-- userId 作为分库关键字段属于必备条件
where t.user_id=:userId
#[and t.log_date>=:beginDate]
#[and t.log_date<=:endDate]
]]>
</value>
</sql>
<!-- 演示分表 -->
<sql id="qstart_sharding_table_case">
<sharding-table tables="sqltoy_trans_info_15d" strategy="realHisTable" params="beginDate" />
<value><![CDATA[
select * from sqltoy_trans_info_15d t
where t.trans_date>=:beginDate
#[and t.trans_date<=:endDate]
]]>
</value>
</sql>
@Sharding 在对象上通过注解来实现分库分表的策略配置
参见:com.sqltoy.quickstart.ShardingSearchTest 进行演示
package com.sqltoy.showcase.vo;
import java.time.LocalDate;
import java.time.LocalDateTime;
import org.sagacity.sqltoy.config.annotation.Sharding;
import org.sagacity.sqltoy.config.annotation.SqlToyEntity;
import org.sagacity.sqltoy.config.annotation.Strategy;
import com.sagframe.sqltoy.showcase.vo.base.AbstractUserLogVO;
/**
* @project sqltoy-showcase
* @author zhongxuchen
* @version 1.0.0 Table: sqltoy_user_log,Remark:用户日志表
*/
/*
* db则是分库策略配置,table 则是分表策略配置,可以同时配置也可以独立配置
* 策略name要跟spring中的bean定义name一致,fields表示要以对象的哪几个字段值作为判断依据,可以一个或多个字段
* maxConcurrents:可选配置,表示最大并行数 maxWaitSeconds:可选配置,表示最大等待秒数
*/
@Sharding(db = @Strategy(name = "hashBalanceDBSharding", fields = { "userId" }),
// table = @Strategy(name = "hashBalanceSharding", fields = {"userId" }),
maxConcurrents = 10, maxWaitSeconds = 1800)
@SqlToyEntity
public class UserLogVO extends AbstractUserLogVO {
/**
*
*/
private static final long serialVersionUID = 1296922598783858512L;
/** default constructor */
public UserLogVO() {
super();
}
}
package com.sqltoy.quickstart;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
*
* @project sqltoy-quickstart
* @description quickstart 主程序入口
* @author zhongxuchen
* @version v1.0, Date:2020年7月17日
* @modify 2020年7月17日,修改说明
*/
@SpringBootApplication
@ComponentScan(basePackages = { "com.sqltoy.config", "com.sqltoy.quickstart" })
@EnableTransactionManagement
public class SqlToyApplication {
/**
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(SqlToyApplication.class, args);
}
}
# sqltoy config
spring.sqltoy.sqlResourcesDir=classpath:com/sqltoy/quickstart
spring.sqltoy.translateConfig=classpath:sqltoy-translate.xml
spring.sqltoy.debug=true
#spring.sqltoy.reservedWords=status,sex_type
#dataSourceSelector: org.sagacity.sqltoy.plugins.datasource.impl.DefaultDataSourceSelector
#spring.sqltoy.defaultDataSource=dataSource
spring.sqltoy.unifyFieldsHandler=com.sqltoy.plugins.SqlToyUnifyFieldsHandler
#spring.sqltoy.printSqlTimeoutMillis=200000
<?xml version="1.0" encoding="UTF-8"?>
<sagacity
xmlns="http://www.sagframe.com/schema/sqltoy-translate"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sagframe.com/schema/sqltoy-translate http://www.sagframe.com/schema/sqltoy/sqltoy-translate.xsd">
<!-- 缓存有默认失效时间,默认为1小时,因此只有较为频繁的缓存才需要及时检测 -->
<cache-translates>
<!-- 基于sql直接查询的方式获取缓存 -->
<sql-translate cache="dictKeyName"
datasource="dataSource">
<sql>
<![CDATA[
select t.DICT_KEY,t.DICT_NAME,t.STATUS
from SQLTOY_DICT_DETAIL t
where t.DICT_TYPE=:dictType
order by t.SHOW_INDEX
]]>
</sql>
</sql-translate>
</cache-translates>
<!-- 缓存刷新检测,可以提供多个基于sql、service、rest服务检测 -->
<cache-update-checkers>
<!-- 增量更新,带有内部分类的查询结果第一列是分类 -->
<sql-increment-checker cache="dictKeyName"
check-frequency="15" has-inside-group="true" datasource="dataSource">
<sql><![CDATA[
--#not_debug#--
select t.DICT_TYPE,t.DICT_KEY,t.DICT_NAME,t.STATUS
from SQLTOY_DICT_DETAIL t
where t.UPDATE_TIME >=:lastUpdateTime
]]></sql>
</sql-increment-checker>
</cache-update-checkers>
</sagacity>
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SqlToyApplication.class)
public class CrudCaseServiceTest {
@Autowired
private SqlToyCRUDService sqlToyCRUDService;
/**
* 创建一条员工记录
*/
@Test
public void saveStaffInfo() {
StaffInfoVO staffInfo = new StaffInfoVO();
staffInfo.setStaffId("S190715005");
staffInfo.setStaffCode("S190715005");
staffInfo.setStaffName("测试员工4");
staffInfo.setSexType("M");
staffInfo.setEmail("[email protected]");
staffInfo.setEntryDate(LocalDate.now());
staffInfo.setStatus(1);
staffInfo.setOrganId("C0001");
staffInfo.setPhoto(FileUtil.readAsBytes("classpath:/mock/staff_photo.jpg"));
staffInfo.setCountry("86");
sqlToyCRUDService.save(staffInfo);
}
}