假设我们必须处理对象的存储, 加载, 和查询. 性能和引用完整性的约束, 给接口的实现带来了以下问题:
-
加载根对象时如何避免加载大半个数据库
-
存储时如何更新整个对象图
-
存储时如何高效的更新整个对象图
-
何时同步对象的内存状态和持久存储状态
-
如何确保在出错时保持对象内存状态和持久存储状态之间的一致性
-
如何保证引用的唯一性以避免可能的更新冲突
对性能的精益求精, 又促使人们解决更多的细节问题:
-
N+1查询问题
-
分离查询模型和存储模型
-
尽量减少查询语句
这些问题的解决方案又会带来新的问题.
1. 加载根对象时如何避免加载大半个数据库
更多的时候这是一个建模问题, 为什么我只需要显示一点信息, 更新一点信息, 却拉家带口把八杆子打不着的亲戚都带上 : 细粒度对象设计, 直接访问需要的信息, 减少所谓根对象的存在
一个workaround是延迟加载, 当你无法修复你错误的建模时, 当真正去访问子对象的时候再发出查询语句去加载. 这个方案会带来如下问题:
-
查询语句较多. 无解, 延迟意味着至少两条SQL语句, 只能尽量减少
-
延迟加载的时机, 是自动透明的延迟加载, 还是用户确定何时加载
Hibernate可通过配置文件指定是否lazy load, 一旦指定, 后面的load就是透明的在访问子对象时发生. 也可在发出每次查询时显式指定
Entity Framework则要求用户在每一次查询时显式指定包含哪个子对象, 对没有指定包含的子对象, 只能在访问前显示使用load(). 理由是决定加载不加载,何时加载都是程序员的责任
-
然而更大的问题是如何管理数据库连接, 要确保延迟加载的时候数据库连接是开着的
可以使用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 的映射类型的名字