OA系统使用weaver.conn.RecordSet类和weaver.conn.RecordSetTrans类来执行数据库操作,其中RecordSet不支持事务,RecordSetTrans支持事务。这两个类的代码量非常大,在没有文档和注释的情况下,仅通过逆向分析,我们根本无法保证能正确地使用其特性。因此,为了更加可控,我们自己实现数据库层。
数据源管理器
DataSourceManager类负责初始化数据源,并从数据源获取数据库连接。启动应用时,会调用DataSourceManager执行初始化。
初始化数据源:我们使用Druid(在OA系统的三方库中)作为数据库连接池。读取OA系统的数据源配置文件weaver.properties,用其中的配置创建DruidDataSource实例,并保存在DataSourceManager的静态属性中。
获取数据库连接:从DruidDataSource实例上获取数据库连接。
线程的数据库连接
ConnectionHolder类是一个ThreadLocal容器,用于保存当前线程的数据库连接,确保一条线程的所有操作都在同一个连接中,即在同一个事务中。
ConnectionHolder提供获取连接、关闭连接、提交事务、回滚事务这4个方法。
数据库服务
应用没有专门的DAO层框架,直接用JDBC模式操作数据库又很繁琐,因此,我们以服务的形式提供通用的数据库操作功能。
DatabaseService类是通用的数据库服务类,封装了通用的查库和写库操作的方法。此服务可以通过IoC容器注入到其他类中,应用中所有的数据库操作最终都是通过调用DatabaseService来完成。
DatabaseService使用Apache Commons DBUtils(在OA系统的三方库中)的QueryRunner来执行SQL。执行时,总是从ConnectionHolder获取数据库连接。
DatabaseService类提供以下查库方法:
| 方法 | 描述 | 查询结果为0行 | 查询结果为1行 | 查询结果为多行 |
|---|---|---|---|---|
queryRowOrNull | 获取唯一行 | 返回null | 返回唯一行 | 抛出异常 |
queryRow | 获取唯一行 | 抛出异常 | 返回唯一行 | 抛出异常 |
queryFirstRowOrNull | 获取第一行 | 返回null | 返回第一行 | 抛出异常 |
queryFirstRow | 获取第一行 | 抛出异常 | 返回第一行 | 抛出异常 |
queryRows | 获取行列表 | 返回行列表(空) | 返回行列表(1行) | 返回行列表(多行) |
DatabaseService类提供以下写库方法:
insert:插入update:更新delete:删除
所有查库和写库方法都支持参数String sql, Object... params,防止注入攻击。
通用的行数据结构
为了通用地表示DatabaseService查库方法查询到的任意结构的行数据,我们定义了Row类。
Row类是一个Map,类似于JavaScript的对象、Python的字典,可以表示任意的数据库行、实体、对象等数据结构。用Map表示对象,可以无需对属性做预定义,可以按需自由地增删属性,这带来了一些灵活性(例如:可以构造更新部分字段的更新数据对象,可以将一种对象修改为另一种对象,可以在字段增减时热更新程序),也降低了一些确定性(例如:IDE因为不知道有哪些属性而无法做代码提示和检查错误)。
Row类继承自LinkedHashMap<String, String>。考虑实际使用场景,我们对其做了一些约束。
键,即对象的属性名,是大小写无关的。通过重写Map的get()、put()等方法,统一将键转换为小写形式,来实现大小写无关性。
值,即对象的属性值,存储为字符串。通过重写Map的put()等方法,实现无论给的值是何种类型,都统一转换为字符串存储,其中null会存储为空字符串。通过增加getInt()、getBigDecimal()等方法,提供获取合适数据类型的功能。这种设计,屏蔽了数据类型的不确定性(例如:小数是Double还是BigDecimal),也使其不支持嵌套的对象。
对于Map具有的、但对对象来说意义不大的方法,如compute(),我们重写之并直接抛异常,以阻止其被使用。
应用中的各种业务数据类都继承自Row,或者直接使用Row。DatabaseService的查库方法支持泛型,可以返回指定的业务数据类(Row或Row的子类)。