Relaciones uno a varios
Las relaciones de uno a varios se usan cuando una sola entidad está asociada a cualquier número de otras entidades. Por ejemplo, una entidad Blog
puede tener muchas entidades Posts
asociadas, pero cada entidad Post
está asociada a una sola entidad Blog
.
Este documento está estructurado en torno a muchos ejemplos. Los ejemplos comienzan con casos comunes, que también presentan conceptos. En ejemplos posteriores, se tratan tipos menos comunes de configuración. Una buena estrategia aquí es comprender los primeros ejemplos y conceptos y, luego, ir a los últimos ejemplos en función de sus necesidades específicas. Según este enfoque, comenzaremos con relaciones de uno a varios sencillas "obligatorias" y "opcionales".
Sugerencia
El código de todos los ejemplos siguientes se puede encontrar en OneToMany.cs.
De uno a varios obligatoria
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
Una relación de uno a varios consta de:
- Una o varias propiedades de clave principal o alternativa en la entidad principal; que es el extremo "uno" de la relación. Por ejemplo:
Blog.Id
. - Una o varias propiedades de clave externa en la entidad dependiente; que es el extremo "varios" de la relación. Por ejemplo:
Post.BlogId
. - Opcionalmente, una navegación de recopilación en la entidad principal que hace referencia a las entidades dependientes. Por ejemplo:
Blog.Posts
. - Opcionalmente, una navegación de referencia en la entidad dependiente que hace referencia a la entidad principal. Por ejemplo:
Post.Blog
.
Por lo tanto, para la relación de este ejemplo:
- La propiedad de clave externa
Post.BlogId
no acepta valores NULL. Esto convierte a la relación en "obligatoria", porque cada entidad dependiente (Post
) debe estar relacionada con alguna principal (Blog
), ya que su propiedad de clave externa debe establecerse en algún valor. - Ambas entidades tienen navegaciones que apuntan a la entidad relacionada o a las entidades del otro lado de la relación.
Nota
Una relación obligatoria garantiza que todas las entidades dependientes deben estar asociadas a alguna entidad principal. Sin embargo, una entidad principal siempre puede existir sin ninguna entidad dependiente. Es decir, una relación obligatoria no indica que siempre haya al menos una entidad dependiente. El modelo EF no permite de ninguna manera, ni tampoco las bases de datos relacionales, garantizar que una entidad de seguridad esté asociada a un determinado número de dependientes. Si es necesario, debe implementarse en la lógica de la aplicación (de negocios). Para más información, consulte Navegaciones obligatorias.
Sugerencia
Una relación con dos navegaciones, una de la entidad dependiente a la principal y una inversa de la entidad principal a la dependiente, se conoce como relación bidireccional.
Esta relación se detecta por convención. Es decir:
Blog
se detecta como la entidad principal de la relación yPost
se detecta como la dependiente.Post.BlogId
se detecta como una clave externa de la entidad dependiente que hace referencia a la clave principalBlog.Id
de la entidad de seguridad. La relación se detecta como obligatoria porquePost.BlogId
no admite valores NULL.Blog.Posts
se detecta como la navegación de recopilación.Post.Blog
se detecta como la navegación de referencia.
Importante
Cuando se usan tipos de referencia que admiten valores NULL de C#, la navegación de referencia debe admitir valores NULL si la propiedad de clave externa también lo hace. Si la propiedad de clave externa no admite valores NULL, la navegación de referencia puede admitir o no valores NULL. En este caso, Post.BlogId
no admite valores NULL y Post.Blog
tampoco lo hace. La construcción = null!;
se usa para marcar este caso como intencional para el compilador de C#, ya que EF normalmente establece la instancia Blog
y no puede ser null en una relación totalmente cargada. Para más información, consulte Tipos de referencia que admiten valores NULL.
En los casos en los que las navegaciones, la clave externa o la naturaleza obligatoria-opcional de la relación no se detecten por convención, estos elementos se pueden configurar explícitamente. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
En el ejemplo anterior, la configuración de las relaciones comienza con HasMany
en el tipo de entidad principal (Blog
) y, luego, sigue con WithOne
. Al igual que con todas las relaciones, es exactamente lo mismo que empezar con el tipo de entidad dependiente (Post
) y usar HasOne
seguido de WithMany
. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(e => e.Blog)
.WithMany(e => e.Posts)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
Ninguna de estas opciones es mejor que la otra; ambas dan como resultado exactamente la misma configuración.
Sugerencia
Nunca es necesario configurar una relación dos veces: una desde la entidad principal y luego otra vez desde la dependiente. Además, el intento de configurar por separado las mitades principal y dependiente de una relación no funciona por norma general. Elija configurar cada relación desde un extremo u otro y, luego, escriba el código de configuración una sola vez.
De uno a varios opcional
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int? BlogId { get; set; } // Optional foreign key property
public Blog? Blog { get; set; } // Optional reference navigation to principal
}
Es igual que el ejemplo anterior, salvo que la propiedad de clave externa y la navegación a la entidad principal ahora admiten valores NULL. Esto convierte a la relación en "opcional" porque una entidad dependiente (Post
) puede existir sin estar relacionada con ninguna entidad principal (Blog
).
Importante
Cuando se usan tipos de referencia que admiten valores NULL de C#, la navegación de referencia debe admitir valores NULL si la propiedad de clave externa también lo hace. En este caso, Post.BlogId
admite valores NULL, por lo que Post.Blog
también debe hacerlo. Para más información, consulte Tipos de referencia que admiten valores NULL.
Como antes, esta relación se detecta por convención. En los casos en los que las navegaciones, la clave externa o la naturaleza obligatoria-opcional de la relación no se detecten por convención, estos elementos se pueden configurar explícitamente. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired(false);
}
De uno a varios obligatoria con clave externa de propiedad reemplazada
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
En algunos casos, es posible que no quiera una propiedad de clave externa en el modelo, ya que las claves externas son un detalle de cómo se representa la relación en la base de datos, lo que no es necesario cuando se usa la relación de una manera puramente orientada a objetos. Sin embargo, si las entidades se van a serializar, por ejemplo, para enviarse a través de una conexión, los valores de clave externa pueden ser una manera útil de mantener intacta la información de la relación cuando las entidades no están en forma de objeto. Por lo tanto, suele ser pragmático mantener las propiedades de clave externa en el tipo de .NET para este fin. Las propiedades de clave externa pueden ser privadas, lo que suele ser un buen compromiso para evitar exponer la clave externa, al tiempo que permite que su valor viaje con la entidad.
Siguiendo con los dos ejemplos anteriores, en este ejemplo se elimina la propiedad de clave externa del tipo de entidad dependiente. Por lo tanto, EF crea una propiedad de clave externa reemplazada denominada BlogId
de tipo int
.
Un punto importante que se debe tener en cuenta aquí es que se usan tipos de referencia que admiten valores NULL en C#, por lo que la nulabilidad de la navegación de referencia se usa para determinar si la propiedad de clave externa admite valores NULL y, por tanto, si la relación es opcional u obligatoria. Si no se usan tipos de referencia que admiten valores NULL, la propiedad de clave externa reemplazada admitirá valores NULL de forma predeterminada, lo que hace que la relación sea opcional de forma predeterminada. En este caso, use IsRequired
para forzar que la propiedad de clave externa reemplazada no admita valores NULL y así hacer que la relación sea obligatoria.
Como antes, esta relación se detecta por convención. En los casos en los que las navegaciones, la clave externa o la naturaleza obligatoria-opcional de la relación no se detecten por convención, estos elementos se pueden configurar explícitamente. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey("BlogId")
.IsRequired();
}
De uno a varios opcional con clave externa de propiedad reemplazada
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public Blog? Blog { get; set; } // Optional reference navigation to principal
}
Al igual que en el ejemplo anterior, la propiedad de clave externa se ha quitado del tipo de entidad dependiente. Por lo tanto, EF crea una propiedad de clave externa reemplazada denominada BlogId
de tipo int?
. A diferencia del ejemplo anterior, esta vez se crea la propiedad de clave externa como que admite valores NULL porque se usan tipos de referencia que admiten valores NULL de C# y la navegación en el tipo de entidad dependiente admite valores NULL. Esto hace que la relación sea opcional.
Cuando no se usan tipos de referencia que admiten valores NULL de C#, la propiedad de clave externa también se creará de forma predeterminada como que admite valores NULL. Esto significa que las relaciones con las propiedades reemplazadas creadas automáticamente son opcionales de forma predeterminada.
Como antes, esta relación se detecta por convención. En los casos en los que las navegaciones, la clave externa o la naturaleza obligatoria-opcional de la relación no se detecten por convención, estos elementos se pueden configurar explícitamente. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey("BlogId")
.IsRequired(false);
}
De uno a varios sin navegación a la entidad principal
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
}
En este ejemplo, se ha vuelto a introducir la propiedad de clave externa, pero se ha quitado la navegación en la entidad dependiente.
Sugerencia
Una relación con solo una navegación, una de la entidad dependiente a la principal o una de la entidad principal a la dependiente, pero no ambas, se conoce como relación unidireccional.
Como antes, esta relación se detecta por convención. En los casos en los que las navegaciones, la clave externa o la naturaleza obligatoria-opcional de la relación no se detecten por convención, estos elementos se pueden configurar explícitamente. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
Observe que la llamada a WithOne
no tiene argumentos. Esta es la manera de indicar a EF que no hay navegación de Post
a Blog
.
Si la configuración comienza desde la entidad sin navegación, el tipo de la entidad del otro extremo de la relación se debe especificar explícitamente mediante la llamada genérica HasOne<>()
. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne<Blog>()
.WithMany(e => e.Posts)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
De uno a varios sin navegación a la entidad principal y con clave externa de propiedad reemplazada
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
}
En este ejemplo se combinan dos de los ejemplos anteriores, de forma que se quita la propiedad de clave externa y la navegación de la entidad dependiente.
Esta relación se detecta por convención como una relación opcional. Puesto que no hay nada en el código que pueda usarse para indicar que debe ser obligatoria, se necesita una configuración mínima mediante IsRequired
para crear una relación obligatoria. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.IsRequired();
}
Se puede usar una configuración más completa para configurar explícitamente el nombre de la navegación y de la clave externa, con una llamada adecuada a IsRequired()
o a IsRequired(false)
según sea necesario. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.HasForeignKey("BlogId")
.IsRequired();
}
De uno a varios sin navegación a dependientes
// Principal (parent)
public class Blog
{
public int Id { get; set; }
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
Los dos ejemplos anteriores tenían navegaciones de la entidad principal a la dependiente, pero no había navegación de la dependiente a la principal. En los dos ejemplos siguientes, se vuelve a introducir la navegación en la entidad dependiente, mientras que la navegación en la principal se quita.
Como antes, esta relación se detecta por convención. En los casos en los que las navegaciones, la clave externa o la naturaleza obligatoria-opcional de la relación no se detecten por convención, estos elementos se pueden configurar explícitamente. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(e => e.Blog)
.WithMany()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
Observe de nuevo que WithMany()
se llama sin argumentos para indicar que no hay navegación en esta dirección.
Si la configuración comienza desde la entidad sin navegación, el tipo de la entidad en el otro extremo de la relación debe especificarse explícitamente mediante la llamada genérica HasMany<>()
. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
De uno a varios sin navegaciones
En ocasiones, puede ser útil configurar una relación sin navegaciones. Esta relación solo se puede manipular cambiando directamente el valor de clave externa.
// Principal (parent)
public class Blog
{
public int Id { get; set; }
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
}
Esta relación no se detecta por convención, ya que no hay ninguna navegación que indique que los dos tipos están relacionados. Se puede configurar explícitamente en OnModelCreating
. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne();
}
Con esta configuración, la propiedad Post.BlogId
se sigue detectando como clave externa por convención y la relación es obligatoria porque la propiedad de clave externa no admite valores NULL. La relación se puede convertir en "opcional" haciendo que la propiedad de clave externa admita valores NULL.
Una configuración explícita más completa de esta relación es la siguiente:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
De uno a varios con clave alternativa
En todos los ejemplos hasta ahora, la propiedad de clave externa de la entidad dependiente está restringida a la propiedad de clave principal de la entidad principal. En su lugar, la clave externa se puede restringir a una propiedad diferente, que entonces se convierte en una clave alternativa para el tipo de entidad principal. Por ejemplo:
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public int AlternateId { get; set; } // Alternate key as target of the Post.BlogId foreign key
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
Esta relación no se detecta por convención, ya que EF siempre creará, por convención, una relación con la clave principal. Se puede configurar explícitamente en OnModelCreating
mediante una llamada a HasPrincipalKey
. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => e.AlternateId);
}
HasPrincipalKey
se puede combinar con otras llamadas para configurar explícitamente las navegaciones, las propiedades de clave externa y la naturaleza obligatoria u opcional. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => e.AlternateId)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
De uno a varios con clave externa compuesta
En todos los ejemplos hasta ahora, la propiedad de clave principal o alternativa de la entidad principal consta de una sola propiedad. Las claves principales o alternativas también se pueden formar a partir de más de una propiedad, que se conocen como "claves compuestas". Cuando la entidad principal de una relación tiene una clave compuesta, la clave externa de la entidad dependiente también debe ser una clave compuesta con el mismo número de propiedades. Por ejemplo:
// Principal (parent)
public class Blog
{
public int Id1 { get; set; } // Composite key part 1
public int Id2 { get; set; } // Composite key part 2
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId1 { get; set; } // Required foreign key property part 1
public int BlogId2 { get; set; } // Required foreign key property part 2
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
Esta relación se detecta por convención. Sin embargo, la propia clave compuesta debe configurarse explícitamente:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasKey(e => new { e.Id1, e.Id2 });
}
Importante
Se considera que un valor de clave externa compuesta es null
si alguno de sus valores de propiedad es NULL. Una clave externa compuesta con una propiedad NULL y otra que no sea NULL no se considerará una coincidencia en el caso de una clave principal o alternativa con los mismos valores. Ambas se considerarán null
.
Tanto HasForeignKey
como HasPrincipalKey
se pueden usar para especificar explícitamente claves con varias propiedades. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(
nestedBuilder =>
{
nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });
nestedBuilder.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => new { e.Id1, e.Id2 })
.HasForeignKey(e => new { e.BlogId1, e.BlogId2 })
.IsRequired();
});
}
Sugerencia
En el código anterior, las llamadas a HasKey
y HasMany
se han agrupado en un generador anidado. Los generadores anidados eliminan la necesidad de llamar a Entity<>()
varias veces cuando el tipo de entidad es el mismo, pero son funcionalmente equivalentes a llamar a Entity<>()
varias veces.
De uno a varios obligatoria sin eliminación en cascada
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
Por convención, las relaciones obligatorias están configuradas para eliminarse en cascada; esto significa que, cuando se elimina la entidad principal, también se eliminan todas sus entidades dependientes, ya que estas no pueden existir en la base de datos sin ella. Es posible configurar EF para iniciar una excepción en lugar de eliminar automáticamente las filas dependientes que ya no pueden existir:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.OnDelete(DeleteBehavior.Restrict);
}
De uno a varios con referencia automática
En todos los ejemplos anteriores, el tipo de entidad principal era diferente del tipo de entidad dependiente. Pero no siempre es así. Por ejemplo, en los tipos siguientes, cada entidad Employee
está relacionada con otras entidades Employees
.
public class Employee
{
public int Id { get; set; }
public int? ManagerId { get; set; } // Optional foreign key property
public Employee? Manager { get; set; } // Optional reference navigation to principal
public ICollection<Employee> Reports { get; } = new List<Employee>(); // Collection navigation containing dependents
}
Esta relación se detecta por convención. En los casos en los que las navegaciones, la clave externa o la naturaleza obligatoria-opcional de la relación no se detecten por convención, estos elementos se pueden configurar explícitamente. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasOne(e => e.Manager)
.WithMany(e => e.Reports)
.HasForeignKey(e => e.ManagerId)
.IsRequired(false);
}