DAO设计问题

时间:2020-01-09 10:35:42  来源:igfitidea点击:

数据访问对象(DAO)模式现已成为一种广泛接受的机制,用于抽象化应用程序中持久性的细节。但是,实际上,使DAO完全隐藏基础持久层并不总是那么容易。例如,当一个事务跨度调用多个DAO时,我们将事务划分代码放其中?本文将仔细研究这个问题以及其他问题及其解决方案。

DAO的定义

简而言之,DAO的目的是从其域逻辑中隐藏应用程序的持久性机制。换句话说,要按此顺序进行通话

domain logic --> persistence mechanism

进入这个

domain logic --> DAO's --> persistence mechanism

这种抽象的优点是我们可以更改持久性机制,而不会影响域逻辑。我们需要更改的只是DAO层,如果设计得当,它比更改所有域逻辑要容易得多。实际上,我们可能可以在新的DAO层中干净地交换新数据库或者备用持久性机制。

我们还可以在这里阅读有关DAO模式的更多信息:

http://java.sun.com/blueprints/corej2eepatterns/Patterns/DataAccessObject.html http://en.wikipedia.org/wiki/Data_Access_Object

DAO连接范围

设计DAO类时遇到的第一个问题是连接作用域。换句话说,每个DAO方法都应该打开和关闭自己的连接(方法范围)吗?还是DAO实例应该具有所有方法都使用的单个内部连接(实例范围)?还是应该在同一线程用于同一会话的所有DAO之间共享连接(线程会话作用域)?连接范围在此处列出,并在下面进行说明:

  • 方法范围
  • 实例范围
  • 线程范围

方法范围

使用JDBC定位关系数据库的DAO类的简单实现可能如下所示(至少我过去曾设计过这样的DAO)。

public class PersonDao{
  protected DataSource dataSource = null;

  public PersonDao(DataSource dataSource){
    this.dataSource = dataSource;
  }

  public Person readPerson(long personId){
    Connection connection = this.dataSource.getConnection();
    try{
      Person person = ...
      return person;
    } finally {
      connection.close();
    }
  }
}

这里显示的PersonDao有点简化,但是我相信我们已经明白了。

注意readPerson()方法如何打开自己的连接,并在完成后再次关闭它。连接范围是readPerson()方法。因此,术语"方法范围"。

使用范围为DAO的方法连接遇到的第一个问题是当一个DAO方法需要调用另一个方法时。在这种情况下,将打开2个连接:一个在调用方法中,一个在被调用方法中。这是连接的浪费。两种方法也可能使用相同的连接。如果这两种方法要在同一事务中运行,这也是一个问题。然后,这两种方法必须使用相同的连接。

实例范围

为了解决方法连接范围的DAO的问题,我们可以实现实例连接范围的DAO实例。这是一个简单的示例:

public class PersonDao{
  protected Connection connection = null;

  public PersonDao(Connection connection){
    this.connection = connection;
  }

  public Person readPerson(long personId){
    Person person = ...
    return person;
  }
}

注意如何在PersonDao的构造函数中传递连接。此DAO实例上的所有方法调用都将使用相同的连接。因此,术语"实例范围"。如果一个方法在同一DAO实例中调用另一个方法,则它们仍将使用相同的连接。这两个方法调用也可以参与同一事务。

实例范围方法的问题在于DAO不再知道如何获取连接,也不知道何时关闭它。可以在DAO工厂中或者在依赖注入容器(如Butterfly Container)中获取连接,以从域逻辑代码中隐藏连接的创建。通过向DAO添加close()方法,我们也许还可以从域逻辑中隐藏连接关闭。但是在域逻辑中的某个地方,我们将不得不调用那种" close()"方法。这是它的外观:

PersonDao personDao = daoFactory.createPersonDao();
Person person = personDao.readPerson(666);
...
personDao.close();

尽管此方法确实从域逻辑中隐藏了数据库连接,但它确实泄漏了信息,该信息表明存在需要关闭的资源(连接)。另外,这种方法还存在另一个问题:

我们可能需要多个DAO,并且可能需要它们使用同一连接。例如,我们可能想将他们的操作加入到同一事务中。在这种情况下,我们如何向DAO工厂发出信号,说明以下创建的n个DAO应该使用相同的连接,而不是使用各自的连接?

线程范围

为了解决实例范围的DAO的问题,我们可以实现线程范围的DAO。还是DAO工厂。这是与范围为DAO工厂的线程连接的交互的外观:

daoFactory.beginConnectionScope();

PersonDao  personDao  = daoFactory.createPersonDao();
VehicleDao vehicleDao = daoFactory.createVehicleDao();

...

daoFactory.endConnectionScope();

调用方法daoFactory.beginConnectionScope()标记连接作用域的开始。在此调用之后,如果由同一线程创建,则所有创建的DAO将使用相同的连接。因此,术语"线程范围"。调用同一个DAO工厂的不同线程将创建自己的作用域,因此线程之间不会共享连接。

调用方法daoFactory.endConnectionScope()结束当前连接范围并关闭与该范围关联的连接。这样,DAO都不需要具有close()方法。 DAO不再对连接范围一无所知。只有调用DAO的线程可以。

DAO交易范围

DAO事务作用域看起来与连接作用域相似。这是一个简单的示例:

daoFactory.beginConnectionScope();
daoFactory.beginTransaction();

PersonDao  personDao  = daoFactory.createPersonDao();
VehicleDao vehicleDao = daoFactory.createVehicleDao();

...
daoFactory.endTransaction();
daoFactory.endConnectionScope();

方法beginTransaction()和endTransaction()标记事务的开始和结束。一个连接范围内可以有多个事务。 " beginTransaction()"方法将调用" connection.setAutoCommit(false)"作为与交易范围相关联的连接。 endTransaction()方法将尝试提交事务并再次调用setAutoCommit(false)。如果提交事务失败,事务将被回滚。

上面显示的事务作用域机制也是线程范围的。事务与当前线程关联。在同一事务范围内执行的所有数据库操作都使用相同的数据库连接,并在同一事务中执行。

线程作用域连接和事务作用域在域逻辑层的DAO层之下隐藏了持久层的大多数细节。但是,仍然有两处泄漏:连接和事务作用域的划分。

适当的异常处理

上面部分中显示的上述示例没有适当的异常处理。如果从DAO方法调用之一引发异常,则该示例将无法正确结束连接或者事务作用域。

这是异常处理的外观示意图。但是请记住,这只是一个草图。我们将必须对其进行修改以适合我们自己的目的,错误处理和报告等。

try{
  daoFactory.beginConnectionScope();

  try{
    daoFactory.beginTransaction();

    PersonDao  personDao  = daoFactory.createPersonDao();
    VehicleDao vehicleDao = daoFactory.createVehicleDao();

    daoFactory.endTransaction();

  } catch(Exception e){
    daoFactory.abortTransaction(e);
  }
} finally {
  daoFactory.endConnectionScope();
}

注意事务范围的catch块中的新方法调用:daoFactory.abortTransaction(e)。如果任何dao方法或者endTransaction()抛出异常,则此方法调用将回滚事务。