DAO设计问题
数据访问对象(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()
抛出异常,则此方法调用将回滚事务。