第13章 统一的数据访问异常层次体系

本章内容

  • DAO模式的背景

  • 梦想照进现实

  • 发现问题,解决问题

  • 不重新发明轮子

要了解Spring为什么要提供统一的数据访问异常层次体系,我们得先从DAO模式说起。

DAO模式的背景

不管是一个逻辑简单的小软件系统,还是一个关系复杂的大软件系统,都可能涉及对相关数据的访问和存储,而这些数据的存储机制和访问方式往往随场景不同而各异。

为了统一和简化相关的数据访问操作,J2EE核心模式提出了DAO(Data Access Object,数据访问对象)模式。使用DAO模式,可以完全分离数据的访问和存储,很好地屏蔽了各种数据访问方式的差异性。

不论数据存储在普通的文本文件或者csv文件,还是关系数据库(RDBMS)或者LDAP(Light weight Directory Access Protocol,轻量级目录访问协议)系统中,使用DAO模式访问数据的客户端代码可以完全忽视这种差异,而以统一的接口来访问相应数据。

空话多说无益,我们还是来看一个具体的应用DAO模式的场景吧!对于大部分软件系统来说,访问顾客信息是大家经常接触的。我们就以访问顾客信息为例,看一下如何使用DAO模式。

使用DAO模式访问顾客数据,首先需要声明一个数据访问对象接口定义,如下代码所示:

public interface ICustomerDao {Customer findCustomerByPK(String customerId);void updateCustomerStatus(Customer customer);// ....
}

对于客户端代码,即通常的服务层代码来说,只需要声明依赖的DAO接口即可,即使数据访问方式方式发生了改变,只需要改变相关的DAO实现方式,客户端代码不需要做任何的调整。

依赖DAO接口的CustomerService类定义:

public class CustomerService {private ICustomerDao customerDao;// 禁用客户活动public void disableCustomerCampain(String customerId) {Customer customer = getCustomerDao().findCustomerByPK(customerId);customer.setCampainStatus(CampainStatus.DISABLE);getCustomerDao().updateCustomerStatus(customer);}public ICustomerDao getCustomerDao() {return cus tomerDao;}public void setCustomerDao(ICustomerDao customerDao) {this.customerDao = customerDao;}
}

通常情况下,顾客信息存储在关系数据库中,所以,相应的我们会提供一个基于JDBC的DAO接口实现类:

public class JDBCCustomerDao implements ICustomerDao {@Overridepublic Customer findCustomerByPK(String customerId) {// TODO Auto-generated method stubreturn null;}@Overridepublic void updateCustomerStatus(Customer customer)    {// TODO Auto-generated method stub}
}

可能随着系统需求的变更,顾客信息需要转移到LDAP服务,这时就需要提供一个基于LDAP的数据访问对象,如下所示:

public class LdapCustomerDao implements ICustomerDao {@Overridepublic Customer findCustomerByPK(String customerId) {// TODO Auto-generated method stubreturn null;}@Overridepublic void updateCustomerStatus(Customer customer)    {// TODO Auto-generated method stub}
}

即使数据访问接口的实现类随着需求而发生变化,客户端代码(这里是CustomerService)也可以完全忽视这种变化,唯一需要变动的地方可能只是Factory对象的几行代码,甚至只是IoC容器配置文件中简单的class类型替换而已,而客户端代码无需任何变动。

所以,DAO模式对屏蔽不同数据访问机制的差异性起到举足轻重的作用。

梦想照进现实

为了简化概念的描述,前面针对DAO的例子省略了部分细节,比如接口与实现之间的某种依赖性。不管是JdbcCustomerDao还是LdapCustomerDao,都省略了最基本的东西,即数据访问机制特定的代码。当引入这些特定于数据访问机制的代码时,问题就产生了,最明显的莫过于特定于数据访问机制的异常处理

当把具体的JDBC代码充实到JdbcCustomerDao时(见下方代码),看看哪里不对头。

public Customer findCustomerByPK(String customerId) {Connection con = null;try {con = getDataSource().getConnection();//...Customer cust = ...;return cust;} catch (SQLException e) {// 是抛出呢?还是在当前位置处理呢?} finally {releaseConnection(con);}
}private void releaseConnection(Connection con) {...
}

使用JDBC进行数据库访问,当其间出现问题的时候,JDBC API会抛出SQLException来表明问题的发生。而SQLException属于checked exception,所以,我们的DAO实现类要捕获这种异常并处理。

那如何处理DAO中捕获的SQLException呢,直接在DAO实现类处理掉?如果这样的话,客户端代码就无法得知在数据访问期间发生了什么变化?所以只好将SQLException抛给客户端,进而,DAO实现类的相应的签名也需要修正为抛出SQLException,如下所示:

public Customer findCustomerByPK(String customerId) throws SQLException {...
}

相应的,DAO接口中的相应的方法签名也需要修改:

public interface ICustomerDao {Customer findCustomerByPK(String customerId) throws SQLException;void updateCustomerStatus(Customer customer);// ....
}

但是,这样并没有解决问题:

  • 问题1

    我们的数据访问接口对于客户端来说是通用的,不管数据访问对象因为数据访问机制的不同而如何变更,客户端代码不应该受其牵连。但是,现在因为使用JDBC做数据访问,需要抛出特定的SQLException,那么客户端代码就需要捕捉该异常并做相应的处理。这是与数据访问对象模式的设计初衷相背离的。

  • 问题2

    在引入另一种数据访问机制时,问题更是接踵而来。当加入LdapCustomerDao实现时,LdapCustomerDao需要抛出NamingException。如果要保证findCustomerByPK方法实现了ICustomerDao中的方法,那么就得更改ICustomerDao的方法签名,如下所示:

    public Customer findCustomerByPK(String customerId) throws NamingException {...
    }
    

    糟糕不是吗?我们又把统一的访问接口给改了,相应的客户端代码又要捕捉NamingException做相应的处理。如果随着不同数据访问对象实现的增多,以及考虑数据访问对象中其他数据访问方法,这种糟糕的情况不得继续下去吗?

    也就是说,因为数据访问机制的不同,我们的数据访问接口的定义现在变成了空中楼阁,我们无法最终定义并确定这个接口,不是吗?

发现问题,解决问题

问题出现了,我们就应该尝试解决问题,因为我们实在不忍舍弃DAO模式所描述的那幅场景。那么如何来避免以上问题呢?

(1)既然直接在DAO实现类内部处理SQLException这条路走不通,而将SQLException直接抛出又不可行,那么将SQLException或者其他特定的数据访问异常进行封装后再抛出又会如何?如果要这么做,以什么类型的异常进行封装然后再抛出呢?是checked exception还是unchecked exception?

大部分的或者说所有的数据访问操作抛出的异常对于客户端来说是系统的Fault,客户端是无法有效处理的,比如数据库操作失败、无法取得相应资源等。客户端对这些情况最有效的处理方式就是不做处理。因为客户端代码对于系统的Fault通常无法处理(当然如果必要,捕捉后处理也是可以的,比如捕捉相应异常后重试等),所以,将SQLException以及其他特定于数据访问机制的异常,以unchecked exception进行封装然后抛出,是比较合适的。因为unchecked exception不需要编译器检查,ICustomerDao的数据访问方法就可恢复其本来面目而实现“大同”,如下所示:

Customer findCustomerByPK(String customerId);

各个DAO实现类内部,只要将SQLException及其他特定的数据访问异常,以unchecked exception进行封装即可,如下方代码所示。

public Customer findCustomerByPK(String customerId) {Connection con = null;try {con = getDataSource().getConnection();//...Customer cust = ...;return cust;} catch (SQLException e) {throw new RuntimeException(e);} finally {releaseConnection(con);}
}

现在,统一数据访问接口定义的问题解决了。

(2)以RuntimeException形式将特定的数据访问异常转换后抛出,虽然解决了统一数据访问接口的问题,但是,该方案依然不够周全。以SQLException为例,各个数据库提供商通过SQLException表达具体的错误信息时,所采用的方式是不同的,比如,有的数据库提供商采用SQLException的ErrorCode作为具体的错误信息标准,有的数据库提供商则通过SQLException的SqlState来返回详细的错误信息。

即使将SQLException封装后抛出给客户端对象,当客户端对象需要了解具体的错误信息时,依然需要根据数据库提供商的不同,采取不同的信息提取方式。要知道,将这种错误信息的具体处理分散到各个客户端对象中处理是何等的糟糕?我们应该向客户端对象屏蔽这种差异性!那么,如何来屏蔽这种差异性呢?

答案当然是异常的分类转译(Exception Translation)。

a)首先,不应该将对特定的数据访问异常的错误信息提取工作留给客户端对象,而是应该由DAO实现类,或者某个工具类以统一的方式进行处理。我们暂且让具体的DAO实现类来做这个工作,那么对于JdbcCustomerDao来说,捕获异常后的处理就如下方代码所示。

try {//...
} catch(SQLException e) {if (isMysqlVendor()) {// 按照Mysql数据库的规则分析错误信息(e)然后抛出throw new RuntimeException(e);}if (isOracleVendor()) {// 按照Oracle数据库的规则分析错误信息(e)然后抛出throw new RuntimeException(e);}...
}

b)信息是提取出来了,可是,只通过RuntimeException一个异常类型,还不足以区分不同的错误类型,我们需要将数据访问期间发生的错误进行分类,然后为具体的错误分类,分配一个对应的异常类型。

比如,数据库连接不上、ldap服务器连接失败,我们认为它们同属于获取资源失败;而主键冲突或者其他资源冲突,我们认为它们属于数据一致性冲突。

那么,针对这些情况,就可以以RuntimeException为基准,为获取资源失败这种情况分配一个RuntimeException的子类型,称其为ResourceFailureException,而数据一致性冲突则可以对应RuntimeException的另一个子类型DataIntegrityviolationException,其他的分类和异常类型以此类推,这样,就有了如下方代码所示的异常处理逻辑。

try {//...
} catch(SQLException e) {if (isMysqlVendor()) {if (1 == e.getErrorCode()) {throw new ResourceFailureException(e);} else if (1062 == e.getErrorCode()) {throw new DataIntegrityviolationException(e);} else {...}}if (isOracleVendor()) {int[] resourceFailureCodes = {17002,17447};int[] dataIntegrationViolationCode = {1,1400,1722,2291};...if (ArrayUtils.contains(resourceFailureCodes, e.getErrorCode())) {throw new ResourceFailureException(e);} else if (ArrayUtils.contains(dataIntegrationViolationCode, e.getErrorCode())) {throw new DataIntegrityviolationException(e);} else {...}}
}

不论采用的是什么数据库服务器,也不论采用的是什么数据访问方式,除了示例所提到的基于JDBC的数据访问方式,还有对于其他的数据访问方式,只要将它们自身的异常,通过某种方式转译为以上提到的这几种异常类型,对于客户端对象来说,只需要关注这几种类型的异常,就可以知道到底出了什么问题,甚至系统监控人员也可以直接根据日志信息判断问题之所在。

说到底,在一套语义完整的异常体系定义完成之后,不管数据访问方式如何变换,只要相应的数据访问方式能够将自身的异常,转译到这套语义完整的异常体系定义之内,对于客户端对象来说,自身的数据访问异常处理逻辑从此就是岿然不动的。

实际上,我们需要的只是一套unchecked exception类型的面向数据访问领域的异常层次体系。

不重新发明轮子

现在,我们知道了unchecked exception类型的面向数据访问领域的异常层次体系存在的必要性,马上着手设计和实现它?不,我们已经有了现成的“轮子"啦!

那就是Spring提供的数据访问异常层次体系

Spring框架中统一的异常层次体系所涉及的大部分异常类型都定义在org.springframework.dao包中,处于这个体系的所有异常类型均以org.springframework.dao.DataAccessException为“统领”,然后根据职能划分为不同的异常子类型,总体上看,整个的异常层次体系如图13-1所示。

关于这些异常的职责不做介绍,有兴趣可以看原文

本章小结

Spring提供的统一的数据访问异常层次体系,在整个Spring数据访问层中起到了提纲挈领的作用。

在本章中,我们一起分析了这个数据访问异常层次体系出现的背景以及其演化过程,最后给出了针对它的详细介绍。

在各位已经对Spring的数据访问异常层次体系了如指掌之后,接下来我们将一起来了解Spring数据访问层的另一个主要特色,即JDBC API的最佳实践。

第13章 统一的数据访问异常层次体系相关推荐

  1. 第13章 使用ADO.NET访问数据库

    Program using System; using System.Collections.Generic; using System.Linq; using System.Text; using ...

  2. 【阿里巴巴大数据实践笔记】第9章:阿里巴巴数据整合及管理体系

    1.大数据系统建设追求目标 建设高效的数据模型和体系, 对这些数据进行有序和有结构地分类组织和存储,避免重复建设和数据不一致性,保证数据的规范性. 2.阿里巴巴OneData OneData 是阿里巴 ...

  3. 转载:使用Spring进行数据访问(Data Access With Spring)

    Table of Contents 1.1. 统一的数据访问异常层次体系(Consistent Exception Hierarchy In Spring) 1.1.1. DAO模式的背景(Backg ...

  4. PP团队圣经巨著《Application Architecture Guide2.0》14章-数据访问层

    第十四章 数据访问层指导 概览 这一章主要描述设计数据访问层时要注意的主要原则.它们覆盖了设计数据访问层遇到的通常问题及错误.下面的图表展示了数据层怎样嵌入一个通用的应用架构. (cnblog我的图片 ...

  5. Spring - Java/J2EE Application Framework 应用框架 第 10 章 使用JDBC进行数据访问

    第 10 章 使用JDBC进行数据访问 10.1. 简介 Spring提供的JDBC抽象框架由core, datasource,object和support四个不同的包组成. 就和它名字的暗示一样,o ...

  6. 3-Spring Boot的数据访问

    概述 在开发中,通常会涉及到对数据库的数据进行操作,Spring Boot在简化项目开发以及实现自动化配置的基础上,对关系型数据库和非关系型数据库的访问操作都提供了非常好的整合支持. Spring B ...

  7. 控制层远程调用业务层_如何应用数据访问服务层分离系统中的业务层和持久层之间耦合关系...

    软件项目实训及课程设计指导--如何应用数据访问服务层分离业务层和持久层之间耦合关系 作者已经在本系列文章<软件项目实训及课程设计指导--如何正确地设计J2EE应用系统持久层中的各个组件结构及关系 ...

  8. 数据访问与sql语句的管理(一)

    在开发过程中数据访问是必不可少的.每个框架都会有自己数据访问机制.大家在一般的情况下会为自己的框架配备2套数据访问机制,ORM和DataHelper.当然,根据项目的需要有时候也可能只一种. 其实这2 ...

  9. 数据访问接口功能及含义

    ADC  高级数据连接器(AdvancedDataConnector):提供绑定ADO数据源到窗体的数据绑定控件上.ADC主要是一种直接访问或者通过ADO访问远程OLEDB对象的一种技术,它也支持主要 ...

最新文章

  1. Oracle Schema Objects——Tables——TableType
  2. Pinpoint 监控
  3. android中的ActionBar和ToolBar
  4. python alphago_资源 | 如何通过 Python 打造一款简易版 AlphaGo?
  5. 人口各省预测模型matlab_利用matlab编程求解人口预测模型.doc
  6. 从汇编的角度分析函数调用过程(2)
  7. 9. 广义表 - 广义表概念,存储结构,深度/长度,复制算法
  8. 周总结20170925学习C,接触GMM和ML
  9. 系统应用修改包名,导致OTA升级后用户数据丢失
  10. python进程池(子进程)函数没有执行
  11. apache实验报告 linux_linux实验报告心得
  12. 多电脑共享键鼠,传输数据,共享剪贴板
  13. 使用网口转换器更换网线后无法上网
  14. C++深度模型部署bili视频的tensorrt onnx和知乎的libtorch
  15. 基于netty的浏览器客户端打印控件实现
  16. Java打印个人基本信息
  17. Arduino-循迹小车-保姆级教程
  18. SQL Server 数据分组
  19. Security Onion Solutions 2.3.10部署指南
  20. 数据库管理员DBA必读

热门文章

  1. 什么是Interested Transaction List(ITL)--Oracle?
  2. 大数据技术对于市场营销的作用
  3. 软件工程概论总结及其对建民欧巴的评价和建议
  4. 虚拟机中Operating System not found 解决方法
  5. 10+31=100小组项目总结报告
  6. RK3588 修改USB/Sata/TF挂载点
  7. 为什么大家说 MySQL 数据库单表最大两千万?依据是啥?
  8. Element UI版本升级
  9. 【小Y学算法】⚡️每日LeetCode打卡⚡️——18.最后一个单词的长度
  10. 国外众筹kickstarter需要准备的工作