原文地址:Part 5: Fixing the Broken Stuff
到目前为止,我们一直都在关注 NHibernate 和持久化。在本篇中,我们会纠正之前模式和映射的问题来通过我们的测试。本篇结束之后,我们会减少对 NHibernate 的关注。下一部分开始集中于整合 Ninject ,我们的控制反转/依赖注入的框架,并加入到 ASP.NET MVC 中。
讲点其他有意思的事,根据 Fabio Maulo 介绍,NHibernate 的LOGO可能是一只睡着的土拨鼠。
明确你要纠正什么当你修正BUG的时候,你应该只修改BUG。很显然,我们编写测试,我们可以找出哪里有问题,不明显的是要知道哪些“错误”其实不是错误。
自白:有时我会先编码,然后测试。有时我会先穿裤子,再穿衬衫。其实只要你离开家之前衣服都穿好了,顺序不是那么重要。同样,编码和测试的顺序也不是那么重要。
下面是 part 4中 NUnit 的测试结果:2个通过,3个失败,5个抛出异常。对我来说,10个测试中有2个通过了已经非常好了。下面让我们一一解决剩余的问题。
最低限度的 NHibernate 调试NHibernate 使用 log4net 作为日志框架,它可以快速准确的显示出 NUnit 和其他测试工具的日志。
- 将 log4net.dll 添加到测试项目的引用中
- 添加 app.config 文件
- 添加一个新类 BaseFixture
- 设置你所有的 test fixture 继承 BaseFixture
下面是 app.config 文件中 log4net 的配置:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/> </configSections> <log4net> <appender name="Debugger" type="log4net.Appender.ConsoleAppender"> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline"/> </layout> </appender> <logger name="NHibernate.SQL"> <level value="ALL"/> <appender-ref ref="Debugger"/> </logger> </log4net> </configuration>
public abstract class BaseFixture { protected static readonly log4net.ILog Log = GetLogger(); private static log4net.ILog GetLogger() { log4net.Config.XmlConfigurator.Configure(); return log4net.LogManager.GetLogger(typeof(BaseFixture)); } }
如果你需要更强大的东西,可以参阅 Ayende 的 NHProf.
问题#1NStackExample.Data.Tests.CourseMappingTests.CanCascadeOrphanDeleteFromCourseToSections: NHibernate.TransientObjectException : object references an unsaved transient instance -
save the transient instance before flushing. Type: NStackExample.Section, Entity: NStackExample.Section
Course Course = new Course { Subject = "SUBJ", CourseNumber = "1234", Title = "Title", Description = "Description", Hours = 3 }; Section Section = new Section { FacultyName = "FacultyName", RoomNumber = "R1", SectionNumber = "1", Term = Term }; Course.AddSection(Section); using (ITransaction Tran = Session.BeginTransaction()) { ID = (Guid) Session.Save(Course); Session.Save(Section); Tran.Commit(); //<==== 这里抛出异常 } Session.Clear();
当提交事务的时候, session 的变更会提交到数据库。这只是将变更的数据写入数据库。这个异常是告诉我们正在尝试保存一个对象,但是它引用的其他对象还没有保存。我们可以推断,级联已经关闭这种关系。当运行到这一特定代码行,我们看到在此事务正在保存(INSERT)一个新创建的 course ,和 course 引用的新创建的 section 。 如果这是 TestCascadeSaveFromParentToChild 的测试,我们也许会调整映射。本例中,我们测试的是删除孤儿的功能,并不是测试级联保存或更新,所以我们将明确的指出在当前事务中保存 section 。
适当更改后,重新运行测试。我们看到,同样的测试还是失败了。
//Test removing Course.RemoveSection(Section); using (ITransaction Tran = Session.BeginTransaction()) { Session.SaveOrUpdate(Course); Tran.Commit(); } Session.Clear();
这样做的话,我们违反了唯一约束,这是因为我们运行了 Session.Save(Course) 两次,Session.Save 只能是保存新的对象。若想要保存 course,我们可以用 Session.SaveOrUpdate 或 Session.Update。因为这些都不返回标识,而我们需要得到最初保存的对象,我们做一些更改,编译后再运行测试。
接下来,我们得到了这个异常:
NStackExample.Data.Tests.CourseMappingTests.CanCascadeOrphanDeleteFromCourseToSections: NHibernate.Exceptions.GenericADOException :
could not delete collection: [NStackExample.Course.Sections#912b489a-4d12-4bc9-9d68-9c6b0147b799]
[SQL: UPDATE "Section" SET Course_id = null WHERE Course_id = @p0] ----> System.Data.SQLite.SQLiteException : Abort due to constraint violation Section.Course_id may not be NULL
这个异常是说我们正在从 section 中分离 course ,NHibernate 尝试将 Section 的 Course_id 设置成 NULL。这违反了非空约束。更重要的是这违反了我们的业务逻辑,当 section 成为孤儿的时候应该已经被删除。更新我们的映射来修正这个错误。在 course 的映射中,我们将在对 section 一对多的关系中添加 Cascade.AllDeleteOrphan()。
HasMany(x => x.Sections)
.AsSet()
.ForeignKeyConstraintName("CourseSections")
.Cascade.AllDeleteOrphan()
修改完成后再次测试,会得到下面这个异常:
NStackExample.Data.Tests.CourseMappingTests.CanCascadeOrphanDeleteFromCourseToSections: NHibernate.PropertyValueException : not-null property references a null or transient valueNStackExample.Section.Course
这个错误很奇怪。 基本上,即使我们删除 section ,NHibernate 都会投诉我们已经设置了 Section.Course = null / nothing。现在,为了遵循 NHibernate 的规则,我们将取消对 Section.Course 的非空限制。如果你打开log4net中记录的 NHibernate.SQL 日志的话,你可以看到这个操作并不会违反数据库的 NOT NULL 约束,孤儿的数据已经删除了,我们只是在 NHibernate 的内部属性检查时失败了。
第二个问题基本上是关系数据库概念和对象关系的分离。所有 one-to-many 的数据库关系都是双向的,many-to-one 的是隐式的,在对象关系图中我们可以有从父亲到孩子的引用,但没有提及从孩子返回父亲的引用,或者反之。对象关系是单向的。即使在大多数情况下这会被标示成一个 BUG,我们仍然要告诉 NHibernate 我们的单向关系是“真的”单向,并且我们希望保持到数据库。默认的是使用 one-to-many。这意味着这个保存关系是基于 course’s sections 集合内的每一个成员。我们宁愿有一个基于 many-to-one 的关系: Section 的 Course 属性。要做到这一点,我们在映射中指定 Course.Sections 为 Inverse() 。这是告诉 NHibernate 它是双向关系中的“另一边”。
Bug 解决了,向前进!等等,重新编译并运行测试,你可能不知不觉之中已经修正了其他问题。
问题#2NStackExample.Data.Tests.CourseMappingTests.CanCascadeSaveFromCourseToSections: Expected: <nstackexample.section> But was: <nstackexample.section>
这又是一个误导性的问题。我们测试的检查两个 section 相等。
Q: 我们如何定义 section 相等?
A: 我们并没有,Object.Equals 只是检查这两个是否是同一个实例。由于其中一个是从数据库中读取的,所以它们不是。我们会定义我们自己的相等检查。
Q: 我们要如何定义相等?
A: 如果两个实例代表相同的 section,它们就是相等的。等等,为什么我们只讨论 sections? 让我们扩展覆盖到所有的实体。
Q: 我们应该将这个规则放到哪里?
A: 我们应该重写 base Entity 类里的 Equals,因为所有的实体都使用它。
Q: 我们如何知道两个实例是代表相同的实体?
A: 判断 ID 字段是相同的。
Q: 如果对象还没有被持久化和还没有 ID 的时候呢?
A: 我们假设它们并不相同。如果具体的类需要更精确的比较,它可以重写 Equals 方法。
代码如下:
public override bool Equals(object obj) { Entity other = obj as Entity; if (other == null) return false; return ID.Equals(other.ID) && !ID.Equals(Guid.Empty); }让我们重新编译并测试一下,看看!10个测试中已经有6个通过了。 问题#3
NStackExample.Data.Tests.SectionMappingTests.CanCascadeSaveFromSectionToStudentSections: NHibernate.PropertyValueException : not-null property references a null or transient valueNStackExample.Student.MiddleName
这种特定的错误可以通过两种方法找到。我们在 Student 映射中定义了 MiddleName 为非空的。我们 Section 的级联测试失败了,因为它不会去给 MiddleName 设定一个值。我们可以改变下测试,将 MiddleName 设为空字符串,或者改变下映射为允许空。我选择前者。改变映射为允许 NULL 会导致 NullReferenceExceptions 异常。我们设置 MiddleName = string.Empty。编译并测试之后,得到这个错误。
NStackExample.Data.Tests.SectionMappingTests.CanCascadeSaveFromSectionToStudentSections: NHibernate.TransientObjectException : object references an unsaved transient instance - save the transient instance before flushing. Type: NStackExample.StudentSection, Entity: NStackExample.StudentSection
这个错误告诉我们级联失败。为什么?因为我们没有为 StudentSection 的 one-to-many 关系指定一个级联。因为我们知道 Sections 和 Students 应该级联到 StudentSection,为它们两个添加 Cascade.All 和 Inverse()。
编译并重新测试。成功。
问题#4NStackExample.Data.Tests.StudentMappingTests.CanCascadeSaveFromStudentToStudentSection: NHibernate.TransientObjectException : object references an unsaved transient instance - save the transient instance before flushing. Type: NStackExample.Student, Entity: NStackExample.Student
这是我们测试中的一个 BUG。如果你看到我们测试的是什么和我们实际保存的是什么,你应该知道我们应该保存 Student,而不是 Section。修复这个错误再测试。现在我们又碰到了和问题#3中的 MiddleName 同样的 BUG,修复它,重新测试。得到了 NullReferenceException 异常。为什么?
如果你看了 Student 映射的测试,你可以发现我们没有检查正确的结果。这非常可能是一个电话会议中或其他类似的分散注意力的情况中粗心的剪切-粘贴的工作。改为预期的正确的结果:
//Check the results using (ITransaction Tran = Session.BeginTransaction()) { Student = Session.Get<Student>(ID); Assert.AreEqual(1, Student.StudentSections.Count); Assert.AreEqual(StudentSection, Student.StudentSections.First()); Tran.Commit(); }
正常工作了!
问题#5NStackExample.Data.Tests.TermMappingTests.CanCascadeSaveFromTermToSections: NHibernate.TransientObjectException : object references an unsaved transient instance - save the transient instance before flushing. Type: NStackExample.Section, Entity: NStackExample.Section
这个和问题#3类似。我们 term 的级联没有级联到保存 section。为 Term.Sections 添加 Cascade,All() 和 Inverse()。
问题#6NStackExample.Data.Tests.TermMappingTests.CanSaveAndLoadTerm: Expected: "Fall 2009" But was: null
在此测试中,我们是需要从 Term 对象的 Name 属性中得到一个值,但我们得到了 null / nothing。 当你看到这里的时候,你应该首先检查你的映射。在这里,你可以很快的发现我们没有映射 Name 属性,将它添加到映射中。接下来,你会发现一个测试的错误。我们比较的日期是错误的,EndDate 应该是和 December 1st, 2009 比较。
示例代码下载:/Files/LeoXing/NStack/NStackExample.Part5.CSharp.zip