posts - 56, comments - 77, trackbacks - 0, articles - 1
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

ORM问题域

Posted on 2009-12-28 23:49 切尔斯基 阅读(1971) 评论(0)  编辑  收藏

假设我们必须处理对象的存储, 加载, 和查询. 性能和引用完整性的约束, 给接口的实现带来了以下问题:

  1. 加载根对象时如何避免加载大半个数据库

  2. 存储时如何更新整个对象图

  3. 存储时如何高效的更新整个对象图

  4. 何时同步对象的内存状态和持久存储状态

  5. 如何确保在出错时保持对象内存状态和持久存储状态之间的一致性

  6. 如何保证引用的唯一性以避免可能的更新冲突

对性能的精益求精, 又促使人们解决更多的细节问题:

  1. N+1查询问题

  2. 分离查询模型和存储模型

  3. 尽量减少查询语句

这些问题的解决方案又会带来新的问题.

 

1. 加载根对象时如何避免加载大半个数据库

更多的时候这是一个建模问题, 为什么我只需要显示一点信息, 更新一点信息, 却拉家带口把八杆子打不着的亲戚都带上 : 细粒度对象设计, 直接访问需要的信息, 减少所谓根对象的存在

一个workaround是延迟加载, 当你无法修复你错误的建模时, 当真正去访问子对象的时候再发出查询语句去加载. 这个方案会带来如下问题:

  1. 查询语句较多. 无解, 延迟意味着至少两条SQL语句, 只能尽量减少

  2. 延迟加载的时机, 是自动透明的延迟加载, 还是用户确定何时加载

Hibernate可通过配置文件指定是否lazy load, 一旦指定, 后面的load就是透明的在访问子对象时发生. 也可在发出每次查询时显式指定

Entity Framework则要求用户在每一次查询时显式指定包含哪个子对象, 对没有指定包含的子对象, 只能在访问前显示使用load(). 理由是决定加载不加载,何时加载都是程序员的责任

  1. 然而更大的问题是如何管理数据库连接, 要确保延迟加载的时候数据库连接是开着的

可以使用Interceptor等技术维持 Session per request, Open Session in View pattern(处理好异常等, 确保session会关闭).

能在一个 Session 中使用两个事务吗?

是的,这事实上是这种模式(Open Session In View)的一个更好的实现。在一个请求事件中,一个数据库事务用于数据的读写。第二个数据库事务仅用于在渲染视图期间读数据。在这点上没有对对象的修改。因此,数据库锁早在第一个事务时就被释放了,这使得应用有更好的可伸缩性,第二个事务可以被优化。要使用两阶段的事务,你需要比 Servlet Filter 更强大的拦截器 - AOP 是个很好的选择。JBoss Seam 使用了这种模式。

为什么 Hibernate 不在需要时就加载 Object?

每个月很多人都会有这种想法,为什么 Hibernate 不能在有需要的就开启一个新的数据库连接(更有效率的是开启一个 Session),然后加载集合或是初始化代理,而是选择抛出一个 LazyInitializationException。当然,这种想法,第一眼看上去可能是明智之举。但这种做法有很多的缺点,只有当你考虑特别的事务访问时才会发现。

如果 Hibernate 可以进行任意的数据库连接和事务,这种操作是开发人员不可知,并且也是在任何事务边界之外的,那还要事务边界做什么。当 Hibernate 开启了新的数据库连接去加载集合,但同时集合的拥有者却被删除了,这是将会发生什么?(注意,这种情况是不会发生在上面提到的两阶段的事务模式中的 - 单个 Session 可对实体可重复读。)当所有的对象都可以通过关联导航获取时为什么还要有 Service 层?这种方式将消耗多少内存?哪些对象要首先被清除掉?所有这些问题都是无解的,因为 Hibernate 是一个在线的事务处理服务(并包含一些批处理操作),并不是一个“在未定义的工作单元中从数据持久仓库取得对象”的服务。此外,对于 n+1 查询问题,我们是否需要 n+1 的事务和连接的问题?

这个问题的解决方案当然是正确的工作单元划分和设计,支撑其的拦截技术就像这里所展现的一样,并且/或者正确的抓取技术,使得特定工作单元所需的全部信息能够以最小的影响、最好的性能和伸缩性被获得。

 

2. 存储时如何更新整个对象图

框架支持级联更新. 是否应该级联更新, 哪些操作可以级联, 哪些不可以, 对象之间的哪些类型的关联可以级联, 哪些不可以, 则是程序员的责任

  • 通常被聚合的对象, 其生命周期应由父对象负责, 新增/更新/删除都应级联

  • 自身有存在意义的实体, 可以级联更新, 但不应删除和新增

 

3. 存储时如何高效的更新整个对象图

常用工作单元模式, Unit of Work.

 

4. 何时同步对象的内存状态和持久存储状态

任何改动都立即提交到数据库会带来额外开销. 一个时机是事务提交时.

Hibernate: 每间隔一段时间,Session会执行一些必需的SQL语句来把内存中的对象的状态同步到JDBC连接中。这个过程被称为刷出(flush),默认会在下面的时间点执行:

  • 在某些查询执行之前

  • 在调用org.hibernate.Transaction.commit()的时候

  • 在调用Session.flush()的时候

 

5. 如何确保在出错时保持对象内存状态和持久存储状态之间的一致性

数据库事务回滚, 清空内存缓存, 重新加载

 

6. 如何避免或处理可能的更新冲突

保证引用的唯一性: 使用单一的加载入口和缓存, Identity Map.

乐观离线锁会引入更新冲突问题, 一般使用Versioning来解决, 类似版本控制系统的更新问题; 但业务对象很少能自动Merge, Merge的语义也不好定义, 所以一般检测到冲突之后只好重做了, 或者取决于业务逻辑, Last Win也是一种策略.

 

7. N+1查询问题

  • Eager Load + JOIN

  • 截然不同的一种避免N+1次查询的方法是,使用二级缓存。

N + 1 是关联引入的问题, 网上的解释和例子倾向于拿one-2-many说事, 但实际上one-2-one依然面临使用多于一条SQL语句加载的问题

 

8. 分离查询模型和存储模型

适合业务关系的对象模型未必对查询是高效的. 需要单独针对查询建模, 可以用单独的索引表来实现. 在更新业务对象的存储时同时更新索引表

 

9. 尽量减少查询语句

比如join over multiple select, 比如批量抓取

 

10. 值类型

不需要有ID, 通常被聚合. 有对应的Class, 但一般没有对应的Table, 仅是Table中的几个字段

挑战在于将对象语言类型系统(和开发者定义的实体和值类型)映射到 SQL/数据库类型系统。 Hibernate: 提供了连接两个系统之间的桥梁:对于实体类型,我们使用class, subclass 等等。对于值类型,我们使用 property, component 及其他,通常跟随着type属性。这个属性的值是Hibernate 的映射类型的名字


只有注册用户登录后才能发表评论。


网站导航: