Este artículo proviene de un motor de traducción automática.

Los puntos de datos

Cómo apañárselas sin claves externas

Julie Lerman

Descargar el código de ejemplo

Julie LermanEste mes estoy escribiendo acerca de un tema que he encontrado yo ayudando a la gente con frecuencia últimamente: problemas relacionados conexos clases definidas en el primer código y luego, en la mayoría de los casos, en el marco del modelo vista controlador (MVC). Los problemas que han experimentado los desarrolladores no son específicos al código primero. Son el resultado de la conducta de Entity Framework (EF) subyacente y, de hecho, común a la mayoría mapeados relacionales de objetos (ORM). Pero parece que el problema es salir a la superficie debido a que los desarrolladores están llegando al primer código con ciertas expectativas. MVC les está causando dolor debido a su naturaleza altamente desconectado.

En lugar de sólo mostrando el código correcto, utilizará esta columna para ayudarle a comprender el comportamiento EF, por lo que se puede aplicar este conocimiento a las muchas situaciones que pueden surgir cuando diseñar sus clases o escribir los datos de acceso código con EF APIs.

La primera Convención de código es capaz de detectar y deducir correctamente diversas relaciones con diversas combinaciones de propiedades en sus clases. En este ejemplo, que estoy escribiendo como las hojas están recurriendo colores espectaculares en los árboles cerca de mi casa en Vermont, usaré árbol y hoja como mis clases relacionadas. Una relación uno a varios, la forma más sencilla podría describir en sus clases y código primero reconocer su intención es tener una propiedad de navegación en la clase de árbol que representa algún tipo de colección de tipos de hoja. La clase hoja no necesita propiedades apuntando al árbol. Figura 1 muestra las clases de árboles y hojas.

Figura 1 árbol relacionado y clases de hoja

public class Tree
{
  public Tree()
  {
    Leaves = new List<Leaf>();
  }
  public int TreeId { get; set; }
  public string Type { get; set; }
  public double Lat { get; set; }
  public double Long { get; set; }
  public string Notes { get; set; }
  public ICollection<Leaf> Leaves { get; set; }
}
public class Leaf
{
  public int LeafId { get; set; }
  public DateTime FellFromTreeDate { get; set; }
  public string FellFromTreeColor { get; set; }
}

Por Convención, primer código sabrá que es necesaria una clave externa en la base de datos en la tabla de la hoja. Presumir de un nombre de campo de clave externa para ser "Tree_TreeId", y con esta información proporcionada en los metadatos creados por el primer código en tiempo de ejecución, EF comprenderá cómo funcionan las consultas y actualizaciones a través de la clave externa. EF aprovecha este comportamiento apoyándose en el mismo proceso utiliza con "asociaciones independientes" — el único tipo de asociación podríamos utilizar antes para Microsoft.NET Framework 4 — que no requieren una propiedad clave externa en la clase dependiente.

Se trata de una forma agradable y limpia para definir las clases cuando esté seguro que no tienes necesidad de navegar nunca de una hoja a su árbol en su aplicación. Sin embargo, sin acceso directo a la clave externa, deberás ser más diligentes cuando la codificación.

Creación de nuevos tipos de dependientes sin clave externa o propiedades de navegación

Aunque fácilmente puede utilizar estas clases para mostrar un árbol y sus hojas en las aplicaciones Web ASP.NET MVC aplicación y edición de hojas, los desarrolladores a menudo problemas creando nuevas hojas en una aplicación MVC típicamente diseñada. He usado la plantilla del paquete MVCScaffolding NuGet (mvcscaffolding.codeplex.com) para permitir que Visual Studio auto­matically construir mis controladores, vistas y repositorios sencillos seleccionando "MvcScaffolding: Controladora con acción de lectura/escritura y vistas, el uso de repositorios". Tenga en cuenta que ya no hay ninguna propiedad de clave externa en la clase de hoja, las plantillas de andamios creerá que la relación uno a varios. Hice algunos cambios menores a las vistas y controladores para permitir a un usuario para desplazarse de un árbol a sus hojas, que se pueden ver en la descarga de muestra.

La acción de devolución de crear para la hoja toma la hoja volvió desde la vista de crear y le dice al repositorio para agregar y, a continuación, guardarlo, como se muestra en figura 2.

Figura 2 Agregar y guardar la hoja en el repositorio

[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    leafRepository.InsertOrUpdate(leaf);
    leafRepository.Save();
    return RedirectToAction("Index", 
      new { treeId = Request.Form["TreeId"] });
  }
  else
  {
    return View();
  }
}

El repositorio toma la hoja, comprueba si es nueva y si es así, agrega a la instancia de contexto que fue creada como resultado de la devolución de datos:

public void InsertOrUpdate(Leaf leaf,int treeId){
  if (leaf.LeafId == default(int)) {
    // New entity
    context.Leaves.Add(leaf);
  } else {
    // Existing entity
    context.Entry(leaf).State = EntityState.Modified;
  }
}

Cuando se llama a guardar, EF crea un comando Insert, que agrega la hoja de nueva a la base de datos:

    exec sp_executesql N'insert [dbo].[Leaves]([FellFromTreeDate], [FellFromTreeColor],
    [Tree_TreeId]) values (@0, @1, null)
    select [LeafId]
    from [dbo].[Leaves]
    where @@ROWCOUNT > 0 and [LeafId] = scope_identity()',
    N'@0 datetime2(7),@1 nvarchar(max) ',
    @0='2011-10-11 00:00:00',@1=N'Pale Yellow'

Observe los valores pasados a en la segunda línea del comando: @ 0 (la fecha); 1 (para el color modificado); y null. El valor null es destinado para el campo de Tree_TreeId. Recuerde que la clase de hoja limpia, agradable no tiene ninguna propiedad clave extranjera para representar el TreeId, por lo que no hay ninguna manera de pasar ese valor cuando se crea una hoja independiente.

Cuando el tipo dependiente (en este caso, hoja) no tiene conocimiento de su tipo principal (árbol), hay sólo una manera de hacer una inserción: La instancia de la hoja y la instancia de árbol deben agregarse al contexto juntos como parte de la misma gráfica. Esto proporcionará EF con toda la información necesaria para averiguar el valor correcto para insertar en la clave externa de la base de datos (por ejemplo, Tree_TreeId). Pero en este caso, donde se trabaja sólo con la hoja, no hay información en la memoria para EF determinar el valor de la propiedad clave del árbol.

Si tuvieras una propiedad clave externa en la clase de hoja, la vida sería mucho más simple. No es demasiado difícil mantener un único valor a la mano cuando se mueven entre los controladores y vistas. De hecho, si nos fijamos en la acción de crear en figura 2, puede ver que el método tiene acceso al valor de la TreeId para que la hoja se está creando.

Hay varias formas de pasar datos en aplicaciones de MVC. Elegí la más simple para esta demostración: el TreeId de relleno en el ViewBag de MVC y aprovechar Html.Hidden campos cuando sea necesario. Esto hace que el valor disponible como uno de los elementos de la vista Request.Form.

Porque tengo acceso a la TreeId, soy capaz de construir el gráfico de árbol y hoja que proporcionará el TreeId del comando Insertar. Una rápida modificación en la clase de repositorio permite el método InsertOrUpdate aceptar esa variable TreeId desde la vista y recupera la instancia de árbol de la base de datos mediante el método DbSet.Find. Aquí está la parte afectada del método:

public void InsertOrUpdate(Leaf leaf,int treeId)
{
  if (leaf.LeafId == default(int)) {
    var tree=context.Trees.Find(treeId);
    tree.Leaves.Add(leaf);
  }
...

El contexto es ahora de seguimiento del árbol y es consciente de que estoy añadiendo la hoja en el árbol. Esta vez, al contexto.Se llama SaveChanges, EF es capaz de desplazarse desde la hoja al árbol para descubrir el valor de la clave y utilizarla en el comando Insertar.

Figura 3 muestra el código de controlador modificado con la nueva versión de InsertOrUpdate.

Figura 3 la nueva versión de InsertOrUpdate

[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    leafRepository.InsertOrUpdate(leaf);
    leafRepository.Save();
    return RedirectToAction("Index",
      new { treeId = Request.Form["TreeId"] });
  }
  else
  {
    return View();
  }
}
[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    var _treeId = Request.Form["TreeId"] as int;
    leafRepository.InsertOrUpdate(leaf, _treeId);
    leafRepository.Save();
    return RedirectToAction("Index", new { treeId = _treeId });
  }
  else
  {
    return View();
  }
}

Con estos cambios, el método insert finalmente tiene el valor de la clave externa, que se puede ver en el parámetro llamado "2":

exec sp_executesql N'insert [dbo].[Leaves]([FellFromTreeDate], [FellFromTreeColor], [Tree_TreeId])
values (@0, @1, @2)
select [LeafId]
from [dbo].[Leaves]
where @@ROWCOUNT > 0 and [LeafId] = scope_identity()',
N'@0 datetime2(7),@1 nvarchar(max) ,
@2 int',@0='2011-10-12 00:00:00',@1=N'Orange-Red',@2=1

Al final, esta solución me obliga a hacer otro viaje a la base de datos. Este es el precio que elegir pagar en este escenario donde no quiero la propiedad clave extranjera en mi clase dependiente.

Problemas con las actualizaciones cuando no hay ninguna clave externa

Hay otras maneras puede pintar usted mismo en una esquina cuando eres dependiente y decidida a no tener propiedades claves extranjeras en sus clases. Aquí hay otro ejemplo.

Agregaré una nueva clase de dominio llamada TreePhoto. Porque no desea desplazarse desde esta clase al árbol, no hay ninguna propiedad de navegación, y una vez más, estoy siguiendo el patrón donde no utilizo una propiedad clave extranjera:

[Table("TreePhotos")]
public class TreePhoto
{
  public int Id { get; set; }
  public Byte[] Photo { get; set; }
  public string Caption { get; set; }
}

La clase de árbol proporciona la única conexión entre las dos clases, y especificar que cada árbol debe tener una foto. Aquí está la nueva propiedad que he añadido a la clase de árbol:

[Required]
  public TreePhoto Photo { get; set; }

Esto deja la posibilidad de fotos huérfanos, pero utilizo este ejemplo porque he visto varias veces — junto con súplicas de ayuda, así que quería solucionar el problema.

Una vez más, disuadir a código primera Convención­minadas que sería necesario en la base de datos y creada uno, Photo_Id, en mi nombre una propiedad clave extranjera. Observe que es que no aceptan valores NULL. Eso es porque la propiedad Leaf.Photo es necesaria (véase figura 4).

Using Code First Convention, Tree Gets a Non-Nullable Foreign Key to TreePhotos
Figura 4 utilizando código primera Convención, árbol obtiene un no NULL clave externa a TreePhotos

Su aplicación podría dejar crear árboles antes de que se han tomado las fotos, pero el árbol todavía necesita esa propiedad de foto a poblarse. Agregaré la lógica en Insert del repositorio árbol­método OrUpdate para crear un valor predeterminado, foto vacía para nuevos árboles cuando uno no está suministrado:

public void InsertOrUpdate(Tree tree)
{
  if (tree.TreeId == default(int)) {
    if (tree.Photo == null)
    {
      tree.Photo = new TreePhoto { Photo = new Byte[] { 0 },
                                   Caption = "No Photo Yet" };
    }
    context.Trees.Add(tree);
}
...

El problema más grande que quiero centrarme aquí es cómo este problema afecta a las actualizaciones. Supongamos que tiene un árbol y su foto requiere ya almacenados en la base de datos. Desea poder editar un árbol y no necesitan interactuar con la foto. Podrá recuperar el árbol, quizás con código como "contexto.Tre­­es.Find(someId)." Cuando es el momento de guardar, obtendrá un error de validación porque árbol requiere una foto. Pero el árbol tiene una foto! Es la base de datos! ¿Qué pasa?

Aquí está el problema: Cuando primero se ejecuta una consulta para recuperar la tabla, ignorando la foto relacionada, sólo los valores escalares del árbol serán devuelto desde la base de datos y la foto será null (véase figura 5).

A Tree Instance Retrieved from the Database Without Its Photo
Instancia de árbol de figura 5 obtenido de la base de datos sin su foto

El cuaderno de modelo MVC y EF tienen la capacidad de validar la anotación necesaria. Cuando es el momento de guardar el árbol editado, su foto aún será null. Si está dejando que MVC comprobar su ModelState.IsValid en el código de controlador, reconocerá que la foto es falta. IsValid será falsa y el controlador no molesta incluso llamando al repositorio. En mi aplicación, he eliminado la validación del modelo Binder, así que puedo dejar a mi código de repositorio ser responsable de ninguna validación del lado del servidor. Cuando el repositorio llama SaveChanges, validación de EF detectar la foto falta e inicia una excepción. Pero en el repositorio, tenemos la oportunidad de manejar el problema.

Si la clase de árbol tiene una propiedad clave exterior — por ejemplo, int PhotoId — que fue necesario (lo que permite eliminar el requisito de la propiedad de navegación de la foto), el valor de clave externa de la base de datos habría sido utilizado para rellenar la propiedad PhotoId de la instancia de árbol. El árbol sería válido, y SaveChanges sería capaz de enviar el comando de actualización para la base de datos. En otras palabras, si hay una propiedad clave extranjera, el árbol habría sido válido incluso sin la instancia de la foto.

Pero sin la clave externa, una vez más necesitará algún mecanismo para aportar la foto antes de guardar los cambios. Si tienes tu código primeras clases y contexto configurado para realizar la carga perezosa, cualquier mención de foto en el código hará EF cargar la instancia de la base de datos. Todavía estoy algo anticuado cuando se trata de vago a cargar, por lo que probablemente sería mi elección personal realizar una carga explícita de la base de datos. La nueva línea de código (la última línea en el siguiente ejemplo, donde estoy llamando carga) utiliza el método DbContext para cargar datos relacionados:

public void InsertOrUpdate(Tree tree)
{
  if (tree.TreeId == default(int)) {
  ...
} else {
    context.Entry(tree).State = EntityState.Modified;
    context.Entry(tree).Reference(t => t.Photo).Load();
  }
}

Esto hace que EF feliz. Árbol validará porque hay foto y EF enviará una actualización de la base de datos para el árbol modificado. La clave aquí es que debe asegurarse que la foto no es nula; He mostrado una manera de satisfacer esa restricción.

Un punto de comparación

Si la clase de árbol simplemente tenía una propiedad PhotoId, nada de esto sería necesario. Un efecto directo de la propiedad de int PhotoId es que ya no necesita la anotación requiere la propiedad de la foto. Como un tipo de valor, siempre debe tener un valor, satisfacer el requisito de que un árbol debe tener una foto, incluso si no está representada como una instancia. Como hay un valor en PhotoId, el requisito quedarán satisfechos, así funciona el siguiente código:

public class Tree
{
  // ...
Other properties
  public int PhotoId { get; set; }
  public TreePhoto Photo { get; set; }
}

Cuando el método de edición del controlador recupera un árbol desde la base de datos, se rellenará la propiedad escalar PhotoId. Como fuerza MVC (o cualquier marco de aplicación está utilizando) ida y vuelta que valor, cuando es el momento de actualizar el árbol, EF será despreocupado sobre la nula propiedad de foto.

Más fácil, pero no mágico

Aunque el equipo EF ha proporcionado más lógica de API para ayudar con escenarios desconectados, es su trabajo para entender cómo funciona el EF y cuáles son sus expectativas cuando usted está moviendo datos. Sí, la codificación es mucho más simple si incluyen claves externas en sus clases, pero son sus clases y eres el mejor juez de lo que debería y no debería estar en ellos. Sin embargo, si el código fue mi responsabilidad, yo seguramente obligaría a convencerme de que tus razones para excluir propiedades claves extranjeras superaban los beneficios de incluirlos. EF algunos trabajos hará para usted si existen las claves externas. Pero si están ausentes, siempre y cuando entiendes lo que EF espera y cómo satisfacer esas expectativas, usted debe ser capaz de conseguir aplicaciones desconectadas a comportarse de la forma que desee.

Julie Lerman es un MVP de Microsoft.NET mentor y consultor que vive en las colinas de Vermont. Puede encontrar su presentación sobre acceso a datos y otro Microsoft.NETOS temas en grupos de usuarios y conferencias alrededor del mundo. Blogs de ella en thedatafarm.com/blog y es el autor de "Programación Entity Framework" (2010) y "programación Entity Framework: Código primero"(2011), ambas de o ' Reilly Media. Seguirla en Twitter en twitter.com/julielerman.

Gracias a los siguientes expertos técnicos para revisar este artículo: Jeff Derstadt y Rick Strahl