Compartir a través de


Convenciones para la detección de relaciones

EF Core usa un conjunto de convenciones al detectar y compilar un modelo basado en clases de tipo de entidad. En este documento se resumen las convenciones usadas para detectar y configurar relaciones entre tipos de entidad.

Importante

Las convenciones que se describen aquí se pueden invalidar mediante la configuración explícita de la relación mediante atributos de asignación o la API de creación de modelos.

Sugerencia

El código siguiente se puede encontrar en RelationshipConventions.cs.

Detección de navegaciones

La detección de relaciones comienza mediante la detección de navegaciones entre tipos de entidad.

Navegación de referencia

Cuando se detecta una propiedad de un tipo de entidad como navegación de referencia :

  • La propiedad es pública.
  • La propiedad tiene un getter y un setter.
    • El establecedor no necesita ser público; puede ser privado o tener cualquier otra accesibilidad.
    • El establecedor/modificador puede ser Init-only.
  • El tipo de propiedad es, o podría ser, un tipo de entidad. Esto significa que el tipo
  • La propiedad no es estática.
  • La propiedad no es una propiedad de indizador.

Por ejemplo, considere los siguientes tipos de entidad:

public class Blog
{
    // Not discovered as reference navigations:
    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public Uri? Uri { get; set; }
    public ConsoleKeyInfo ConsoleKeyInfo { get; set; }
    public Author DefaultAuthor => new() { Name = $"Author of the blog {Title}" };

    // Discovered as a reference navigation:
    public Author? Author { get; private set; }
}

public class Author
{
    // Not discovered as reference navigations:
    public Guid Id { get; set; }
    public string Name { get; set; } = null!;
    public int BlogId { get; set; }

    // Discovered as a reference navigation:
    public Blog Blog { get; init; } = null!;
}

Para estos tipos, Blog.Author y Author.Blog se detectan como navegaciones de referencia. Por otro lado, las siguientes propiedades no se detectan como navegaciones de referencia:

  • Blog.Id, porque int es un tipo primitivo asignado
  • Blog.Title, porque 'string' es un tipo primitivo asignado
  • Blog.Uri, porque Uri se convierte automáticamente en un tipo primitivo asignado.
  • Blog.ConsoleKeyInfo, porque ConsoleKeyInfo es un tipo de valor de C#
  • Blog.DefaultAuthor, porque la propiedad no tiene un método para establecerla
  • Author.Id, porque Guid es un tipo primitivo asignado
  • Author.Name, porque 'string' es un tipo primitivo asignado
  • Author.BlogId, porque int es un tipo primitivo asignado

Navegaciones de colecciones

Una propiedad de un tipo de entidad se detecta como una navegación de colección cuando:

  • La propiedad es pública.
  • La propiedad tiene un captador. Las navegaciones de colección pueden tener establecedores, pero esto no es necesario.
  • El tipo de propiedad es o implementa IEnumerable<TEntity>, donde TEntity es o podría ser un tipo de entidad. Esto significa que el tipo de TEntity:
  • La propiedad no es estática.
  • La propiedad no es una propiedad de indizador.

Por ejemplo, en el código siguiente, tanto Blog.Tags como Tag.Blogs se detectan como navegaciones de colección.

public class Blog
{
    public int Id { get; set; }
    public List<Tag> Tags { get; set; } = null!;
}

public class Tag
{
    public Guid Id { get; set; }
    public IEnumerable<Blog> Blogs { get; } = new List<Blog>();
}

Emparejamiento de navegaciones

Una vez que se detecta una navegación desde, por ejemplo, el tipo de entidad A al tipo de entidad B, debe determinarse a continuación si esta navegación tiene un inverso que va en la dirección opuesta, es decir, del tipo de entidad B al tipo de entidad A. Si se encuentra este tipo de inverso, las dos navegaciones se emparejan para formar una única relación bidireccional.

El tipo de relación se determina según si la navegación y su inversa son navegaciones de referencia o de colección. Concretamente:

  • Si una navegación es una navegación de colección y la otra es una navegación de referencia, la relación es uno a varios.
  • Si ambas navegaciones son navegaciones de referencia, la relación es uno a uno.
  • Si ambas navegaciones son navegaciones de colección, la relación es de muchos a muchos.

La detección de cada uno de estos tipos de relación se muestra en los ejemplos siguientes:

Una única relación uno-a-muchos entre Blog y Post se detecta mediante el emparejamiento de las navegaciones Blog.Posts y Post.Blog.

public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

Se descubre una única relación uno a uno entre Blog y Author mediante el emparejamiento de las navegaciones Blog.Author y Author.Blog.

public class Blog
{
    public int Id { get; set; }
    public Author? Author { get; set; }
}

public class Author
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

Se detecta una relación de varios a varios entre Post y Tag mediante el emparejamiento de las navegaciones Post.Tags y Tag.Posts.

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

Nota:

Este emparejamiento de navegaciones puede ser incorrecto si las dos navegaciones representan dos relaciones unidireccionales diferentes. En este caso, las dos relaciones deben configurarse explícitamente.

El emparejamiento de relaciones solo funciona cuando hay una única relación entre dos tipos. Se deben configurar explícitamente varias relaciones entre dos tipos.

Nota:

Las descripciones aquí se encuentran en términos de relaciones entre dos tipos diferentes. Sin embargo, es posible que el mismo tipo esté en ambos extremos de una relación, y, por tanto, que un único tipo tenga dos navegaciones que se emparejan entre sí. Esto se denomina relación de autorreferencia.

Detección de propiedades de clave externa

Una vez que las navegaciones de una relación se han detectado o configurado explícitamente, estas navegaciones se usan para detectar las propiedades de clave externa adecuadas para la relación. Una propiedad se detecta como una clave externa cuando:

  • El tipo de propiedad es compatible con la clave principal o alternativa en el tipo de entidad principal.
    • Los tipos son compatibles si son iguales, o si el tipo de la propiedad de clave externa es una versión anulable del tipo de propiedad de clave principal o alternativa.
  • El nombre de propiedad coincide con una de las convenciones de nomenclatura de una propiedad de clave externa. Las convenciones de nomenclatura son:
    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity type name><principal key property name>
    • <principal entity type name>Id
  • Además, si el extremo dependiente se ha configurado explícitamente mediante la API de creación de modelos y la clave principal dependiente es compatible, la clave principal dependiente también se usará como clave externa.

Sugerencia

El sufijo "Id" puede tener cualquier combinación de mayúsculas y minúsculas.

Los siguientes tipos de entidad muestran ejemplos para cada una de estas convenciones de nomenclatura.

Post.TheBlogKey se detecta como clave externa porque coincide con el patrón <navigation property name><principal key property name>:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.TheBlogID se detecta como clave externa porque coincide con el patrón <navigation property name>Id:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogID { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.BlogKey se detecta como clave externa porque coincide con el patrón <principal entity type name><principal key property name>:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.Blogid se detecta como clave externa porque coincide con el patrón <principal entity type name>Id:

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? Blogid { get; set; }
    public Blog? TheBlog { get; set; }
}

Nota:

En el caso de las relaciones de uno a muchos, las propiedades de clave externa deben estar en el tipo que tiene la navegación de referencia, ya que esta será la entidad dependiente. En el caso de relaciones uno a uno, se utiliza la identificación de una propiedad de clave externa para determinar qué tipo representa el extremo dependiente de la relación. Si no se detecta ninguna propiedad de clave externa, el extremo dependiente debe configurarse mediante HasForeignKey. Consulte Relaciones uno a uno para ver ejemplos de esto.

Las reglas anteriores también se aplican a las claves externas compuestas, donde cada propiedad del compuesto debe tener un tipo compatible con la propiedad correspondiente de la clave principal o alternativa, y cada nombre de propiedad debe coincidir con una de las convenciones de nomenclatura descritas anteriormente.

Determinación de la cardinalidad

EF usa las navegaciones detectadas y las propiedades de clave externa para determinar la cardinalidad de la relación junto con sus extremos principales y dependientes:

  • Si hay una navegación de referencia no emparejada, la relación se configura como unidireccional de uno a varios, teniendo la navegación de referencia en el extremo dependiente.
  • Si hay una navegación de colección no emparejada, la relación se configura como una de uno a varios de forma unidireccional, con la navegación de la colección en el extremo principal.
  • Si hay navegaciones de referencia y colección emparejadas, la relación se configura como bidireccional de uno a muchos, con la navegación de colección en el extremo principal.
  • Si una navegación de referencia está emparejada con otra navegación de referencia, haga lo siguiente:
    • Si se detectó una propiedad de clave externa en un lado pero no en el otro, entonces la relación se configura como un uno a uno bidireccional, con la propiedad de clave externa en el extremo dependiente.
    • De lo contrario, el lado dependiente no se puede determinar y EF produce una excepción que indica que el dependiente debe configurarse explícitamente.
  • Si una navegación de colección está emparejada con otra navegación de colección, la relación se configura como un muchos a muchos bidireccional.

Propiedades de clave externa de sombra

Si EF ha determinado el extremo dependiente de la relación, pero no se detectó ninguna propiedad de clave externa, entonces EF creará una propiedad en sombra para representar la clave externa. La propiedad sombra:

  • Tiene el tipo de propiedad de clave primaria o alternativa en el extremo principal de la relación.
    • El tipo se convierte en nullable de forma predeterminada, lo que hace que la relación sea opcional de forma predeterminada.
  • Si hay una navegación en el extremo dependiente, la propiedad de clave externa en sombra se denomina utilizando el nombre de navegación concatenado con el nombre de la propiedad de clave principal o alternativa.
  • Si no hay navegación en el extremo dependiente, la propiedad de clave externa oculta se denomina utilizando el nombre del tipo de entidad principal concatenado con el nombre de la propiedad de clave primaria o alternativa en la base de datos.

Eliminación en cascada

Por convención, las relaciones necesarias se configuran para eliminar en cascada. Las relaciones opcionales están configuradas para no eliminar en cascada.

Varios a varios

Las relaciones de varios a varios no tienen extremos principales y dependientes, y ninguno de ellos contiene una propiedad de clave externa. En su lugar, las relaciones de muchos a muchos utilizan un tipo de entidad de unión que contiene pares de claves externas que apuntan a ambos extremos del muchos a muchos. Tenga en cuenta los siguientes tipos de entidad, para los que se detecta una relación de varios a varios por convención:

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

Las convenciones usadas en esta detección son:

  • El tipo de entidad de combinación se denomina <left entity type name><right entity type name>. Por lo tanto, PostTag en este ejemplo.
    • La tabla de combinación tiene el mismo nombre que el tipo de entidad de combinación.
  • El tipo de entidad de combinación recibe una propiedad de clave externa para cada dirección de la relación. Estos se denominan <navigation name><principal key name>. Por lo tanto, en este ejemplo, las propiedades de clave externa son PostsId y TagsId.
    • Para un muchos a muchos unidireccional, la propiedad de clave externa sin una navegación asociada se denomina <principal entity type name><principal key name>.
  • Las propiedades de clave externa no admiten valores NULL, lo que hace que se requieran ambas relaciones con la entidad de combinación.
    • Las convenciones de eliminación en cascada significan que estas relaciones se configurarán para la eliminación en cascada.
  • El tipo de entidad de unión se configura con una clave primaria compuesta que consta de las dos propiedades de clave externa. Por lo tanto, en este ejemplo, la clave principal se compone de PostsId y TagsId.

Esto da como resultado el siguiente modelo de EF:

Model:
  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Tags (ICollection<Tag>) CollectionTag Inverse: Posts
    Keys:
      Id PK
  EntityType: Tag
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Posts (ICollection<Post>) CollectionPost Inverse: Tags
    Keys:
      Id PK
  EntityType: PostTag (Dictionary<string, object>) CLR Type: Dictionary<string, object>
    Properties:
      PostsId (no field, int) Indexer Required PK FK AfterSave:Throw
      TagsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      PostsId, TagsId PK
    Foreign keys:
      PostTag (Dictionary<string, object>) {'PostsId'} -> Post {'Id'} Cascade
      PostTag (Dictionary<string, object>) {'TagsId'} -> Tag {'Id'} Cascade
    Indexes:
      TagsId

Y se traduce en el siguiente esquema de base de datos cuando se usa SQLite:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tag" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tag_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tag" ("Id") ON DELETE CASCADE);

CREATE INDEX "IX_PostTag_TagsId" ON "PostTag" ("TagsId");

Índices

Por convención, EF crea un índice de base de datos para la propiedad o propiedades de una clave externa. El tipo de índice creado viene determinado por:

  • Cardinalidad de la relación
  • Si la relación es opcional o necesaria
  • Número de propiedades que componen la clave externa

Para una relación uno a varios, se crea un índice sencillo por convención. Se crea el mismo índice para las relaciones opcionales y necesarias. Por ejemplo, en SQLite:

CREATE INDEX "IX_Post_BlogId" ON "Post" ("BlogId");

O en SQL Server:

CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);

Para una relación uno a uno necesaria, se crea un índice único. Por ejemplo, en SQLite:

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

O en SQL Sever:

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]);

Para las relaciones de uno a uno opcionales, el índice creado en SQLite es el mismo:

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

Sin embargo, en SQL Server, se agrega un IS NOT NULL filtro para controlar mejor los valores de clave externa null. Por ejemplo:

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]) WHERE [BlogId] IS NOT NULL;

Para las claves externas compuestas, se crea un índice que abarca todas las columnas de clave externa. Por ejemplo:

CREATE INDEX "IX_Post_ContainingBlogId1_ContainingBlogId2" ON "Post" ("ContainingBlogId1", "ContainingBlogId2");

Nota:

EF no crea índices para las propiedades que ya están cubiertas por un índice existente o una restricción de clave principal.

Cómo detener la creación de índices de EF para claves externas

Los índices tienen sobrecarga y, como se le pide aquí, puede que no siempre sea adecuado crearlos para todas las columnas de FK. Para lograrlo, ForeignKeyIndexConvention se puede quitar al compilar el modelo:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Cuando se desea, los índices se pueden crear explícitamente para esas columnas de clave externa que las necesiten.

Nombres de restricción de clave externa

Por convención, las restricciones de clave externa se denominan FK_<dependent type name>_<principal type name>_<foreign key property name>. En el caso de las claves externas compuestas, <foreign key property name> se convierte en una lista de caracteres de subrayado separados por nombres de propiedad de clave externa.

Recursos adicionales