Apache Cloudstack Development 101 -- Data Access Layer

时间:2024-06-25 12:07:32

刚接触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的例子。

  • 如果你正在编写cloud-engine核心代码或者子系统插件代码(如Nicira),需要存取数据库,那就用位于cloud-engine-schema工程的VO和DAO。
  • 如果你在编写API访问的代码,而且不涉及orchestration和提供硬件资源,那就用位于cloud-server工程的VO和DAO,特别是以JoinVO结尾的。如果你能将这些JoinVO转换为用通用的ORM实现,以便JPQL能用于生成更复杂的查询,那就更好。我期待谁能在这一块做出贡献。

DAO

CloudStack选择用DAO实现它的数据访问层,是因为CloudStack的核心是一系列的作业处理引擎,需要直接读取schema。CloudStack也希望实现以下数据库使用模式:

  • 当一个类实体第一次从CloudStack删除时,除非admin在数据库运行expunge 进程,否则它不会从数据库删除。这样在出现错误时更容易恢复。它还可以用来遵守数据保留策略。然而,当一个实体被删除后,该实体对业务逻辑是不可见的。当VO中的字段带有@Column(name=GenericDao.REMOVED_COLUMN)注解时,就会按这种模式执行。当出现这种情况时,DAO层自动将删除操作改为通过时间戳更新被删除列,但是所有的查询操作不会返回被删除列的数据集。
  • 当一个类实体第一次在CloudStack创建时,为了审计而创建时应当被记录。对于更健壮的审计机制,这是在业务逻辑中实现的失败安全策略。通过这些信息,作为最后一种手段,管理员能查看创建时的CloudStack日志推断出可能发生了什么。当VO中的字段带有@Column(name=GenericDao.CREATE_COLUMN)注解时,就会执行这种模式。当出现这种情况时,DAO层自动往列里插入时间戳。我们不需要依靠业务逻辑来记得要填写这些信息。
  • 执行对象映射而不是关系映射。作为大多数ORM实现的一部分的关系映射的问题是,开发者很容易通过@OneToMany注解和饥渴抓取将全部数据库读入内存。因此DAO不支持@OneToMany和@FetchType注解。
  • 独立SQL从数据库连接生成。DAO不管理数据库连接,也不管理将要连接的数据库,而是由业务逻辑管理。这易于支持数据库复制和分区策略。
  • 不支持嵌套事务。
  • 自动释放Connection、PreparedStatements和Results。
  • 自动回滚未提交事务。
  • 自动释放数据库锁。
  • 防止SQL注入攻击。

CloudStack DAO层由四部分实现:

  • GenericDao和GenericDaoBase提供一组方法供使用。
  • @DB annotation 和TransactionContextBuilder实现Connection, PreparedStatement和Results的自动释放。
  • Transaction 管理数据库连接和数据库事务。
  • SearchBuilder 创建基于VO的查询。

 编写新的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查找行、执行查询、删除行等。如果这就满足你访问新的表,那你就真的不用再写其它代码。请注意类中的注释,总结如下:

  • 如果VO要被其它模块使用,则需声明一个接口,通过这个接口跟其它模块通信。
  • 接口不带任何setter方法,它只供其他模块使用,而不被修改。
  • getter和setter方法需遵从Java约定,将get/set置于字段名称前面。在你更新VO对象时,DAO代码由此找到被修改的字段。
  • 如果不打算给其它模块使用,没必要为DAO实现声明接口。我将在下面讨论一个这个的特殊用例。
  • 没必要为移除字段和创建字段提供setter方法,它会由DAO代码自动填充。
  • 对于移除字段和创建字段,必须使用在GenericDao中声明的变量。如果使用@Column(name="removed"),DAO会认为该字段你会更新。
  • 受保护的默认构造方法用来实例化类。

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用在商业逻辑层和表示层。