Considere las siguientes dos entidades * :
public class Parent { public int Id { get; set; } public string Name { get; set; } public virtual Child PrimaryChild { get; set; } public virtual Child SecondaryChild { get; set; } } public class Child { public int Id { get; set; } public string Name { get; set; } // Foreign keys to be added here or // in the other class (that's what I'm trying to figure out) }Como muestra el código, un padre debe tener un hijo principal y un hijo secundario; y (preferiblemente) cuando se elimina el padre, ambos hijos también deben eliminarse.
¿Es incluso posible representar tal relación con EF?
Si el padre tuviera solo un hijo, podría usar fácilmente una relación uno a uno ( 1 to 0..1 ) al convertir la clave principal de Child en PK y FK. Obviamente, eso no funciona con la situación actual porque, por un lado, una clave principal no puede tener múltiples valores.
Parece que es una situación lo suficientemente común que debería tener una solución bien conocida. Sin embargo, después de revisar innumerables publicaciones, todavía no puedo encontrar ninguna. Lo más parecido que pude encontrar es esta respuesta , pero no funciona porque:
Crea relaciones * to 0..1 .
Cuando ignoré ese hecho e intenté ejecutar el siguiente código:
var c1 = new Child { Name = "C1" }; var c2 = new Child { Name = "C2" }; context.Parents.Add(new Parent { Name = "P1", PrimaryChild = c1, SecondaryChild = c2 });... arrojó una DbUpdateException con el mensaje:
No se puede determinar un orden válido para las operaciones dependientes. Es posible que existan dependencias debido a restricciones de clave externa, requisitos del modelo o valores generados por la tienda.
¿Cómo se puede resolver este problema?
* Este es un ejemplo simplificado. En el escenario de la vida real, existen relaciones entre el padre y otras entidades secundarias con dos o más referencias a cada uno.
Se me ocurrió una solución hacky, pero todavía espero una mejor.
En lugar de relaciones uno a uno, creé una buena relación uno a muchos ( Parent tiene muchos de Child ) con una propiedad en la clase Child para almacenar el tipo (primario/secundario) y creé propiedades sin asignar en el Parent para reemplazar las propiedades de navegación originales.
Así es como se vería el código para el ejemplo de la pregunta:
Clase Child :
public enum ChildType { Primary, Secondary } public class Child { public int Id { get; set; } public string Name { get; set; } public ChildType ChildType { get; set; } [ForeignKey("Parent")] public int ParentId { get; set; } public virtual Parent Parent { get; set; } } Clase de Parent :
public class Parent { public Parent() { Children = new HashSet<Child>(); } public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Child> Children { get; set; } [NotMapped] public Child PrimaryChild { get { return Children.SingleOrDefault(c => c.ChildType == ChildType.Primary); } set { var existingChild = Children.SingleOrDefault(c => c.ChildType == ChildType.Primary); if (existingChild != null) Children.Remove(existingChild); Children.Add(value); } } [NotMapped] public Child SecondaryChild { get { return Children.SingleOrDefault(c => c.ChildType == ChildType.Secondary); } set { var existingChild = Children.SingleOrDefault(c => c.ChildType == ChildType.Secondary); if (existingChild != null) Children.Remove(existingChild); Children.Add(value); } } }Uso:
var c1 = new Child { Name = "C1", ChildType = ChildType.Primary }; var c2 = new Child { Name = "C2", ChildType = ChildType.Secondary }; context.Parents.Add(new Parent { Name = "P1", PrimaryChild = c1, SecondaryChild = c2 }); Obviamente, esto no impone el número mínimo o máximo de hijos que el padre puede tener o qué ChildType deben ser. Aunque es lo mejor que se me ocurrió.
Hay dos formas de crear tales relaciones:
La primera opción restringe muy fácilmente a un padre a dos hijos y hace que sea muy fácil distinguir entre el hijo principal y el secundario.
La segunda opción, necesitaría agregar un tipo a los objetos secundarios si necesita distinguir el primario del secundario. El uso de la identificación del padre y el tipo como un índice único le permitiría limitar los hijos a un solo hijo principal y un solo hijo secundario por padre.
Ahora, en cuanto a cuándo se elimina el padre, si tiene eliminaciones en cascada para la relación, sucederá automáticamente. Sé que en la segunda opción, el simple uno a muchos, las eliminaciones en cascada funcionarán como se esperaba. La primera opción, básicamente dos relaciones uno a uno, puede configurar eliminaciones en cascada, pero sospecho que deberá asegurarse de que sea una cascada unidireccional. por ejemplo, si se elimina el elemento principal, los elementos secundarios también lo son, pero si se elimina un elemento secundario, el elemento principal debe permanecer.
Me inclinaría por que los niños tengan las claves extranjeras y un tipo. Así que algo a lo largo de estas líneas:
public class Parent { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Child> Children { get; set; } } public class Child { public int Id { get; set; } public string Name { get; set; } [Index("IX_Child_Parent_Type", 1, IsUnique = true)] public int ParentId { get; set; } public virtual Parent Parent { get; set; } [Index("IX_Child_Parent_Type", 2, IsUnique = true)] public int ChildTypeId { get; set; } public virtual ChildType Type { get; set; } } public class ChildType { public int Id { get; set; } public string Name { get; set; } }La otra opción quedaría así:
public class Parent { public int Id { get; set; } public string Name { get; set; } public virtual Child PrimaryChild { get; set; } public virtual Child SecondaryChild { get; set; } } public class Child { public int Id { get; set; } public string Name { get; set; } }Prueba este método,
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp2 { // this class will represent a person, a parent or a child since they have the same attributes class Person { protected int _id; protected string _name; public int Id { get => _id; } public string Name { get => _name; } // Any object wether it's a parent or a child must be instantiated with those properties public Person(int id, string name) { this._id = id; this._name = name; } } // this represents a parent which is a persone but have two persons reprenseting it's children. the children can't be instanciated alone. class Parent : Person { private Person _primaryChild; private Person _secondaryChild; public Person PrimaryChild { get => _primaryChild; } public Person SecondaryChild { get => _secondaryChild; } // this creates the parent with it's children. one the parent dispose its children will be deleted. public Parent(Person parent, Person primaryChild, Person secondaryChild) : base( parent.Id, parent.Name) { // this primaryChild enforce that a parent must have two different children to represents a primary and a secondry child. if(primaryChild.Id != secondaryChild.Id) { this._id = parent.Id; this._name = parent.Name; this._primaryChild = primaryChild; this._secondaryChild = secondaryChild; } else { throw new Exception("Children must not be tweens."); } } } class Program { static void Main(string[] args) { // instanciating a person does'nt represent an actor in the process. A parent must be instianciated. try { // creating a new parent with its related children var parent = new Parent(new Person(1, "Parent"), new Person(1, "ChildOne"), new Person(2, "ChildTwo")); // diplaying children names Console.WriteLine($"Primary Child is {parent.PrimaryChild.Name}. Secondary Child is {parent.SecondaryChild.Name}."); } catch (Exception e) { Console.WriteLine(e.Message); } } } }