类orm的方案

编写dal层代码,java领域比较火的两个阵营:JPA(Hibernate)和MyBatis。JPA是java制定的一种ORM规范;MyBatis较轻量,还需要开发人员理解sql。

国内的情况,mybatis会更流行一些。能够胜出的点,大概是因为:可定制性更强,更易于进行性能优化。
但是,mybatis用久了,需要不断修改xml,又引起了很多人的不爽。随后基于mybatis之上的工具,如mybatis-plus,tk.mybatis便被提出来。使用lambda的函数,替代动态xml模板。
同时,采用类似做法的也有很多其他语言的框架,比如kotlin的Exposed、ktorm,golang的gorm。

简单介绍下这几个框架的用法:

  • mybatis-plus和tk.mybatis比较类似,使用lambda函数描述sql中的一些语法含义,替代了mybatis的动态xml模板层
  • Exposed和ktorm,kotlin写的,和上面两个比较像。一个比较明显区别,没有基于mybatis

spring jdbc

除了上面类orm的框架,还有spring提供的jdbcTemplate,对JDBC进行简单的封装,提供了数据层操作最基本的一些能力,诸如:动态数据源、返回结果到java对象的映射等。
spring jdbc的用法,没有动态xml,需要在java代码里手写sql。sql一长,换行拼接的方式,又非常不具有可读性。所以,被大规模正式地使用的情况,还是比较少。
所有,在github也有类似spring-data-mybatis-mini的小工具,基于spring jdbc + mybatis的xml动态模板,实现动态sql的能力。

那么,在上面两种方式之外,有没有一种中间路径,比orm更轻,比spring jdbc更易用的第三个选择?

第三种方案:kotlin + spring jdbc

先上代码:

@Repository
abstract class BaseDAO {

    @Resource
    private lateinit var sequenceService: SequenceService

    fun createEntity(tableName: String, entityDO: EntityDO, jdbcInsert: SimpleJdbcInsert): Long {
        if (entityDO.id == null) {
            entityDO.id = sequenceService.nextValue(tableName)
        }
        entityDO.gmtCreate = Date()
        entityDO.gmtModified = Date()
        val affectRow: Number = jdbcInsert.execute(BeanPropertySqlParameterSource(entityDO))
        if (affectRow != 1) {
            throw RuntimeException("create jdbcInsert.execute fail")
        }

        return entityDO.id
    }
}

@Repository
class TenantStaffDAO : BaseDAO() {

    private val tableName: String = "tenant_staff"

    @Resource
    private lateinit var jdbcTemplate: JdbcTemplate

    private lateinit var jdbcInsert: SimpleJdbcInsert

    private lateinit var jdbcUpdate: SimpleJdbcUpdate

    @PostConstruct
    fun init() {
        jdbcInsert = SimpleJdbcInsert(jdbcTemplate).withTableName(tableName)
        jdbcUpdate = SimpleJdbcUpdate(jdbcTemplate).withTableName(tableName).ukColumns("tenant_id", "id")
    }

    fun create(staffDO: TenantStaffDO): Long {
        return super.createEntity(tableName, staffDO, jdbcInsert)
    }

    fun get(tenantId: Long, staffId: Long): TenantStaffDO? {
        val results = jdbcTemplate.query("select * from tenant_staff where tenant_id = ? and id = ?",
                arrayOf(tenantId, staffId), BeanPropertyRowMapper(TenantStaffDO::class.java))
        return DataAccessUtils.singleResult(results)
    }

    fun getByUid(tenantId: Long, uid: Long): TenantStaffDO? {
        val results = jdbcTemplate.query("select * from tenant_staff where tenant_id = ? and uid = ?",
                arrayOf(tenantId, uid), BeanPropertyRowMapper(TenantStaffDO::class.java))
        return DataAccessUtils.singleResult(results)
    }

    fun batchGet(tenantId: Long, staffIdList: List<Long>): List<TenantStaffDO>? {
        val sql = """
             select * from tenant_staff 
             where tenant_id = ? 
             and id in ${staffIdList.joinToString(prefix = "(", separator = ", ", postfix = ")") { "?" }}
            """
        val args = arrayListOf<Any>()
        args.add(tenantId)
        args.addAll(staffIdList)
        return jdbcTemplate.query(sql, args.toArray(), BeanPropertyRowMapper(TenantStaffDO::class.java))
    }

    fun listStaffs(tenantId: Long, size: Int, offset: Long): List<TenantStaffDO>? {
        val sql = "select * from tenant_staff where tenant_id = ? order by id desc limit ? offset ?"
        return jdbcTemplate.query(sql, arrayOf(tenantId, size, offset), BeanPropertyRowMapper(TenantStaffDO::class.java))
    }

    fun listTenants(uid: Long): List<TenantStaffDO>? {
        val sql = "select * from tenant_staff where uid = ?"
        return jdbcTemplate.query(sql, arrayOf(uid), BeanPropertyRowMapper(TenantStaffDO::class.java))
    }

    fun singleUpdate(staffDO: TenantStaffDO): Boolean {
        staffDO.gmtModified = Date()
        return jdbcUpdate.singleUpdate(staffDO)
    }

    fun delete(tenantId: Long, id: Long): Int {
        return jdbcTemplate.update("delete from tenant_staff where tenant_id = ? and id = ?", tenantId, id)
    }

    fun count(tenantId: Long): Int {
        return jdbcTemplate.queryForObject("select count(*) from tenant_staff where tenant_id = ?",
                arrayOf(tenantId), Int::class.java)
    }

}

解决了dal层最基本需求

  • 数据库结果到java对象映射
  • 支持动态sql能力,比如in查询的列表,动态拼接
  • 使用SimpleJdbcInsert进行DO对象的写入,可以做到不用关心表的字段
  • 另外,spring-jdbc没有提供SimpleJdbcUpdate的实现,简单实现了一个,可以达到一样的效果:传入DO对象进行update

几个好处

  • 和java混合编程,基本可以无缝衔接
  • 直接编写程序员最熟悉的sql,而不是去理解一个xml或者lambda的转换层
    • 不需要了解xml动态模板语法
    • 特殊字符无需转义
  • 底层基于spring jdbc,简单纯粹
  • 借助kotlin的字符串模板功能,基本可以做到mybatis的xml模板同样的效果。支持if、for等

存在的问题

  • java编写的DO类,如果使用了lombok,kotlin dao层代码在引用DO对象的属性时,会报编译错误。目前lombok和kotlin还无法在一起完美使用。解决办法是,在DO类里将kotlin代码使用的对象,手动生成setter、getter

另外,附上文中提到的几个框架的介绍链接