概述
南大先腾J2EE持久化框架(一) 中对持久化平台做了一个总体的介绍。和hibernate、myBatis不同,先腾Spring JDBC框架在spring-jdbc的基础上进行了深度开发,包括对jpa一个常用子集的实现和扩展,对参数驱动SQL的支持。使得开发效率的到很到的提高;前者解决增删改操作,后者专用于解决查询操作。
本节用一个员工工作经历的示例来展示如何在项目中使用先腾的jdbc持久化平台。
Maven 引用
配置框架bao的依赖
<dependency>
<groupId>com.centit.framework</groupId>
<artifactId>centit-persistence-jdbc</artifactId>
</dependency>
<!-- 这个config不是必须的,如果用xml的配置方式就不需要 -->
<dependency>
<groupId>com.centit.framework</groupId>
<artifactId>centit-persistence-jdbc-config</artifactId>
</dependency>
配置说明
在system.properties中配置相关参数
jdbc.user =metaform
jdbc.password =××××××
#用H2数据库比较方便测试
jdbc.driver = org.h2.Driver
jdbc.url = jdbc:h2:file:/D/Projects/RunData/demo_home/iph2db2;MODE=MYSQL
jdbc.dialect=org.hibernate.dialect.H2Dialect
#数据库datasource属性配置
jdbc.maxActive = 10
jdbc.maxIdle = 3
jdbc.maxWait = 1000
jdbc.defaultAutoCommit = true
jdbc.removeAbandoned = true
jdbc.removeAbandonedTimeout = 60
jdbc.validationQuery = select 1 from dual
#这个是数据库版本管理,这个不是必须的,可以关闭
flyway.enable=true
flyway.sql.dir=classpath:migration/h2
示例模型
这里用员工和员工的职业生涯来作为示例来介绍。Po对象主要的作用是通过jpa注解将业务对象和数据库中的表对应起来。po对象实现EntityWithDeleteTag接口,表示这个对象需要逻辑删除;Po对象实现了EntityWithVersionTag接口,表示这个对象更新前必须做版本校验,这可以作为一个乐观锁。源码如下:
/**
* 员工信息表
*/
@Entity
@Table(name = "T_OFFICE_WORKER")
public class OfficeWorker implements
/*逻辑删除标记接口*/ EntityWithDeleteTag,/*版本更新接口*/EntityWithVersionTag,Serializable {
/**
* 主键,在创建时根据序列S_WORKER_ID自动生成
*/
@Column(name = "WORKER_ID")
@ValueGenerator( strategy= GeneratorType.SEQUENCE, value = "S_WORKER_ID")
private String workerId;
@Column(name = "WORKER_NAME")
private String workerName;
@Column(name = "WORKER_SEX")
private String workerSex;
@Column(name = "WORKER_BIRTHDAY")
private Date workerBirthday;
/**
* 头像lob字段,这个默认查询时不查询这个字段,所以用Lazy这个懒加载标签
*/
@Lazy
@Column(name = "HEAD_IMAGE")
private byte[] headImage;
@Column(name = "IS_DELETE")
private String isDelete;
/**
* 更新版本号,初始版本为 000001
*/
@Column(name = "VERSION_NO")
@ValueGenerator(strategy= GeneratorType.CONSTANT, value = "000001" )
private String versionNo;
/**
* 创建时间,新建时自动赋值,更加函数获取当前时间
*/
@Column(name = "CREATE_DATE")
@ValueGenerator( strategy= GeneratorType.FUNCTION, value = "today()" )
private Date createDate;
/**
* 最后更新时间,每次数据有更改是都会赋值
*/
@Column(name = "LAST_UPDATE_DATE")
@ValueGenerator( strategy= GeneratorType.FUNCTION, value = "today()",
condition = GeneratorCondition.ALWAYS, occasion = GeneratorTime.ALWAYS )
private Date lastUpdateTime;
/**
* 这个 targetEntity 必须指定,因为java反射获取不到泛型的实体类型
* name 为主表的字段名
* referencedColumnName 为子表的字段名
* 多字段引用使用 JoinColumns 注解
*/
@OneToMany(targetEntity=Career.class)
@JoinColumn(name="WORKER_ID", referencedColumnName="WORKER_ID")
private List<Career> workerCareers;
/**
* 判断是否为已删除
* @return 是否为已删除
*/
@Override
public boolean isDeleted() {
return "T".equals(isDelete);
}
/**
* 设置删除标志
*
* @param isDeleted 删除标志
*/
@Override
public void setDeleted(boolean isDeleted) {
isDelete = isDeleted?"T":"F";
}
/**
* 计算下一个版本号
* 版本号可以为任何类型,但是必须支持sql语句中的 =
* @return 下一个版本号
*/
@Override
public Object calcNextVersion() {
return StringBaseOpt.nextCode(versionNo);
}
/**
* 返回记录版本的属性,这个属性必须和数据库表中的某个字段对应,
* 也就是说这个属性必须有 @Column
* @return 版本字段属性名
*/
@Override
public String obtainVersionProperty() {
return "versionNo";
}
//getter setter 方法
........
}
/**
* 职业生涯表
*/
@Entity
@Table(name = "T_CAREER")
public class Career implements Serializable {
/**
* 主键,在创建时生成随机UUID
*/
@Column(name = "CAREER_ID")
@ValueGenerator( strategy= GeneratorType.UUID)
String careerId;
/**
* 外建
*/
@Column(name = "WORKER_ID")
private String workerId;
@Column(name = "CORPORATE_NAME")
private String corporateName;
@Column(name = "BEGIN_DATE")
private Date beginDate;
@Column(name = "END_DATE")
private Date endDate;
//getter setter 方法
........
}
增删改
框架中约定增删改操作全部写在Dao中,共业务层(server层)调用。Jdbc框架封装了一个抽象类BaseDaoImpl对常用的操作进行封装,所以没有特殊需求的Dao代码都非常简单,只要简单的继承一下这个抽象类就可以了。Dao示例代码如下:
public class OfficeWorkerDao extends BaseDaoImpl<OfficeWorker, String> {
@Override
public Map<String, String> getFilterField() {
if (filterField == null) {
filterField = new HashMap<>();
filterField.put("(like)workerName", "WORKER_NAME like :workerName");
filterField.put("(date)birthdayBegin", "WORKER_BIRTHDAY>= :createDateBeg");
filterField.put("(nextday)birthdayEnd", "WORKER_BIRTHDAY< :birthdayEnd");
}
return filterField;
}
}
public class CareerDao extends BaseDaoImpl<Career, String> {
@Override
public Map<String, String> getFilterField() {
return null;
}
}
类OfficeWorkerDao代码中函数getFilterField在下一节页面查询中需要。
新建(增)
增加前,框架会自动根据ValueGenerator中的指令为po附上适当的值。
OfficeWorker worker = new OfficeWorker();
//设置所有必填的,并且没有ValueGenerator注解的属性
workerDao.saveNewObject(worker);
删除
如果Po实现EntityWithDeleteTag就会逻辑删除,如果实现了EntityWithVersionTag接口回校验版本号,避免数据操作冲突。
//删除,如果worker对象实现了EntityWithDeleteTag接口则并没有在数据库中删除,
//而是调用setDeleted(true),然后update。
//如果没有实现这个接口就直接从数据库中删除,效果就和deleteObjectForce一样
workerDao.deleteObject(worker);
workerDao.deleteObjectById(worker.getWorkerId());
//强制删除, 从数据库中删除,不管是否实现EntityWithDeleteTag接口
workerDao.deleteObjectForce(worker);
workerDao.deleteObjectForceById(worker.getWorkerId());
// 根据属性逻辑删除,这个属性是需要严格相等才能删除,
workerDao.deleteObjectsByProperties(CollectionsOpt.createHashMap(
"sex","男" ,"workerBirthday",DatetimeOpt.createUtilDate(1980,12,12)
));
//根据属性强制删除
workerDao.deleteObjectsForceByProperties(CollectionsOpt.createHashMap(
"sex","男" ,"workerBirthday",DatetimeOpt.createUtilDate(1980,12,12)
));
修改
因为实现了EntityWithVersionTag接口,所以修改前会做版本校验,如果返回<=0 或者抛异常(IBM DB2会抛异常),说明修改失败。
简单(单对象)修改
//修改,
workerDao.updateObject(worker);
只修改对象中的部分属性
//只修改部分属性,比如只修改 姓名和性别,这个属性可以是对象的属性名,也可以是对应的表的字段名
workerDao.updateObject(CollectionsOpt.createList("workerName","sex"),worker);
Merge新建或者修改
//先检查是否存在,如果存在更新,否在新建
workerDao.mergeObject(worker);
对子表的修改
业务开发的时候经常会碰到对子表的替换的场景。比如上面的例子中,前端修改了职业生涯(career)信息,可能有修改的、新增的还有删除的;以往程序员都会偷懒,将子表对应的记录全部删除再将新的全部添加,我认为这个不是一个好的注意,会在数据库中留下很多碎片信息。框架用一个函数就解决了这个问题,它同过比较子表的主键来决定update、insert或delete操作。
//对子表进行替换 操作,它先根据主表的主键在子表中查出数据库中的记录,
//再根据子表的主键对比来决定执行update、insert或delete操作
int n = workerDao.saveObjectReference(worker, "workerCareers");
查询(查)
业务系统中数据持久化查询是最为复杂的,我们查询表的目的一般可以分为两类:操作查询和页面查询。
操作查询
操作查询,就是查询出来的数据是需要进行业务逻辑操作的,所以查询处理的数据需要转化为对象,这样就可以进行面向对象操作。这类查询BaseDaoImpl也已经做了很多的封装,所以不需要写额外的代码实现。基类中所有返回List 并且是以listObjects开头的函数都是这类查询。常用的就下面几个函数:
//根据主键查询
workerDao.getObjectById(workerId)
//查询所有的记录,这个如果表太大一般不要使用
workerDao.listObjects();
//根据属性查询
workerDao.listObjectsByProperties(CollectionsOpt.createHashMap(
"sex","男" ,"workerBirthday",DatetimeOpt.createUtilDate(1980,12,12)));
子表查询
jdbc框架在查询对象是,默认是不查询子表的,对上面的类子来说workerCareers属性始终是空的。如果需要查询子表需要这样做:
//在查询对象后获取对象的子表信息,会遍历所有的OneToMany、OneToOne、ManyToOne注解
workerDao.getObjectWithReferences(workerId);
//获取对象中指定属性的子表信息
workerDao.fetchObjectReferences(worker,"workerCareers");
//获取对象的所有属性的子表信息
workerDao.fetchObjectReference(worker);
Lazy字段查询
和子表一样,lazy字段默认查询也是不返回的,需要通过一下代码获取:
//获取指定的懒加载字段
workerDao.fetchObjectLazyColumn(worker,"headImage");
//获取所有的懒加载字段
workerDao.fetchObjectLazyColumns(worker);
页面查询
页面查询是开发过程中使用最多的一种查询,这类查询的目标式为了向用户展示信息的,所以查询结果不需要转行为对象,因为我们框架后台接口采用的式restful+json的形式,查询返回的json结果是最为合适的。框架提供两种形式来做页面查询,一种是语句构建法,另一种是用参数驱动SQL直接访问数据库。
语句构建法
回顾一下增删改一节中OfficeWorkerDao中的getFilterField中的代码:
filterField.put("(like)workerName", "WORKER_NAME like :workerName");
filterField.put("(date)birthdayBegin", "WORKER_BIRTHDAY>= :createDateBeg");
filterField.put("(nextday)birthdayEnd", "WORKER_BIRTHDAY< :birthdayEnd");
假设我们在页面上有三个属性输入框workerName、birthdayBegin、birthdayEnd前台会根据输入生成一个对应的Map,后台会根据这map中的key来判断,如果key在filterField的key中就将对应的条件过滤语句添加到查询中。后台可以通过下面代码来实现查询:
workerDao.listObjectsAsJson(/*前端输入的查询参数*/map,/*PageDesc 分页信息 */pageDesc);
需要说明的:
- filterField中key前面括号部分是,参数的预处理工作,参数参数驱动SQL(二)变量预处理和转换。
- 所有的页面查询都返回 JSONArray。
- 所有的页面查询都是分页,分页信息 在pageDesc中,并且这个参数是 in、out形式,执行完查询,pageDesc中的totalRows会自动被设置为所有符合条件的总数,所以一次页面查询其实执行了两条sql语句;框架会根据数据库类型自动生成分页查询语句和求总数的查询语句。
- 这种语句构建法不同的条件之间只能用与(and)方式连接。
- 这种查询只能查询对象对应的单表。
参数驱动SQL
由于语句构造法智能查询单表,并且不同的条件只能用and连接,所以框架还提供了参数驱动sql查询方式,这类方法没有集成在BaseDaoImpl类中,而是在类DatabaseOptUtils中的一组listObjectsByParamsDriverSqlAsJson方法,它们的差别就是参数个数不一样。
/** 这时一个参数齐全的查询接口
* 参数驱动sql查询
* @param baseDao 任意dao对象,需要用dao中的session访问数据库
* @param querySql 查询语句:参数驱动sql,不需要写分页查询,框架会自动转换为分页查询
* @param fieldNames 这个是返回结果放到json中的属性名,这个不是必须的,缺省是通过sql语句中的字段名自动转换成小驼峰的属性名
* @param queryCountSql 查询总数的参数驱动sql语句,这个也不是必须的,如果缺省,系统会自动根据查询语句来生成
* @param namedParams 这个是前台输入的 查询参数
* @param pageDesc 这个式前台输入的 分页信息
* @return JSONArray
*/
public static JSONArray listObjectsByParamsDriverSqlAsJson(BaseDaoImpl<?, ?> baseDao,
String querySql, String[] fieldNames , String queryCountSql,
Map<String, Object> namedParams, PageDesc pageDesc ) {
.......
}
/** 这是最常用的查询接口,对开发来说只要写一个参数驱动查询语句就可以,不需要考虑分页和求总数
* 参数驱动sql查询
* @param baseDao 任意dao对象,需要用dao中的session访问数据库
* @param querySql 查询语句:参数驱动sql,不需要写分页查询,框架会自动转换为分页查询
* @param namedParams 这个是前台输入的 查询参数
* @param pageDesc 这个式前台输入的 分页信息
* @return JSONArray
*/
public static JSONArray listObjectsByParamsDriverSqlAsJson(BaseDaoImpl<?, ?> baseDao, String querySql,
Map<String, Object> namedParams, PageDesc pageDesc ) {
QueryAndNamedParams qap = QueryUtils.translateQuery( querySql, namedParams);
.......
}
这个查询非常灵活,并且对开发来说只要写一个参数驱动查询语句就可以,不需要考虑分页和求总数。
其他功能
BaseDaoImpl和DatabaseOptUtils类中封装了很多实用的方法,大概分以下几类:
- 通用的存储过程调用方法。
- 执行原生DML语句。
- 批量增删改操作。
- 各种查询接口。
- 在DDLOperationsWorks中还封装了数据库的各种DDL操作方法。