刚接触CloudStack,也是第一次翻译英文文档,限于水平有限,不当之处欢迎拍砖!
原文地址:https://cwiki.apache.org/confluence/display/CloudStack/Data+Access+Layer
读前必知
关于CloudStack的数据访问层要知道三件事:
第一:CloudStack的数据访问层以位于cloud-engine-schema中的VO来表示,底层的数据库只是schema的一个实现。这意味着一个VO可能是一张表的一部分,也可能是联合了多张表。位于VO上层的软件不需要关注这点,对其而言,schema就是VO。
第二:CloudStack的数据访问层使用JPA。因此,任何类型的ORM,比如Hibernate使用会有一些麻烦。事实上,对于任何类型的API访问schema,我们推荐使用像Hibernate这样的ORM框架。然而,在跃跃欲试之前,还请先阅读完本章节的所有部分。
第三:目前cloud-engine-schema中的VO集不应用于API访问。这个VO集是CloudStack关系的标准视图。它被用于CloudStack的作业处理引擎。通过API访问时,CloudStack schema要更具描述性。比如说,VM VO包含账号数据库id,但是VM Resource/Response应包含账号名称和uuid而不包含数据库id。因此VO不等于Resource/Response。在cloud-server创建数据库视图以定义CloudStack Resource/Response,但不是完整的,不包含所有的CloudStack Resource/Response。
在哪里寻找例子?
鉴于上述,我们很自然的想到在哪里能找到怎样去增加数据库读取CloudStack的例子。
DAO
CloudStack选择用DAO实现它的数据访问层,是因为CloudStack的核心是一系列的作业处理引擎,需要直接读取schema。CloudStack也希望实现以下数据库使用模式:
CloudStack DAO层由四部分实现:
编写新的VO和DAO
//如果需要,使用VO接口,注意在接口中没有set方法 //如果其它代码要用到VO,应该只提供接口,而且它只是只读的对象 public interface Test { String getText(); String getCreated(); } // VO....@Entity @Table(name="test") public class TestVO implements Test { //假定已设定接口 @Column(name="id") private long id; @Column(name="text") String text; @Column(name=GenericDao.CREATED_COLUMN) @Temporal(value=TemporalType.TIMESTAMP) private Date created; // 注意移除的字段没有个getter和setter方法 // 由DAO设定和使用 // 如果需要,可以增加getter方法,但是setter方法是没意义的 @Column(name=GenericDao.REMOVED_COLUMN) @Temporal(value=TemporalType.TIMESTAMP) private Date removed; public TestVO(String text) { // 常规构造方法 this.text = text; } protected TestVO() { // 受保护的构造方法用于反射 } // getter和setter方法需遵守Java约定,即将get/set放在字段前面 public String getText() { return text; } public void setText(String text) { this.text = text; } // 假如你需要用到不使用标准前缀(getXXX或者isXXX)的getter方法, // 需在方法上标注@Column(name="<columnName>")注解 // 来指明该方法关联的是哪一列 @Column(name="text") public String retrieveText() { return text; } // 注意不需要创建setter方法,它有DAO代码自动创建 public Date getCreated() { return created; } } // DAO... (接口不需要时可略过) public interface TestDao extends GenericDao<TestVO, Long> { } // Impl... @Local(value={HostDao.class}) // 如果已指定接口 public class TestDaoImpl extends GenericDaoBase<TestVO, Long> implements GenericDao<TestVO, Long> { protected TestDaoImpl() { } }
创建类似上面的文件,就可以使用继承自GenericDao 的方法,比如通过id查找行、执行查询、删除行等。如果这就满足你访问新的表,那你就真的不用再写其它代码。请注意类中的注释,总结如下:
DAO实现支持的注解
(此表格请参见原文)
用法
使用DAO
public class Foo { @Inject TestDao _testDao; // 注入DAO,不要new public String getText(long id) { TestVO vo = _testDao.findById(id); // findById由GenericDaoBase定义 return vo.getText(); } }
更新VO
public class Foo { @Inject TestDao _testDao; public void updateText(long id, String text) { TestVO vo = _testDao.findById(id); vo.setText(text); // 调用set方法让DAO明白修改的哪个字段
_testDao.update(vo); } }
查找
CloudStack提供两种类型的查询结构:SearchBuilder 和SearchCriteria。SearchBuilder 在编译和加载时构造查询语句。它相当于编写一个类似于“SELECT * FROM test WHERE text=?”的查询语句,“?”在运行时填充。这样会在编译和加载时检查全部构造,易于更早地发现错误。在运行时,SearchBuilder 生成SearchCriteria ,SearchCriteria 被用来填充“?”。SearchBuilder 在SQL查询时优先使用,因为通过上下文敏感的编辑器重构时,会自动更改SearchBuilder ,但不会更改SQL。
public class Foo { @Inject TestDao _testDao; protected SearchBuilder<TestVO> TestSearch; public Foo() { // SELECT * FROM test WHERE text=? AND created<? // SearchBuilder的entity()实际上返回VO本身 // 然后你就可以用使用getter方法 TestSearch = _testDao.createSearchBuilder(); TestVO entity = TestSearch.entity(); TestSearch.and("bar", entity.getTest(), SearchCriteria.Op.EQ) .and("time", entity.getCreated(), searchCriteria.Op.LT).done(); } public List<? extends Test> findTest(String text, Date date) { SearchCriteria<TestVO> sc = TestSearch.create(); sc.setParameters("bar", text); // 注意“bar”是在SearchBuilder设置的参数名称 sc.setParameters("time", date); // 注意"time"是在SearchBuilder设置的参数名称 return _testDao.listAll(sc); } }
在上面的用法中,SearchBuilder 在构造时初始化,SearchCriteria 在运行时使用。SearchCriteria 能在DAO运行时重新获取,编写更专业的查询。
下面的代码片段说明用SearchBuilder构造一个连接查询(join)。
// 生成 // SELECT * FROM HOST INNER JOIN host_tag ON host_tag.host_id=host.id WHERE host.type=? AND host.pod_id=? // AND host.data_center_id=? AND host.cluster_id=? AND host.status=? AND resource_state=? AND host_tag.tag=? SearchBuilder<HostTagVO> hostTagSearch = _hostTagsDao.createSearchBuilder(); HostTagVO tagEntity = hostTagSearch.entity(); hostTagSearch.and("tag", tagEntity.getTag(), SearchCriteria.Op.EQ); SearchBuilder<HostVO> hostSearch = createSearchBuilder(); HostVO entity = hostSearch.entity(); hostSearch.and("type", entity.getType(), SearchCriteria.Op.EQ); hostSearch.and("pod", entity.getPodId(), SearchCriteria.Op.EQ); hostSearch.and("dc", entity.getDataCenterId(), SearchCriteria.Op.EQ); hostSearch.and("cluster", entity.getClusterId(), SearchCriteria.Op.EQ); hostSearch.and("status", entity.getStatus(), SearchCriteria.Op.EQ); hostSearch.and("resourceState", entity.getResourceState(), SearchCriteria.Op.EQ); hostSearch.join("hostTagSearch", hostTagSearch, entity.getId(), tagEntity.getHostId(), JoinBuilder.JoinType.INNER); hostSearch.done();
下面的代码说明用GenericSearchBuilder构造用到特定的字段或特定的函数的查询
// 相当于SELECT COUNT(*) FROM host WHERE data_center_id=? AND type=? AND status=? // 在这个例子中,用GenericSearchBuilder 可以指定返回值类型(本例中是long)。 GenericSearchBuilder<HostVO, Long> CountRoutingByDc; CountRoutingByDc = createSearchBuilder(Long.class); CountRoutingByDc.select(null, Func.COUNT, null); CountRoutingByDc.and("dc", CountRoutingByDc.entity().getDataCenterId(), SearchCriteria.Op.EQ); CountRoutingByDc.and("type", CountRoutingByDc.entity().getType(), SearchCriteria.Op.EQ); CountRoutingByDc.and("status", CountRoutingByDc.entity().getStatus(), SearchCriteria.Op.EQ); CountRoutingByDc.done();
使用事务
CloudStack 数据访问层也会定义如何使用事务和数据库连接。在上面的例子中,代码没有用到数据库连接、PreparedStatement或者Result。其实它已经实现,这样你只需关注构造实现你业务逻辑的SQL。然而,如果你需要一个数据库事务或者真的需要自己编写处理SQL,请使用CloudStack事务。CloudStack事务不只是数据库事务,它实际上是数据库应用上下文。下面的例子说明了如何用。
public class Foo { @Inject TestDao _testDao; @DB // 重要 protected void updateText(long id, String text) { // 方法不能是私有的 Transaction txn = Transaction.currentTxn(); // 获得事务上下文,但没有开始DB连接 // ... 在此处添加代码 txn.start(); // DB事务实际从此处开始 TestVO vo = _testDao.lockRow(id, true); if (vo.getText() == null) { vo.setText(text); } _testDao.update(vo); txn.commit(); // DB 事务提交 } @DB protected void updateTextComplicated(long id, String text) { // 控制回滚请看这个例子. Transaction txn = Transaction.currentTxn(); // ... 在此处添加代码 boolean committed = true; TestVO vo = null; txn.start(); try { vo = _testDao.lockRow(id, true); if (vo.getText() == null) { vo.setText(text); } _testDao.update(vo); txn.commit(); } catch(Exception e) { // 记录异常日志 committed = false; txn.rollback(); } if (!committed) { // 操作没有提交 } } }
上面的代码创建了检索和更新TestVO 的一个原子操作。为此你需要在方法上标注@DB注解。到此你熟悉了编写DB代码,你会发现在错误发生时没有回滚。这在使用@DB注解时要特别注意。
一旦代码离开了这个方法的范围,明白@DB注解的拦截器就会启动检查并回滚事务。该拦截器还会释放PreparedStatement和Result对象以及数据库连接对象。
处理嵌套事务
当你的代码开启一个数据库事务,并调用一个也开启了数据库事务且还未提交的方法时会发生什么。CloudStack 不想支持嵌套事务的概念。在这种情况下,两个事务实际上会合并到一个大的事务,并且直到最外面的事务提交时才会提交结果。如果发生回滚操作,所有的都会回滚。
处理锁
CloudStack数据访问层支持两种类型的锁。第一种是数据库行锁。lockRow()方法调用如上例所示。你可以在查询返回行时加锁。请看GenericDao支持此类型的方法。 数据库行锁只应用于当你需要对数据库操作加锁的情况。例如,在上面的例子中,原子操作或在字段上设置时,应该使用数据库锁。
第二种是表锁。这些操作在GenericDao接口中以"InLockTable"结尾的方法列出。这些锁执行时需要外部资源,需花费较长时间。在这种情况下,数据库行锁是不够的,因为它会超时,而且它会通过锁行和在外部资源操作完成前不允许其它并发操作来降低可扩展性。这时你应该用到表锁。请注意@DB注解不会自动释放锁表里的锁。因为设计就是这样的。当执行外部资源操作时,使用表锁,线程释放锁和线程得到锁很可能是不一样的。因此,当方法超出范围时,@DB不会自动释放锁表锁。
附:VO与PO的区别:
一、PO:persistant object 持久对象,可以看成是与数据库中的表相映射的java对象。使用Hibernate来生成PO是不错的选择。
二、VO:value object值对象。通常用于业务层之间的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不对应,这根据业务的需要。
三、PO只能用在数据层,VO用在商业逻辑层和表示层。