江苏南大先腾J2EE持久化框架(二)Spring JDBC框架[附源码]

概述

南大先腾J2EE持久化框架(一) 中对持久化平台做了一个总体的介绍。和hibernate、myBatis不同,先腾Spring JDBC框架在spring-jdbc的基础上进行了深度开发,包括对jpa一个常用子集的实现和扩展,对参数驱动SQL的支持。使得开发效率的到很到的提高;前者解决增删改操作,后者专用于解决查询操作。
本节用一个员工工作经历的示例来展示如何在项目中使用先腾的jdbc持久化平台。

主要特点汇总

  1. 实现jpa,增删改不用编写sql语句,可以灵活的应对数据重构,另外因为没有直接写sql语句,所以可以做到跨数据库兼容的问题,目前我们支持主要的国产数据库,比如:达梦、金仓等等。
  2. 查询采用参数驱动SQL可以做到灵活的配置数据权限,同事也可避免拼接SQL,并且参数驱动sql中的参数预处理可以减少很多数据处理程式代码。
  3. 这个框架基于spring jdbc所以可以使用spring tx的事务管理机制。
  4. 丰富的工具类,可以统一实现数据逻辑删除,数据删改时版本校验等等特性。

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这个懒加载标签
    */
    @Basic( fetch = FetchType.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会抛异常),说明修改失败。

简单(单对象)修改

这个修改 默认只修改 worker中的非null字段,也就是说无法把数据库中已有数据的字段修改为null,如果需要修改为null则需要调用方法 updateObjectWithNullField 或者调用下面的“修改部分属性方法”并制定对应的属性名。

    //修改
	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);

需要说明的:

  1. filterField中key前面括号部分是,参数的预处理工作,参数参数驱动SQL(二)变量预处理和转换
  2. 所有的页面查询都返回 JSONArray。
  3. 所有的页面查询都是分页,分页信息 在pageDesc中,并且这个参数是 in、out形式,执行完查询,pageDesc中的totalRows会自动被设置为所有符合条件的总数,所以一次页面查询其实执行了两条sql语句;框架会根据数据库类型自动生成分页查询语句和求总数的查询语句。
  4. 这种语句构建法不同的条件之间只能用与(and)方式连接。
  5. 这种查询只能查询对象对应的单表。

参数驱动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);

        .......
    }

这个查询非常灵活,并且对开发来说只要写一个参数驱动查询语句就可以,不需要考虑分页和求总数。

其他功能

BaseDaoImplDatabaseOptUtils类中封装了很多实用的方法,大概分以下几类:

  1. 通用的存储过程调用方法。
  2. 执行原生DML语句。
  3. 批量增删改操作。
  4. 各种查询接口。
  5. 在DDLOperationsWorks中还封装了数据库的各种DDL操作方法。

权限控制

参见参数驱动SQL(三)数据范围权限

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值