数据模型
About 7265 wordsAbout 24 min
1984-01-24
BeetlSQL的参数和出参支持POJO和Map。一般来说POJO跟容易维护,而Map不容易维护,尤其是数字类型,很可能不同数据库,Map的Value类型还可能不一样。而POJO映射则会强制转化为POJO定义的类型,或者使用注解,RowMapper,ResultSetMapper等方式等进一步扩展
BeetlSQL推荐使用POJO作为数据模型
本章介绍了BeetlSQL操作的数据模型,以及模型生命周期里(beetlsql操作过程中)的一些可以扩展点
POJO
POJO的定义是普通java对象(相对于JavaEE的EJB来说的),java对象需要准守JavaBean规范,即提供getter和setter方法。如下是一个符合BeetlSQL的POJO
public class UserData{
Integer id;
String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
下面的代码则不是遵守JavaBean规范的POJO
public class UserData{
public Integer id;
public String name;
}
下面的代码则不是遵守JavaBean规范的POJO,大写开头的属性
public class UserData{
private Integer Id;
private String Name;
public String getName(){return Name;}
....
}
下面的代码则不是遵守JavaBean规范的POJO,链式调用不符合JavaBean
public class UserData{
Integer id;
public Integer getId() {
return id;
}
public UserData setId(Integer id) {
this.id = id;
return this
}
}
如果使用lombok,则只需要使用@Data注解
@Data
public class UserData{
Integer id;
String name;
}
lombok的chain = true 会破坏JavaBean规范,导致BeetlSQL无法使用此POJO
不严格的POJO
如果POJO不遵守JavaBean规范,在许多序列化框架都会有问题。建议使用BeetlSQL使用JavaBean规范,如果因为历史问题遇到不符合JavaBean规范的POJO,Beetlsql可以在初始化的时候设置
SQLManager.javabeanStrict(false)
这样,支持如下不符合JavaBean的POJO
- 链式调用
- 变量名大写开头
- 在lombok中,类似 aBc 这种字段,会错误的生成getABc(), 实际应该是getaBc(),javabeanStrict(false)能识别这种情况
交集
这是一个非常重要的概念。BeetlSQL 在操作数据库的时候,默认情况只会处理POJO与数据库表(或者视图)的交集,比如,数据库表有id,user_name 俩个字段
而Java的POJO 有id和name。 因此(除非你使用@Cloumn说明),只有POJO的id属性和表的id字段对应上,这将作为BeetlSQL的所有操作基础元数据
当你调用insert方法的时候,POJO的id属性将被插入到id列,但name属性不做任何操作。
当你调用select方法的时候POJO的属性name无法赋值
@Table 注解
当要进行插入或者更新操作的时候,类名通过NameConversion隐喻了表名,也可使用@Table注解说明表名字
@Table(name="sys_user")
@Data
public static class UserData{
}
如果表在其他schema或者catalog里,可以加上前缀
@Table(name="das10.sys_user")
@Data
public static class UserData{
}
@Table注解可以使用表达式,以实现动态分表
static final String USER_TABLE="${toTable('sys_user',id)}";
@Data
@Table(name = USER_TABLE)
public class MyUser {
@AssignID
private Integer id;
private String name;
}
toTable是一个自定义函数,可以查看源码S6MoreDatabase,或者查看《BeetlSQL3 多库使用》
如果不是更新或者插入,而只是映射查询结果,则不需要@Table,你可以定义任意多的POJO来映射结果集,BeetlSQL3默认情况会采用POJO属性和结果集的交集来映射。比如MyUser对象,只有id和name,那么查询结果中的列名department_id不做映射
@Table注解有isView属性,默认false。此设置适用table和视图。仅仅当视图具备逻辑主键的时候,你想复用内置的updateById,single等依赖主键的id操作的时候,你可以设置为true,这样,POJO的主键不依赖数据库设定,而是根据@AssignId 指定
@Column 注解
同@Table一样,如果NameConversion无法满足,可以使用@Column来标识属性对应的列名
@Table(name="sys_user")
public class TestUser {
@Column("id")
@AutoID
Integer myId;
@Column("name")
String myName;
Integer departmentId;
}
属性departmentId并未使用注解,这说明符合NameConversion
主键
表格主键以数据库定义为准,POJO必须严格的以数据库定义来使用@AutoId,@AssingId,和@SeqId。如果是视图,想要一个逻辑主键,这必须使用@Table(isView=true),BeetlSQL 则认为@AssingId标注的属性的是视图主键
@AutoID,作用于属性字段或者getter方法,告诉beetlsql,这是自增主键,对应于数据自增长
@AutoID
Integer myId;
@AssignID,作用于属性字段或者getter方法,告诉beetlsql,这是程序设定
@AssignID
Integer id;
代码设定主键允许像@AssignID 传入id的生成策略以自动生成序列,BeetlSQL默认提供了一个snowflake算法,一个用于分布式环境的id生成器(https://github.com/twitter/snowflake),生成器的名字是simple
@AssignID("simple")
@AssignID()
Long id;
这个simple 算法不能用在实际项目里,因为实际雪花算法需要配置工作机器ID,beetlsql的simple生成器没有提供
你可以实现自己的id生成策略,比如最常用的UUID
sqlManager.addIdAutonGen("uuid", new IDAutoGen(){
@Override
public Object nextID(String params) {
return UUID.randomUUID().toString();
}
});
@AssignID("uuid")
String id;
需要注意Java自带的 UUID可能有性能上的问题,因为UUID的随机数来自于操作系统生成,因为操作系统生成不了足够的uuid导致性能能下降。可以搜索uuid+urandom 从网上查找改善Java内置UUID的性能,或者使用第三方的UUID,如uuid-creator
- @SeqID(name="xx_seq"),告诉beetlsql,这是序列主键,目前只有H2,Oracle和Postgres或者DB2使用序列主键,以源码单元测试为例子,如下DeviceDetail具有id,序列名称是
label_sequence
@Data
public abstract class BaseSeqIdEntity<ID> extends BaseEntity implements Serializable{
@SeqID(name="label_sequence")
protected ID id;
}
@Data
@Table(name="device_detail")
public class DeviceDetail extends BaseSeqIdEntity<Integer>{
String json;
}
IdTest的seqIdTest如下
public class IdTest extends BaseTest {
@Test
public void seqIdTest(){
DeviceDetail data = new DeviceDetail();
data.setJson("{}");
sqlManager.insert(data);
Assert.assertNotNull(data.getId());
System.out.println(data);
}
}
执行后,可以看到H2的输出
┏━━━━━ Debug [deviceDetail.$insert] ━━━
┣ SQL: insert into device_detail (ID ,JSON ) values (NEXT VALUE FOR label_sequence ,? )
┣ 参数: [{}]
┣ 位置: org.beetl.sql.id.IdTest.seqIdTest(IdTest.java:47)
┣ 时间: 7ms
┣ 更新: [1]
┗━━━━━ Debug [deviceDetail.$insert] ━━━
H2Style.getSeqValue返回了序列名称对应的求值语句,Oralce,Postgres类似,如下是DbStyle的getSeqValue实现
public class H2Style extends AbstractDBStyle {
@Override
public String getSeqValue(String seqName) {
return "NEXT VALUE FOR "+seqName;
}
}
public class OracleStyle extends AbstractDBStyle {
@Override
public String getSeqValue(String seqName) {
return seqName+".nextval";
}
}
public class OracleStyle extends AbstractDBStyle {
@Override
public String getSeqValue(String seqName) {
return "nextval('" + seqName + "')";
}
}
对于支持多种数据库的,这些annotation可以叠加在一起,但作为跨库更好的选择是使用@AssignId,并自定义个id生成策略
RowMapper
BeetlSQL完成默认的映射,你可以自定义一个RowmMapper子类,完成额外的映射
public interface RowMapper<T> {
/**
*
* @param obj 正常处理后的对象
* @param rs 结果集
* @param rowNum 处理的记录位置(第几条记录):可以只针对某一条记录做特殊处理
* @param config 注解相关配置,参考 {@link ProviderConfig}
* @throws SQLException
* @return T
*/
T mapRow(ExecuteContext ctx, Object obj, ResultSet rs, int rowNum, Annotation config) throws SQLException;
}
RowMapper会在BeetlSQL默认映射结果集的基础上做额外处理,比如,有些未映射的也可以通过ResultSet rs
中调用获取
public static class MyRowMapper implements RowMapper<UserVo>{
@Override
public UserVo mapRow(ExecuteContext ctx, Object obj, ResultSet rs, int rowNum, Annotation config) throws SQLException {
//内置的映射已经完成
UserVo vo = (UserVo)obj;
//额外取得结果集
String col = rs.getString("col");
vo.setExtraAttribute(col);
return vo;
}
}
有俩种方式可以使用RowMapper,一种是通过在POJO上加上注解@RowProvider
@RowProvider(MyRowMapper.class)
public class UserVo2 {
//忽略其他属性
public void setExtraAttribute(String col){}
}
另外一种是SQLManager.rowMapper 方法,临时设置一次当前查询使用RowMapper(这种方式不常用)
sqlManager.rowMapper(MyRowMapper.class).select(sqlId,xxxx.class,paras);
当查询结果返回后,rowMapper使用结束。除非再次调用rowMapper方法
ResultSetMapper
如果想自己完全掌控结果集映射,可以使用ResultSetMapper,定义如下
public interface ResultSetMapper<T> {
/**
* 将数据库查询结果集ResultSet映射到一个对象上,对象通过target指定
* @param ctx
* @param target
* @param resultSet
* @param config 实现了ProviderConfig注解的注解,如果没有,则为空
* @return
*/
public List<T> mapping(ExecuteContext ctx, Class target, ResultSet resultSet, Annotation config) throws SQLException;
}
ExecuteContext代表了执行上下文,比如SqlId,当前的SQLManager,入参等,一般很少需要关注,除非有些高级需求,比如BeetlSQL提供的JSON映射就是用到了ExecuteContext
一个简单的实现如下
public class MyResultSetMapper implements ResultSetMapper<ResultSetObject>{
@Override
public List<ResultSetObject> mapping(ExecuteContext ctx, Class target, ResultSet resultSet, Annotation config) throws SQLException {
List<ResultSetObject> list = new ArrayList<>();
while(resultSet.next()){
ResultSetObject obj = new ResultSetObject();
obj.setMyId(resultSet.getInt("id"));
obj.setMyName(resultSet.getString("name"));
list.add(obj);
}
return list;
}
}
有俩种方法使用ResultSetMapper,第一种在POJO使用注解
@Data
@ResultProvider(MyResultSetMapper.class)
public class ResultSetObject{
private Integer myId;
private String myName;
}
或者使用SQLManager.resultSetMapper(Class resultSetMapperClass),临时设置一次当前查询采用的映射类
Json配置映射
类似MyBatis通过xml提供映射,BeetlSQL3通过JsonConfigMapper,实现ResultSetMapper,提供了一个json格式映射
private static final String USER_MAPPING = "{'id':'id','name':'name','deptName':'dept_name'}";
@Data
@ResultProvider(JsonConfigMapper.class)
@JsonMapper(USER_MAPPING)
public static class UserInfo {
Integer id;
String name;
String deptName;
}
对于UserInfo,使用了BeetlSQL3提供的JsonConfigMapper对象,JsonConfigMapper会读取@JsonMapper( 此类被@ProviderConfig标注)作为配置映射参数,这样,如果查询的SQL结果集是
id,name,detp_name
则会按照USER_MAPPING
的配置映射到各自属性上
@JsonMapper 实现了@ProviderConfig()注解,因此,这注解将会传给JsonConfigMapper。了解BeetlSQL3注解如何实现,可以参考《源码解读》
如果需要一对多的映射,也可以使用
private static final String DEPT_MAPPING = "{'id':'id','name':'name','users':{'id':'u_id','name':'u_name'}}";
@Data
@ResultProvider(JsonConfigMapper.class)
@JsonMapper(DEPT_MAPPING)
public static class DepartmentInfo {
Integer id;
String name;
List<UserInfo> users;
}
对于DEPT_MAPPING配置,如果结果集满足
id,name,u_id,u_name
则可以进行映射,并且,u_id,u_name,赋值给UserInfo对象, 此对象合并到**相同(id,name)**的DepartmentInfo的users属性上
JsonConfigMapper可以进行任意复杂的映射。 并将结果集合并
@JsonMapper提供了json配置,也可以指定一个sqlId作为配置,因此配置可以放到markdown文件里
@ResultProvider(JsonConfigMapper.class)
//@JsonMapper(
// "{'id':'id','name':'name','users':{'id':'u_id','name':'u_name'}}")
@JsonMapper(resource ="dept.departmentJsonMapping")
public class MyDepartment {
Integer id;
String name;
List<MyUser> users;
}
dept.md内容如下
departmentJsonMapping
===
* MyDepartment 映射关系配置
```json
{
"id":"id",
"name":"name",
"users":
{
"id":"u_id",
"name":"u_name"
}
}
```
Json自动映射
如果查询结果集跟java类定义匹配,则不需要显示的json配置,可以AutoJsonMapper
@Data
@ResultProvider(AutoJsonMapper.class)
public class MyUserView {
Integer id;
String name;
DepartmentEntity dept;
}
如上配置,可以自动映射如下查询结果
String sql = "select u.id ,u.name ,d.id `dept.id`,d.name `dept.name` " +
" from sys_user u left join department d on d.id=u.department_id";
SQLReady ready = new SQLReady(sql);
List<MyUserView> list = sqlManager.execute(ready,MyUserView.class);
之所以成为AutoJsonMapper,是因为AutoJsonMapper会解析POJO类,生成一个json配置,类似如下
{
"id":"id",
"name":"name",
"dept":
{
"id":"dept.id",
"name":"dept.name"
}
}
XML支持
beetlsql提供一个子模块支持sql-xml
XML,语法上借鉴了mybatis,这是beetlsql的扩展包,因此需要额外引入
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>sql-xml</artifactId>
<version>3.23.11-RELEASE</version>
</dependency>
如下是一个xml文件定义
<?xml version="1.0" encoding="UTF-8" ?>
<beetlsql>
<sql id="select">
<!-- 测试sql -->
select * from sys_user
<where>
<if test="has(user) and user.name=='a'">
and name='3'
</if>
<bind value="1+99" export="newValue"/>
<isNotEmpty value="'1'">
and name='5'
</isNotEmpty>
and name = #{newValue}
and name in
<foreach items="[1,2,3]" var="id,status" open="(" close=")" separator=",">
#{id+status.index}
</foreach>
<include refid="commonWhere"/>
</where>
</sql>
<resultMap id="simpleMap">
<result property="id" column="id"/>
<result property="myName" column="user_name"/>
</resultMap>
<resultMap id="complexMap">
<result property="id" column="id"/>
<association property="info" >
<result property="name" column="name"/>
<result property="age" column="age"/>
</association>
</resultMap>
<resultMap id="complexListMap">
<result property="id" column="id"/>
<collection property="listInfo" >
<result property="name" column="name"/>
<result property="age" column="age"/>
</collection>
</resultMap>
</beetlsql>
参考源码sql-test下的代码QuickXMLTestBeetlSQL,以及对应的xml文件user.xml
因为xml支持并非xml核心功能,因此需要一定步奏引入和配置
- sqlLoader使用XMLClasspathLoader,可以通过配置或者api设定
ConnectionSource source = ....
SQLManagerBuilder builder = new SQLManagerBuilder(source);
builder.setNc(new UnderlinedNameConversion());
builder.setDbStyle(new H2Style());
builder.setInters(new Interceptor[]{new DebugInterceptor()});
builder.setSqlLoader(new XMLClasspathLoader("sql"));
sqlManager = builder.build();
- 同创建md文件那样,创建xml文件,格式可以参考如上例子
- 如果提供的xml tag不够用,可以完全自定义tag,以if标签实现为例子,用beetl的标签实现,参考https://www.kancloud.cn/xiandafu/beetl3_guide/2138969 了解如何定义标签
public class IfTag extends Tag{
@Override
public void render() {
if (!containHtmlAttribute("test")) {
throw new IllegalArgumentException("缺少 test属性");
}
Object value = this.getHtmlAttribute("test");
if (!(value instanceof Boolean)) {
throw new IllegalArgumentException("期望test表达式运算结果是boolean类型");
}
if ((Boolean) value) {
this.doBodyRender();
}
}
}
- 调用XMLBeetlSQL,初始化xml支持,如果你定义了xml标签,调用
XMLBeetlSQL.config(sqlManager);
//可选,自定义tag
XMLBeetlSQL.regisregisterTagter(sqlManager,"yourTag",YourTag.clas);
初始化XML支持后,就可以像使用md文件那样使用XML文件
- 如果有自定义映射,可以在xml文件定义resultMap,并通过@ResultProvider(XMLConfigMapper.class) 和@XMLMapper(resource=sqlId) 来指定对象的映射文件
@Test
public void testForeach(){
Map map = new HashMap();
map.put("ids", Arrays.asList(1,2));
//访问的是user.xml文件下的 testFoeach 标签
List<User> list = sqlManager.select(SqlId.of("user.testForeach"),User.class,map);
Assert.assertTrue(list.size()==2);
}
@Test
public void testComplexXMLMapping(){
String sql = "select id,name,age from sys_user";
List<MyXMLComplexUser> list = sqlManager.execute(new SQLReady(sql),MyXMLComplexUser.class);
Assert.assertTrue(list.get(0) instanceof MyXMLComplexUser );
}
@ResultProvider(XMLConfigMapper.class)
@XMLMapper(resource="user.complexMap")
@Data
public static class MyXMLComplexUser {
private Long id;
private MyXMLComplexUserInfo info;
}
@Data
public static class MyXMLComplexUserInfo {
private String name;
private Integer age;
}
user.complexMap对应了user.xml的映射配置
<resultMap id="complexMap">
<result property="id" column="id"/>
<association property="info" >
<result property="name" column="name"/>
<result property="age" column="age"/>
</association>
</resultMap>
自动Fetch
有时候查询结果出来后需要自动加载额外的数据,类似Hibernate 的关系映射。BeetlSQL3也支持这种自动抓取。不同hibernate的是,他不强制要求有外键关系
越来越多数据库设计不考虑外键,这样能提升一些性能。系统从某种角度来说也好维护。
Fetch不支持复合主键,只支持单主键
自动抓取通过@Fetch注解,提醒BeetlSQL3在执行完查询操作后有自动抓取需要完成,BeetlSQL3会解析此POJO的属性,如果一旦有@FetchMany或者@FetchOne,或者@FetchSql,则会执行查询操作
@Data
@Table(name="sys_order")
@Fetch(level =2)
public class CustomerOrder {
@AutoID
Integer id;
String name;
Integer customerId;
@FetchOne(value="customerId")
Customer customer;
}
@Data
@Fetch(level = 2)
@Table(name="sys_customer")
public class Customer {
@AutoID
Integer id;
String name;
@FetchMany("customerId")
List<CustomerOrder> order;
}
@Table(name="sys_order")
@Fetch(level =2)
public class CustomerOrder2 {
@AutoID
Integer id;
String name;
Integer customerId;
@FetchSql("select * from sys_customer where id =#{customerId}")
Customer customer;
@FetchSql("select * from sys_customer s where s.id =#{customerId} order by s.id desc")
List<Customer> customers;
}
@Fetch的level属性表示抓取数据的深度,默认是一层,CustomerOrder设定为2,则不仅仅会自动抓取Customer数据,也会抓取Customer的CustomerOrder数据。如果CustomerOrder设定为3,那么,还会从CustomerOrder再次抓取Customer,实现3层抓取
BeetlSQL3在Fetch过程中把已经抓取的过数据放入内存里,根据各自POJO类的主键判断,数据一旦曾经抓取过,则不会再从数据库里获取。因此不需要担心出现死循环.同时,缓存有利于性能优化,不需要查询数据库
@FetchOne 表示抓取一个,其value值是POJO的一个属性名,该属性名对应的值作为需要·抓取对象的主键,因此CustomerOrder的@FetchOne注解表明了需要使用CustomerOrder.customerId属性值作为主键来查询Customer。因此BeetlSQL3会发起类似如下查询
Integer customerId = getAttrValue(customerOrderIns,"customerId")
Customer customer = sqlMqnager.unique(Customer,customerId);
对于Customer对象,需要自动抓取多个CustomerOrder,注解@FetchMany("customerId") 告诉BeetlSQL3.启用模板查询功能查询CustomerOrder,模板的key是“customerId”(也就是CustomerOrder属性customerId),值是POJO的主键,就是Customer.id.因此BeetlSQL会发起类似入如下查询
Integer customerId = getPrimakeyValue(customerIns);
CustomerOrder template = new CustomerOrder();
tempalte.setCustomerId(customerId);
List<CustomerOrder> list = sqlManager.template(tempalte);
BeetlSQL3 的FetchOne操作会进行合并查询,比如查询所有CustomerOrder
List<CustomerOrder> orders = sqlManager.all();
在BeetlSQL3 进行自动抓取的时候,并不会逐一抓取Customer对象,而是调用sqlManager.selectByIds 一次性抓取所有Customer,提高性能
当使用自动Fetch,设置level=2的时候,出现A引用B,B又引用A的时候,需要特别设计A和B对象的hashcode方法和equals方法,避免出现无限循环,比如
@Fetch(level = 2)
@Table(name="sys_customer")
@EqualsAndHashCode(of="id")
public class Customer {
@AutoID
Integer id;
String name;
@FetchMany("customerId")
List<CustomerOrder> order;
}
如果没有@EqualsAndHashCode(of="id")
那么Customer的hashcode方法包括CustomerOrder,而CustomerOrder又包含Customer,这样导致hashcode无需循环,出现StackOverflowError。 这并不是BeetlSQL的问题,是hash设计的问题
FetchOne和FetchMany支持符合条件时候抓取。如下
@Data
@Table(name="sys_order")
@Fetch
public class CustomerOrder {
@AutoID
Integer id;
String name;
Integer customerId;
@FetchOne(value="customerId",enableOn="x")
Customer customer;
@FetchOne(value="customerId"")
Account account;
}
enableOn表示但BeetlSQL 存在变量x的时候,FetchOne才生效。 sql语句可以通过函数fetchEnableOn 来申明一个变量,如sql语句
dynamicFetchOrder1
===
select * from sys_order where id = #{id}
-- @ fetchEnableOn("c");
fetchEnableOn的实现类是DynamicFetchEnableOnFunction,它能接受多个变量名字,并赋值一个固定值。FetchOne和FetchMany将检测是否存在此变量,如果不存在,则不会实现Fetch操作
AttributeConvert
可以自定义一个属性注解,BeetlSQL上遇到此属性注解,将按照属性注解的执行类去执行映射,比如对手机号码的入库加密,出库解密。比如对JSON对象序列化成字符串到数据库,然后从数据库反序列成成对象。同其他BeetlSQL扩展注解机制类似,实现一个扩展注解,需要使用@Builder注解申明其执行类
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Builder(Base64Convert.class)
public static @interface Base64 {
}
如上申明一个@Base64,用于字段在入库加密,出库解密。其实现类使用@Builder注解申明,本例其执行类是Base64Convert。
执行类必须是一个AttributeConvert的子类
public static class Base64Convert implements AttributeConvert {
Charset utf8 = Charset.forName("UTF-8");
public Object toDb(ExecuteContext ctx, Class cls, String name, Object pojo) {
String value= (String) BeanKit.getBeanProperty(pojo,name);
byte[] bs = java.util.Base64.getEncoder().encode(value.getBytes(utf8));
return new String(bs,utf8);
}
public Object toAttr(ExecuteContext ctx, Class cls, String name, ResultSet rs, int index) throws SQLException {
String value = rs.getString(index);
return new String(java.util.Base64.getDecoder().decode(value),utf8);
}
}
toDb方法用于将属性转化为列,pojo是入库的POJO对象,name是指其属性名称,可以调用BeetlSQL3提供的类BeanKit.getBeanProperty获取对象属性值
toAttr将数据库转化为属性。 如下是使用@Base64的一个例子
@Table(name="sys_user")
@Data
public class UserData{
@AutoID
Integer id;
@Base64
String name;
}
如下是定义了一个@Update注解
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Builder(UpdateTimeConvert.class)
public @interface UpdateTime {
}
public class UpdateTimeConvert implements AttributeConvert {
@Override
public Object toDb(ExecuteContext ctx, Class cls,String name, Object pojo){
Date now = new Date();
BeanKit.setBeanProperty(pojo,now,name);
return now;
}
}
这样,在每次入库操作的时候,都取得最新的时间。并调用BeanKit.setBeanProperty赋值给pojo对象,并返回当前时间
。BeetlSQL3通过返回的当前时间做入库操作,因此调用BeanKit.setBeanProperty 不是必须操作。但POJO对象还需要有一个与数据库操作结果一致的值。如果未调用。pojo的对应属性为null。
源码例子org.beetl.sql.test.annotation.Jackson 是一个把对象序列化成字符串存入数据库,或者从数据库取出反序列化成对象。
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Builder(JacksonConvert.class)
public @interface Jackson {
}
JacksonConvert 实现了AttributeConvert,以toDb为例子
public class JacksonConvert implements AttributeConvert {
ObjectMapper objectMapper = new ObjectMapper();
@Override
public Object toDb(ExecuteContext ctx, Class cls,String name, Object dbValue) {
Object obj = BeanKit.getBeanProperty(dbValue,name);
if(obj==null){
return null;
}
try {
String str = objectMapper.writeValueAsString(obj);
return str;
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(...);
}
}
}
AttributeConvert还能影响针对POJO自动生成的内置SQL语句,提供如下方法
default String toAutoSqlPart(DBStyle dbStyle,Class cls,AutoSQLEnum autoSQLEnum, String name){
return null;
}
默认情况下,返回nul,不会影响自动生成语句,比如对于内置insert语句,生成的是
insert into .... value( #{id},#{jsonData} )
在postgres数据库,如果json_data列使用了jsonb,那期望内置生成的insert sql语句是
insert into .... value( #{id},#{jsonData}::JSON )
针对这个情况,可以JacksonConvert
可以重写toAutoSqlPart
default String toAutoSqlPart(DBStyle dbStyle,Class cls,AutoSQLEnum autoSQLEnum, String name){
return "$$::JSON";
}
这里的$$代表了属性占位符号,BeetlSQL的内置生成sql语句依据此生成合适的sql语句,如替换$$
,生成#{jsonData}::JSON
. 或者替换成?::JSON
取决于生成方式。
可以参考源码例子 org.beetl.sql.postgres.JacksonConvert
BeanConvert
可以为POJO定义一个注解,在sql准备参数前,调用此API,得到一个新Bean,用于参数设定。BeanConvert定义如下
@Plugin
public interface BeanConvert {
/**
* 返回入库之前的对象
* @param ctx
* @param obj
* @param an 注解信息,可以提供额外参数
* @return
*/
default Object before(ExecuteContext ctx, Object obj, Annotation an){
return obj;
}
/**
* 返回查询结果后的对象
* @param ctx
* @param obj
* @param an
* @return
*/
default Object after(ExecuteContext ctx, Object obj, Annotation an){
return obj;
}
}
比如以AttributeConvert的例子作为说明,可以定义如下BeanEncrypt, 其执行类BeanStringConvert, 注解有个attr方法,标识需要加密的字段
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Builder(BeanStringConvert.class)
public @interface BeanEncrypt {
String attr();
}
BeanStringConvert的实现如下(仅仅简单修改了需要加密的字段,添加一个时间戳)
public class BeanStringConvert implements BeanConvert{
public Object before(ExecuteContext ctx, Object obj, Annotation an){
BeanEncrypt beanEncrypt = (BeanEncrypt)an;
String attrName = beanEncrypt.attr();
String attrValue = (String)BeanKit.getBeanProperty(obj,attrName);
String encryptAttrValue = attrValue+"-"+System.currentTimeMillis();
BeanKit.setBeanProperty(obj,encryptAttrValue,attrName);
return obj;
}
public Object after(ExecuteContext ctx, Object obj, Annotation an){
BeanEncrypt beanEncrypt = (BeanEncrypt)an;
String attrName = beanEncrypt.attr();
String encryptAttrValue = (String)BeanKit.getBeanProperty(obj,attrName);
String attrValue = encryptAttrValue.split("-")[0];
BeanKit.setBeanProperty(obj,attrValue,attrName);
return obj;
}
}
@Table(name="sys_user")
@Data
@BeanEncrypt( attr="name")
public static class UserEntity2{
@Auto
Long id ;
String name;
}
枚举
BeetlSQL3默认情况会调用枚举的name方法转化为字符串,存入数据库。当从数据库取出字符串的时候,调用枚举的valueOf方法得到其枚举
public class UserData{
@AutoID
Integer id;
Name name;
}
/*使用枚举名存库*/
enum Name{
Li,
Zhang;
}
BeetlSQL3也支持自定义枚举存入数据库的值,使用@EnumValue标注在枚举的属性字段上,如下枚举Name2,取值是属性str
public enum Name2{
Li("li"),
Zhang("zhang");
@EnumValue
String str;
Name2(String str){
this.str = str;
}
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
}
如果枚举来自第三方,无法使用@EnumValue,则可以使用@EnumMapping,如上Name2如果来自第三方,则可以在POJO中@EnumValue
@Table(name="sys_user")
@Data
public static class UserData3{
@AutoID
Integer id;
@EnumMapping("str")
Name2 name;
}
关于枚举,可以参考源码单元测试EnumSelectTest
BeetlSQL 也提供了 SQLManagerExtend可以扩展对枚举的操作,只需要设置一个自定义的EnumExtend
,默认实现如下
public class EnumExtend {
public Enum getEnumByValue(Class c, Object value) {
return EnumKit.getEnumByValue(c,value);
}
public Object getValueByEnum(Object en) {
return EnumKit.getValueByEnum(en);
}
}
EnumKit会解析@EnumValue注解或者@EnumMapping注解
混合模型
混合模型。兼具灵活性和更好的维护性。POJO可以实现Tail(尾巴的意思),或者继承TailBean,这样查询出的ResultSet 除了按照pojo进行映射外,无法映射的值将按照列表/值保存。如下一个混合模型:
/*混合模型*/
public User extends TailBean{
private int id ;
private String name;
private int roleId;
/*以下是getter和setter 方法*/
}
对于sql语句:
selectUser
===
select u.*,r.name r_name from user u left join role r on u.roleId=r.id .....
执行查询的时候
List<User> list = sqlManager.select(sqlId,User.class,paras);
for(User user:list){
System.out.println(user.getId());
System.out.println(user.get("rName"));
}
程序可以通过get方法获取到未被映射到pojo的值,也可以在模板里直接 ${user.rName} 显示(对于大多数模板引擎都支持)
另外一种更自由的实现混合模型的方法是在目标Pojo上采用注解@Tail,如果注解不带参数,则默认会调用set(String,Object) 方法来放置额外的查询属性,否则,依据注解的set参数来确定调用方法
@Tail(set="addValue")
public class User {
private Integer id ;
private Integer age ;
public User addValue(String str,Object ok){
ext.put(str, ok);
return this;
}
Map 模型
BeetlSQL支持将结果集映射成Map而不需要创建具体POJO
List<Map> xxx = sqlManager.select(sqlId,Map.class);
或者是在Mapper中定义
@Select
public List<Map> selectUsers();
BeetSQL作为ORM工具不推荐使用Map,Map难以表达数据的结构,开发和后期维护都非常难。另外从数据库返回的的字段映射到Map,类型是不可控的,类型取决于JDBC。而如果使用POJO,那么Beetlsql将会把数据库查询结果映射成期望类型。
动态模型
当系统自动生成表时候,又不想使用Map作为beetlsql参数来操作这些表,可以使用BeetSQL提供的扩展包,向操作POJO一样操作这些动态表
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>sql-dynamic-table</artifactId>
<version>${version}</version>
</dependency>
使用方式举例如下,使用DynamicEntityLoader来获取一个动态表对应的动态类
SQLManager sqlManager = getSQLManager();
int max = 5;
//创建5个表
for(int i=0;i<max;i++){
String dml="create table my_table"+i+"(id int NOT NULL"
+ ",name varchar(20)"
+ ",PRIMARY KEY (`id`)"
+ ") ";
sqlManager.executeUpdate(new SQLReady(dml));
}
// 配置加载类,使用默认的BassEntity,也可以使用任何类作为父类,比如MyOfficeEntity
DynamicEntityLoader<BaseEntity> dynamicEntityLoader = new DynamicEntityLoader(sqlManager);
for(int i=0;i<max;i++){
Class<? extends BaseEntity> c = dynamicEntityLoader.getDynamicEntity("my_table"+i);
long count = sqlManager.allCount(c);
System.out.println(count);
}
for(int i=0;i<max;i++){
Class<? extends BaseEntity> c = dynamicEntityLoader.getDynamicEntity("my_table"+i);
BaseEntity obj = c.newInstance();
obj.setValue("id",1);
obj.setValue("name","hello");
sqlManager.insert(obj);
}
dynamicEntityLoader.getDynamicEntity方法本质上会根据表,生成一个临时的POJO类(参考 beetlsql代码生成),,返回的是baseEntity的一个子类(可以通过debug 看到这个子类,就是一个跟表对应的JavaBean)
使用此类操作的时候,beetlsql就当成一个普通的POJO类,如下代码allCout将访问my_table 表
Class<? extends BaseEntity> c = dynamicEntityLoader.getDynamicEntity("my_table");
long count = sqlManager.allCount(c);
BaseEntity是扩展包内置的的类,所有生成的类都是BaseEntity的子类,比如my_table表
@Table(name="mytable")
public class MyTable exends BaseEntity{
@AssingId
private Integer id;
private String name;
}
BaseEntity具有setValue和getValue方法,通过反射用于程序设置POJO属性.
public class BaseEntity {
public void setValue(String attrName,Object value){
MethodInvoker methodInvoker = ObjectUtil.getInvokder(this.getClass(),attrName);
if(methodInvoker==null){
throw new IllegalArgumentException("不存在的属性 "+attrName);
}
methodInvoker.set(this,value);
}
public Object getValue(String attrName){
MethodInvoker methodInvoker = ObjectUtil.getInvokder(this.getClass(),attrName);
if(methodInvoker==null){
throw new IllegalArgumentException("不存在的属性 "+attrName);
}
return methodInvoker.get(this);
}
}
dynamicEntityLoader.getDynamicEntity也可以使用其他父类作为生成的POJO的父亲对象,比如
Class<? extends MyOfficeEntity> c = dynamicEntityLoader.getDynamicEntity("my_table"+i,MyOfficeEntity.class);
可以有如下定义,假设所有表都有id列
public abstract MyOfficeEntity {
public abstract Integer getId();
public abstract void setId(Integer id);
public abstract String getName();
public abstract void setName(String name);
}
模型其他注解
- @UpdateIgnore 作用于属性上,当使用内置的更新语句的时候,会忽略此字段
- @InsertIgnore 作用于属性上,当使用内置的插入语句的时候,会忽略此字段
- @LogicDelete,作用在属性上,告诉BeetlSQL,deleteById语句 生成更新语句,并设置此属性字段为LogicDelete指定的值
@Data
@Table(name="sys_user")
public class SysUser{
@AutoId
Integer id;
String name;
@LogicDelete(1)
Integer flag;
}
逻辑删除改变了deleteById的sql,但对于其他内置查询,没有把逻辑删除作为过滤条件,依然能查询出来。这点不同于mybatis-plus。如果需要过滤,请参考扩展BeetlSQL3
beetlsql 认为逻辑删除依然是业务的一部分,不应该干涉
- @Version
注解@Version作用在类型为int,long的属性或者getter方法上,用于乐观锁实现。
public class Credit implements Serializable{
private Integer id ;
private Integer balance ;
@Version
private Integer version ;
当调用内置的updateById,或者updateTemlateById的时候,被@Version注解的字段将作为where条件的一部分
┏━━━━━ Debug [credit._gen_updateTemplateById] ━━━
┣ SQL: update `credit` set `balance`=?, `version`=`version`+1 where `id` = ? and `version` = ?
┣ 参数: [15, 1, 5]
┣ 位置: org.beetl.sql.test.QuickTest.main(QuickTest.java:38)
┣ 时间: 4ms
┣ 更新: [1]
┗━━━━━ Debug [credit._gen_updateTemplateById] ━━━
- @Auto 标识此列是一个自增列
@Column("number");
@Auto
private Long number
- @Seq 标识此列是按数据库序列赋值
@Column("number");
@Seq("xxx_seq")
private Long number
@View注解 (不常用,未来将取消)
在BeetlSQL内置查询语句里,返回的结果集是POJO和列的交集,使用@View注解,可以进一步限定内置SQL语句需要返回的列
如下MyUser对象,有三个字段,id和name,以及photo。当sqlManager发起内置的查询的时候,这三个字段都会返回结果集,如果你想在某些查询下排除photo字段,可以使用@View
@Data
@Table(name="user")
public class MyUser {
static interface Simple{}
static interface Complex{}
@AssignID
@View(Simple.class,Complex.class)
private Integer id;
@View(Simple.class)
private String name;
@View(Complex.class)
private byte[] photo;
}
如上name属性,当view是Simple.class的时候将返回,photo属性则只在view是Complex.class的返回。 id则总是返回。
SQLManager.viewType指定此次查询的view,如下指定Simple
MyUser cacheItem = SQLManager.viewType(MyUser.Simple).single(MyUser.class,1)
如下指定Complex
MyUser userWithPohot = SQLManager.viewType(MyUser.Complex).single(MyUser.class,1)
安全扩展注解
BeetlSQL提供了安全扩展包支持,MD5,AES,DES等字段加密和解密
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>sql-bean-encrypt</artifactId>
<version>${version}</version>
</dependency>
使用方式举例如下
String code;
@MD5(saltProperty = "code")
String content;
@AES // 或者 @DES
String phone;
采用MD5,可选的使用salt加密。
- saltProperty,使用当前POJO的某个字段
- salt 使用固定的某个字符串常量
对称加密注解AES和DES,以及SM4, 加密需要的key来自与EncryptConfig,你可以使用默认的,或者自己配置Key
public class EncryptConfig {
static Map<EncryptType,String> prop = new HashMap<>();
static {
prop.put(EncryptType.AES,"19780214xiandafu");
prop.put(EncryptType.DES,"19780214xiandafu");
prop.put(EncryptType.SM4,"19780214xiandafu");
}
public static synchronized void config(EncryptType key,String value){
prop.put(key,value);
}
public static synchronized String get(EncryptType key){
return prop.get(key);
}
}
安全扩展包是加密注解实现了AttributeConvert接口,可以参考ql-bean-encrypt的源码或者的AttributeConvert文档编写自己的加密注解