diff --git a/doc/reference/modules/basic_mapping.xml b/doc/reference/modules/basic_mapping.xml index 5ea3bf9f859..8f7e9fb04c6 100644 --- a/doc/reference/modules/basic_mapping.xml +++ b/doc/reference/modules/basic_mapping.xml @@ -1628,7 +1628,7 @@ name="PropertyName" column="column_name" class="ClassName" - cascade="all|none|save-update|delete" + cascade="all|none|save-update|delete|delete-orphan|all-delete-orphan" fetch="join|select" update="true|false" insert="true|false" @@ -1788,7 +1788,7 @@ - Mapping an association (many-to-one, or collection) with cascade="all" + Mapping an association (many-to-one, one-to-one or collection) with cascade="all" marks the association as a parent/child style relationship where save/update/deletion of the parent results in save/update/deletion of the child(ren). Futhermore, a mere reference to a child from a persistent parent will result in save / update of the child. The metaphor is incomplete, however. A child which becomes unreferenced by its - parent is not automatically deleted, except in the case of a - <one-to-many> association mapped with - cascade="all-delete-orphan". The precise semantics of cascading operations + parent is not automatically deleted, except in the cases of + <one-to-many> and <one-to-one> associations + that have been mapped with cascade="all-delete-orphan" or + cascade="delete-orphan". The precise semantics of cascading operations are as follows: @@ -1081,8 +1082,8 @@ finally If a transient child is dereferenced by a persistent parent, nothing special happens (the application should explicitly delete the child if - necessary) unless cascade="all-delete-orphan", in which case the - "orphaned" child is deleted. + necessary) unless cascade="all-delete-orphan" or + cascade="delete-orphan", in which case the "orphaned" child is deleted. diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..3827fc9a86f --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,84 @@ +using NUnit.Framework; +using System.Collections; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Bidirectional +{ + [TestFixture] + public class DeleteOneToOneOrphansTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new string[] { "Cascade.OneToOneCascadeDelete.Hbm.Fk.Bidirectional.Mappings.hbm.xml" }; } + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(emp); + + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var session = OpenSession()) + using (var tx = session.BeginTransaction()) + { + session.Delete("from EmployeeInfo"); + session.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var empInfoResults = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoResults.Count); + + var empResults = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empResults.Count); + + var emp = empResults[0]; + Assert.NotNull(emp); + + empId = emp.Id; + emp.Info = null; + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.Null(emp.Info); + + var empInfoResults = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoResults.Count); + + var empResults = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empResults.Count); + + t.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/Mappings.hbm.xml b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/Mappings.hbm.xml new file mode 100644 index 00000000000..5a6124455d7 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/Mappings.hbm.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/Model.cs new file mode 100644 index 00000000000..7a82fd53515 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Bidirectional/Model.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Bidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + public virtual Employee EmployeeDetails { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(Employee emp) + { + EmployeeDetails = emp; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..40804a52e64 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,84 @@ +using NUnit.Framework; +using System.Collections; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Composite +{ + [TestFixture] + public class DeleteOneToOneOrphansTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new string[] { "Cascade.OneToOneCascadeDelete.Hbm.Fk.Composite.Mappings.hbm.xml" }; } + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo( 1L, 1L); + + s.Save(emp.Info); + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var session = OpenSession()) + using (var tx = session.BeginTransaction()) + { + session.Delete("from EmployeeInfo"); + session.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var infoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, infoList.Count ); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + var emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + emp.Info = null; + + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/Mappings.hbm.xml b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/Mappings.hbm.xml new file mode 100644 index 00000000000..f5f295ea4b8 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/Mappings.hbm.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/Model.cs new file mode 100644 index 00000000000..08929fc6529 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Composite/Model.cs @@ -0,0 +1,82 @@ +using System; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Composite +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public class Identifier + { + public virtual long CompanyId { get; set; } + public virtual long PersonId { get; set; } + + public Identifier() + { + + } + + public Identifier(long companyId, long personId) + { + this.CompanyId = companyId; + this.PersonId = personId; + } + + + public override bool Equals(Object o) + { + if (this == o) + { + return true; + } + + var t = this.GetType(); + var u = o.GetType(); + + + if (o == null || !t.IsAssignableFrom(u) || !u.IsAssignableFrom(t)) + { + return false; + } + + var id = o as Identifier; + + return CompanyId.Equals(id.CompanyId) + && PersonId.Equals(id.PersonId); + + } + + public override int GetHashCode() + { + return (31 * CompanyId.GetHashCode()) + PersonId.GetHashCode(); + } + } + + public virtual Identifier Id { get; set; } + public virtual Employee Employee { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(long companyId, long personId) + { + this.Id = new Identifier(companyId, personId); + } + + public EmployeeInfo(Identifier id) + { + this.Id = id; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..9f9b955dfb0 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,84 @@ +using NUnit.Framework; +using System.Collections; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Reversed.Bidirectional +{ + [TestFixture] + public class DeleteOneToOneOrphansTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new string[] { "Cascade.OneToOneCascadeDelete.Hbm.Fk.Reversed.Bidirectional.Mappings.hbm.xml" }; } + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(emp); + + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + Employee emp = empList[0]; + Assert.NotNull(emp.Info ); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual( 0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/Mappings.hbm.xml b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/Mappings.hbm.xml new file mode 100644 index 00000000000..f7ca502f70a --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/Mappings.hbm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/Model.cs new file mode 100644 index 00000000000..21d5b7cabdb --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Bidirectional/Model.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Reversed.Bidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + public virtual Employee EmployeeDetails { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(Employee employee) + { + this.EmployeeDetails = employee; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..4d1c5a431ef --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,135 @@ +using NUnit.Framework; +using System.Collections; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Reversed.Unidirectional +{ + [TestFixture] + public class DeleteOneToOneOrphansTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new string[] { "Cascade.OneToOneCascadeDelete.Hbm.Fk.Reversed.Unidirectional.Mappings.hbm.xml" }; } + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(); + + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + Employee emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + + } + + [Test] + public void TestOrphanedWhileDetached() + { + long empId = 0; + Employee emp; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + + tx.Commit(); + } + + //only fails if the object is detached + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Lock(emp, LockMode.None); + emp.Info = null; + //save using the new session (this used to work prior to 3.5.x) + s.SaveOrUpdate(emp); + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + emp = s.Get(emp.Id); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/Mappings.hbm.xml b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/Mappings.hbm.xml new file mode 100644 index 00000000000..2276a9893f0 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/Mappings.hbm.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/Model.cs new file mode 100644 index 00000000000..a4158191e4a --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Fk/Reversed/Unidirectional/Model.cs @@ -0,0 +1,28 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Fk.Reversed.Unidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(long id) + { + this.Id = id; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..102af12e990 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using System.Collections; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Pk.Bidirectional +{ + [TestFixture] + public class DeleteOneToOneOrphansTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new string[] { "Cascade.OneToOneCascadeDelete.Hbm.Pk.Bidirectional.Mappings.hbm.xml" }; } + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(emp); + + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual( 1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual( 1, empList.Count); + + var emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/Mappings.hbm.xml b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/Mappings.hbm.xml new file mode 100644 index 00000000000..04b24c09121 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/Mappings.hbm.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + EmployeeDetails + + + + + + diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/Model.cs new file mode 100644 index 00000000000..66d16bba04f --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Bidirectional/Model.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Pk.Bidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + public virtual Employee EmployeeDetails { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(Employee employee) + { + this.EmployeeDetails = employee; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..81a5afa3277 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,98 @@ +using NUnit.Framework; +using System.Collections; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Pk.Unidirectional +{ + [TestFixture] + public class DeleteOneToOneOrphansTest : TestCase + { + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override IList Mappings + { + get { return new string[] { "Cascade.OneToOneCascadeDelete.Hbm.Pk.Unidirectional.Mappings.hbm.xml" }; } + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + s.Save(emp); + var info = new EmployeeInfo(emp.Id); + emp.Info = info; + + s.Save(info); + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + Employee emp = empList[0]; + Assert.NotNull(emp.Info); + + var empAndInfoList = s.CreateQuery("from Employee e, EmployeeInfo i where e.Info = i").List(); + Assert.AreEqual(1, empAndInfoList.Count); + + var result = (object[])empAndInfoList[0]; + + emp = result[0] as Employee; + + Assert.NotNull(result[1]); + Assert.AreSame(emp.Info, result[1]); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/Mappings.hbm.xml b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/Mappings.hbm.xml new file mode 100644 index 00000000000..6e37b7c4eed --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/Mappings.hbm.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/Model.cs new file mode 100644 index 00000000000..8efdb807822 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/Hbm/Pk/Unidirectional/Model.cs @@ -0,0 +1,28 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.Hbm.Pk.Unidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(long id) + { + this.Id = id; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Bidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Bidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..d9fc93eccc3 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Bidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,112 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Bidirectional +{ + [TestFixture] + public class DeleteOneToOneOrphansTest : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Identity); + map.Column("Id"); + }); + mc.OneToOne(x => x.Info, map => + { + map.PropertyReference(x => x.EmployeeDetails); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Constrained(false); + }); + }); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Identity); + map.Column("Id"); + }); + mc.ManyToOne(x => x.EmployeeDetails, map => + { + map.Column("employee_id"); + map.Unique(true); + map.NotNullable(true); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(emp); + + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var session = OpenSession()) + using (var tx = session.BeginTransaction()) + { + session.Delete("from EmployeeInfo"); + session.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var empInfoResults = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoResults.Count); + + var empResults = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empResults.Count); + + var emp = empResults[0]; + Assert.NotNull(emp); + + empId = emp.Id; + emp.Info = null; + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.Null(emp.Info); + + var empInfoResults = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoResults.Count); + + var empResults = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empResults.Count); + + t.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Bidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Bidirectional/Model.cs new file mode 100644 index 00000000000..5581814eba8 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Bidirectional/Model.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Bidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + public virtual Employee EmployeeDetails { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(Employee emp) + { + EmployeeDetails = emp; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Composite/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Composite/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..9654c769411 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Composite/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,110 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; +using System; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Composite +{ + public class DeleteOneToOneOrphansTest : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Identity); + map.Column("Id"); + }); + mc.ManyToOne(x => x.Info, map => + { + // Columns have to be declared first otherwise other properties are reset. + map.Columns(x => { x.Name("COMP_ID"); }, + x => { x.Name("PERS_ID"); }); + map.Unique(true); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.NotFound(NotFoundMode.Exception); + }); + }); + + mapper.Class(mc => + { + mc.ComponentAsId(x => x.Id, map => + { + map.Property(x => x.CompanyId, m => m.Column("COMPS_ID")); + map.Property(x => x.PersonId, m => m.Column("PERS_ID")); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo( 1L, 1L); + + s.Save(emp.Info); + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var session = OpenSession()) + using (var tx = session.BeginTransaction()) + { + session.Delete("from EmployeeInfo"); + session.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var infoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, infoList.Count ); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + var emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + emp.Info = null; + + s.Update(emp); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Composite/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Composite/Model.cs new file mode 100644 index 00000000000..6a69a64424a --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Composite/Model.cs @@ -0,0 +1,82 @@ +using System; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Composite +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public class Identifier + { + public virtual long CompanyId { get; set; } + public virtual long PersonId { get; set; } + + public Identifier() + { + + } + + public Identifier(long companyId, long personId) + { + this.CompanyId = companyId; + this.PersonId = personId; + } + + + public override bool Equals(Object o) + { + if (this == o) + { + return true; + } + + var t = this.GetType(); + var u = o.GetType(); + + + if (o == null || !t.IsAssignableFrom(u) || !u.IsAssignableFrom(t)) + { + return false; + } + + var id = o as Identifier; + + return CompanyId.Equals(id.CompanyId) + && PersonId.Equals(id.PersonId); + + } + + public override int GetHashCode() + { + return (31 * CompanyId.GetHashCode()) + PersonId.GetHashCode(); + } + } + + public virtual Identifier Id { get; set; } + public virtual Employee Employee { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(long companyId, long personId) + { + this.Id = new Identifier(companyId, personId); + } + + public EmployeeInfo(Identifier id) + { + this.Id = id; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Bidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Bidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..70d714d6291 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Bidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,110 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Reversed.Bidirectional +{ + public class DeleteOneToOneOrphansTest : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Identity); + map.Column("Id"); + }); + mc.ManyToOne(x => x.Info, map => + { + map.Column("Info_id"); + map.Unique(true); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + + }); + }); + + mapper.Class(cm => + { + cm.Id(x => x.Id, m => + { + m.Generator(Generators.Identity); + m.Column("Id"); + }); + cm.OneToOne(x => x.EmployeeDetails, map => + { + map.PropertyReference(x => x.Info); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(emp); + + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + Employee emp = empList[0]; + Assert.NotNull(emp.Info ); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual( 0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Bidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Bidirectional/Model.cs new file mode 100644 index 00000000000..6c87aa807f7 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Bidirectional/Model.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Reversed.Bidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + public virtual Employee EmployeeDetails { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(Employee employee) + { + this.EmployeeDetails = employee; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Unidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Unidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..22e739dc079 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Unidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,156 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Reversed.Unidirectional +{ + public class DeleteOneToOneOrphansTest : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class(mc => + { + mc.Id(x => x.Id, m => + { + m.Generator(Generators.Identity); + m.Column("Id"); + }); + mc.ManyToOne(x => x.Info, map => + { + map.Column("Info_id"); + map.Unique(true); + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + + }); + }); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Identity); + map.Column("Id"); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(); + + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId = 0; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + Employee emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileDetached() + { + long empId = 0; + Employee emp; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + + tx.Commit(); + } + + //only fails if the object is detached + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Lock(emp, LockMode.None); + emp.Info = null; + //save using the new session (this used to work prior to 3.5.x) + s.SaveOrUpdate(emp); + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + emp = s.Get(emp.Id); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Unidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Unidirectional/Model.cs new file mode 100644 index 00000000000..3e68e0ac061 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Fk/Reversed/Unidirectional/Model.cs @@ -0,0 +1,28 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Fk.Reversed.Unidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(long id) + { + this.Id = id; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Bidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Bidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..4236f7794ed --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Bidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,111 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Pk.Bidirectional +{ + public class DeleteOneToOneOrphansTest : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Identity); + map.Column("Id"); + }); + mc.OneToOne(x => x.Info, map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Constrained(false); + }); + }); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Foreign(x => x.EmployeeDetails)); + map.Column("Id"); + }); + mc.OneToOne(x => x.EmployeeDetails, map => + { + map.Constrained(true); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + emp.Info = new EmployeeInfo(emp); + + s.Save(emp); + s.Flush(); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + s.Flush(); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual( 1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual( 1, empList.Count); + + var emp = empList[0]; + Assert.NotNull(emp.Info); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Bidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Bidirectional/Model.cs new file mode 100644 index 00000000000..f7823eada92 --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Bidirectional/Model.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Pk.Bidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + public virtual Employee EmployeeDetails { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(Employee employee) + { + this.EmployeeDetails = employee; + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Unidirectional/DeleteOneToOneOrphansTest.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Unidirectional/DeleteOneToOneOrphansTest.cs new file mode 100644 index 00000000000..b5fe4a98a8f --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Unidirectional/DeleteOneToOneOrphansTest.cs @@ -0,0 +1,118 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Pk.Unidirectional +{ + public class DeleteOneToOneOrphansTest : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class(mc => + { + mc.Id(x => x.Id, m => + { + m.Generator(Generators.Identity); + m.Column("Id"); + }); + mc.OneToOne(x => x.Info, map => + { + map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans); + map.Constrained(false); + }); + }); + + mapper.Class(mc => + { + mc.Id(x => x.Id, map => + { + map.Generator(Generators.Assigned); + map.Column("Id"); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + base.OnSetUp(); + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var emp = new Employee(); + s.Save(emp); + var info = new EmployeeInfo(emp.Id); + emp.Info = info; + + s.Save(info); + s.Save(emp); + t.Commit(); + } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.Delete("from EmployeeInfo"); + s.Delete("from Employee"); + tx.Commit(); + } + } + + [Test] + public void TestOrphanedWhileManaged() + { + long empId; + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(1, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + Employee emp = empList[0]; + Assert.NotNull(emp.Info); + + var empAndInfoList = s.CreateQuery("from Employee e, EmployeeInfo i where e.Info = i").List(); + Assert.AreEqual(1, empAndInfoList.Count); + + var result = (object[])empAndInfoList[0]; + + emp = result[0] as Employee; + + Assert.NotNull(result[1]); + Assert.AreSame(emp.Info, result[1]); + + empId = emp.Id; + emp.Info = null; + + tx.Commit(); + } + + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + var emp = s.Get(empId); + Assert.IsNull(emp.Info); + + var empInfoList = s.CreateQuery("from EmployeeInfo").List(); + Assert.AreEqual(0, empInfoList.Count); + + var empList = s.CreateQuery("from Employee").List(); + Assert.AreEqual(1, empList.Count); + + tx.Commit(); + } + } + } +} diff --git a/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Unidirectional/Model.cs b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Unidirectional/Model.cs new file mode 100644 index 00000000000..4ac9c9fab5a --- /dev/null +++ b/src/NHibernate.Test/Cascade/OneToOneCascadeDelete/MappingByCode/Pk/Unidirectional/Model.cs @@ -0,0 +1,28 @@ +namespace NHibernate.Test.Cascade.OneToOneCascadeDelete.MappingByCode.Pk.Unidirectional +{ + public class Employee + { + public virtual long Id { get; set; } + public virtual EmployeeInfo Info { get; set; } + + public Employee() + { + + } + } + + public class EmployeeInfo + { + public virtual long Id { get; set; } + + public EmployeeInfo() + { + + } + + public EmployeeInfo(long id) + { + this.Id = id; + } + } +} diff --git a/src/NHibernate.Test/MappingByCode/MappersTests/ManyToOneMapperTest.cs b/src/NHibernate.Test/MappingByCode/MappersTests/ManyToOneMapperTest.cs index 631ec1c05bc..e5e787dbbd5 100644 --- a/src/NHibernate.Test/MappingByCode/MappersTests/ManyToOneMapperTest.cs +++ b/src/NHibernate.Test/MappingByCode/MappersTests/ManyToOneMapperTest.cs @@ -37,16 +37,6 @@ public void AssignCascadeStyle() Assert.That(hbm.cascade.Split(',').Select(w => w.Trim()), Contains.Item("persist").And.Contains("delete")); } - [Test] - public void AutoCleanUnsupportedCascadeStyle() - { - var hbmMapping = new HbmMapping(); - var hbm = new HbmManyToOne(); - var mapper = new ManyToOneMapper(null, hbm, hbmMapping); - mapper.Cascade(Mapping.ByCode.Cascade.Persist | Mapping.ByCode.Cascade.DeleteOrphans | Mapping.ByCode.Cascade.Remove); - Assert.True(hbm.cascade.Split(',').Select(w => w.Trim()).All(w => !w.Contains("orphan"))); - } - [Test] public void CanSetAccessor() { diff --git a/src/NHibernate.Test/MappingByCode/MappersTests/OneToOneMapperTest.cs b/src/NHibernate.Test/MappingByCode/MappersTests/OneToOneMapperTest.cs index e433783095d..ddb3e7fd6b0 100644 --- a/src/NHibernate.Test/MappingByCode/MappersTests/OneToOneMapperTest.cs +++ b/src/NHibernate.Test/MappingByCode/MappersTests/OneToOneMapperTest.cs @@ -28,15 +28,6 @@ public void AssignCascadeStyle() Assert.That(hbm.cascade.Split(',').Select(w => w.Trim()), Contains.Item("persist").And.Contains("delete")); } - [Test] - public void AutoCleanUnsupportedCascadeStyle() - { - var hbm = new HbmOneToOne(); - var mapper = new OneToOneMapper(null, hbm); - mapper.Cascade(Mapping.ByCode.Cascade.Persist | Mapping.ByCode.Cascade.DeleteOrphans | Mapping.ByCode.Cascade.Remove); - Assert.That(hbm.cascade.Split(',').Select(w => w.Trim()).All(cascade => !cascade.Contains("orphan")), Is.True); - } - [Test] public void CanSetAccessor() { diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index c60b89ab659..0123cf1f0b5 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -137,6 +137,25 @@ + + + + + + + + + + + + + + + + + + + @@ -2550,6 +2569,17 @@ + + + + + + + + + + + diff --git a/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs b/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs index c39b61d2ecb..007dd48c156 100644 --- a/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs +++ b/src/NHibernate/Cfg/XmlHbmBinding/PropertiesBinder.cs @@ -235,6 +235,11 @@ private void BindManyToOne(HbmManyToOne manyToOneMapping, ManyToOne model, strin { AddManyToOneSecondPass(model); } + + if (manyToOneMapping.unique) + { + model.IsLogicalOneToOne = true; + } BindForeignKey(manyToOneMapping.foreignkey, model); } diff --git a/src/NHibernate/Engine/Cascade.cs b/src/NHibernate/Engine/Cascade.cs index f211fc40b7f..7689ef90efa 100644 --- a/src/NHibernate/Engine/Cascade.cs +++ b/src/NHibernate/Engine/Cascade.cs @@ -76,6 +76,8 @@ public sealed class Cascade private readonly IEventSource eventSource; private readonly CascadingAction action; + private readonly Stack componentPathStack = new Stack(); + public Cascade(CascadingAction action, CascadePoint point, IEventSource eventSource) { this.point = point; @@ -115,6 +117,7 @@ public void CascadeOn(IEntityPersister persister, object parent, object anything for (int i = 0; i < types.Length; i++) { CascadeStyle style = cascadeStyles[i]; + string propertyName = persister.PropertyNames[i]; if (hasUninitializedLazyProperties && persister.PropertyLaziness[i] && !action.PerformOnLazyProperty) { //do nothing to avoid a lazy property initialization @@ -123,7 +126,7 @@ public void CascadeOn(IEntityPersister persister, object parent, object anything if (style.DoCascade(action)) { - CascadeProperty(parent, persister.GetPropertyValue(parent, i, entityMode), types[i], style, anything, false); + CascadeProperty(parent, persister.GetPropertyValue(parent, i, entityMode), types[i], style, propertyName, anything, false); } else if (action.RequiresNoCascadeChecking) { @@ -136,7 +139,7 @@ public void CascadeOn(IEntityPersister persister, object parent, object anything } /// Cascade an action to the child or children - private void CascadeProperty(object parent, object child, IType type, CascadeStyle style, object anything, bool isCascadeDeleteEnabled) + private void CascadeProperty(object parent, object child, IType type, CascadeStyle style, string propertyName, object anything, bool isCascadeDeleteEnabled) { if (child != null) { @@ -150,7 +153,54 @@ private void CascadeProperty(object parent, object child, IType type, CascadeSty } else if (type.IsComponentType) { - CascadeComponent(parent, child, (IAbstractComponentType)type, anything); + CascadeComponent(parent, child, (IAbstractComponentType)type, propertyName, anything); + } + } + else + { + // potentially we need to handle orphan deletes for one-to-ones here... + if (type.IsEntityType && ((EntityType)type).IsLogicalOneToOne()) + { + // We have a physical or logical one-to-one and from previous checks we know we + // have a null value. See if the attribute cascade settings and action-type require + // orphan checking + if (style.HasOrphanDelete && action.DeleteOrphans) + { + // value is orphaned if loaded state for this property shows not null + // because it is currently null. + EntityEntry entry = eventSource.PersistenceContext.GetEntry(parent); + if (entry != null && entry.Status != Status.Saving) + { + EntityType entityType = (EntityType)type; + object loadedValue; + if (!componentPathStack.Any()) + { + // association defined on entity + loadedValue = entry.GetLoadedValue(propertyName); + } + else + { + // association defined on component + // todo : this is currently unsupported because of the fact that + // we do not know the loaded state of this value properly + // and doing so would be very difficult given how components and + // entities are loaded (and how 'loaded state' is put into the + // EntityEntry). Solutions here are to either: + // 1) properly account for components as a 2-phase load construct + // 2) just assume the association was just now orphaned and + // issue the orphan delete. This would require a special + // set of SQL statements though since we do not know the + // orphaned value, something a delete with a subquery to + // match the owner. + loadedValue = null; + } + + if (loadedValue != null) + { + eventSource.Delete(entry.Persister.EntityName, loadedValue, false, null); + } + } + } } } } @@ -160,18 +210,21 @@ private bool CascadeAssociationNow(IAssociationType associationType) return associationType.ForeignKeyDirection.CascadeNow(point) && (eventSource.EntityMode != EntityMode.Xml || associationType.IsEmbeddedInXML); } - private void CascadeComponent(object parent, object child, IAbstractComponentType componentType, object anything) + private void CascadeComponent(object parent, object child, IAbstractComponentType componentType, string componentPropertyName, object anything) { + componentPathStack.Push(componentPropertyName); object[] children = componentType.GetPropertyValues(child, eventSource); IType[] types = componentType.Subtypes; for (int i = 0; i < types.Length; i++) { CascadeStyle componentPropertyStyle = componentType.GetCascadeStyle(i); + string subPropertyName = componentType.PropertyNames[i]; if (componentPropertyStyle.DoCascade(action)) { - CascadeProperty(parent, children[i], types[i], componentPropertyStyle, anything, false); + CascadeProperty(parent, children[i], types[i], componentPropertyStyle, subPropertyName, anything, false); } } + componentPathStack.Pop(); } private void CascadeAssociation(object parent, object child, IType type, CascadeStyle style, object anything, bool isCascadeDeleteEnabled) @@ -237,7 +290,7 @@ private void CascadeCollectionElements(object parent, object child, CollectionTy log.Info("cascade " + action + " for collection: " + collectionType.Role); foreach (object o in action.GetCascadableChildrenIterator(eventSource, collectionType, child)) - CascadeProperty(parent, o, elemType, style, anything, isCascadeDeleteEnabled); + CascadeProperty(parent, o, elemType, style, null, anything, isCascadeDeleteEnabled); log.Info("done cascade " + action + " for collection: " + collectionType.Role); } diff --git a/src/NHibernate/Mapping/ByCode/Impl/ManyToOneMapper.cs b/src/NHibernate/Mapping/ByCode/Impl/ManyToOneMapper.cs index 6194d5e524d..80024dd16c3 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/ManyToOneMapper.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/ManyToOneMapper.cs @@ -45,7 +45,7 @@ public void Class(System.Type entityType) public void Cascade(Cascade cascadeStyle) { - _manyToOne.cascade = (cascadeStyle.Exclude(ByCode.Cascade.DeleteOrphans)).ToCascadeString(); + _manyToOne.cascade = cascadeStyle.ToCascadeString(); } public void NotNullable(bool notnull) diff --git a/src/NHibernate/Mapping/ByCode/Impl/OneToOneMapper.cs b/src/NHibernate/Mapping/ByCode/Impl/OneToOneMapper.cs index fca7c2964c9..ca3511319a7 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/OneToOneMapper.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/OneToOneMapper.cs @@ -30,7 +30,7 @@ public OneToOneMapper(MemberInfo member, IAccessorPropertyMapper accessorMapper, public void Cascade(Cascade cascadeStyle) { - _oneToOne.cascade = (cascadeStyle.Exclude(ByCode.Cascade.DeleteOrphans)).ToCascadeString(); + _oneToOne.cascade = cascadeStyle.ToCascadeString(); } public void Class(System.Type clazz) diff --git a/src/NHibernate/Mapping/ManyToOne.cs b/src/NHibernate/Mapping/ManyToOne.cs index a1424afba38..b7d9ff5b590 100644 --- a/src/NHibernate/Mapping/ManyToOne.cs +++ b/src/NHibernate/Mapping/ManyToOne.cs @@ -28,6 +28,7 @@ public override void CreateForeignKey() } private bool isIgnoreNotFound = false; + private bool isLogicalOneToOne; public bool IsIgnoreNotFound { @@ -35,6 +36,12 @@ public bool IsIgnoreNotFound set { isIgnoreNotFound = value; } } + public bool IsLogicalOneToOne + { + get { return isLogicalOneToOne; } + set { isLogicalOneToOne = value; } + } + private IType type; public override IType Type { @@ -43,7 +50,7 @@ public override IType Type if (type == null) { type = - TypeFactory.ManyToOne(ReferencedEntityName, ReferencedPropertyName, IsLazy, UnwrapProxy, Embedded, IsIgnoreNotFound); + TypeFactory.ManyToOne(ReferencedEntityName, ReferencedPropertyName, IsLazy, UnwrapProxy, Embedded, IsIgnoreNotFound, isLogicalOneToOne); } return type; } diff --git a/src/NHibernate/Mapping/OneToMany.cs b/src/NHibernate/Mapping/OneToMany.cs index fa42b316ed4..51dbdc4ac5f 100644 --- a/src/NHibernate/Mapping/OneToMany.cs +++ b/src/NHibernate/Mapping/OneToMany.cs @@ -24,7 +24,7 @@ public OneToMany(PersistentClass owner) private EntityType EntityType { - get { return TypeFactory.ManyToOne(ReferencedEntityName, null, false, false, IsEmbedded, IsIgnoreNotFound); } + get { return TypeFactory.ManyToOne(ReferencedEntityName, null, false, false, IsEmbedded, IsIgnoreNotFound, false); } } public bool IsIgnoreNotFound diff --git a/src/NHibernate/Type/EntityType.cs b/src/NHibernate/Type/EntityType.cs index 447ceaf3f05..ca9859df4a8 100644 --- a/src/NHibernate/Type/EntityType.cs +++ b/src/NHibernate/Type/EntityType.cs @@ -253,6 +253,11 @@ public override bool IsMutable public abstract bool IsOneToOne { get; } + public virtual bool IsLogicalOneToOne() + { + return IsOneToOne; + } + public override object Replace(object original, object target, ISessionImplementor session, object owner, IDictionary copyCache) { if (original == null) diff --git a/src/NHibernate/Type/ManyToOneType.cs b/src/NHibernate/Type/ManyToOneType.cs index c5744090372..e7a89af4152 100644 --- a/src/NHibernate/Type/ManyToOneType.cs +++ b/src/NHibernate/Type/ManyToOneType.cs @@ -14,6 +14,7 @@ namespace NHibernate.Type public class ManyToOneType : EntityType { private readonly bool ignoreNotFound; + private readonly bool isLogicalOneToOne; public ManyToOneType(string className) : this(className, false) @@ -24,12 +25,14 @@ public ManyToOneType(string className, bool lazy) : base(className, null, !lazy, true, false) { ignoreNotFound = false; + isLogicalOneToOne = false; } - public ManyToOneType(string entityName, string uniqueKeyPropertyName, bool lazy, bool unwrapProxy, bool isEmbeddedInXML, bool ignoreNotFound) + public ManyToOneType(string entityName, string uniqueKeyPropertyName, bool lazy, bool unwrapProxy, bool isEmbeddedInXML, bool ignoreNotFound, bool isLogicalOneToOne) : base(entityName, uniqueKeyPropertyName, !lazy, isEmbeddedInXML, unwrapProxy) { this.ignoreNotFound = ignoreNotFound; + this.isLogicalOneToOne = isLogicalOneToOne; } public override int GetColumnSpan(IMapping mapping) @@ -60,6 +63,11 @@ public override bool IsOneToOne get { return false; } } + public override bool IsLogicalOneToOne() + { + return isLogicalOneToOne; + } + public override ForeignKeyDirection ForeignKeyDirection { get { return ForeignKeyDirection.ForeignKeyFromParent; } diff --git a/src/NHibernate/Type/TypeFactory.cs b/src/NHibernate/Type/TypeFactory.cs index 9cfd63cbddd..221e0b390a9 100644 --- a/src/NHibernate/Type/TypeFactory.cs +++ b/src/NHibernate/Type/TypeFactory.cs @@ -744,9 +744,9 @@ public static EntityType ManyToOne(string persistentClass, bool lazy) /// A many-to-one association type for the given class and cascade style. /// public static EntityType ManyToOne(string persistentClass, string uniqueKeyPropertyName, bool lazy, bool unwrapProxy, - bool isEmbeddedInXML, bool ignoreNotFound) + bool isEmbeddedInXML, bool ignoreNotFound, bool isLogicalOneToOne) { - return new ManyToOneType(persistentClass, uniqueKeyPropertyName, lazy, unwrapProxy, isEmbeddedInXML, ignoreNotFound); + return new ManyToOneType(persistentClass, uniqueKeyPropertyName, lazy, unwrapProxy, isEmbeddedInXML, ignoreNotFound, isLogicalOneToOne); } public static CollectionType Array(string role, string propertyRef, bool embedded, System.Type elementClass)