使用MVC2使用外键更新Entity Framework v4对象失败

时间:2010-05-19 21:02:37

标签: entity-framework asp.net-mvc-2 entity-framework-4

使用以下简单的关系数据库结构:Order有一个或多个OrderItems,每个OrderItem都有一个OrderItemStatus。

Order, OrderItem and OrderItemStatus tables linked with foreign key relationships http://www.mindthe.net/images/OrdersDB.jpg 实体框架v4用于与数据库通信,并且已从此模式生成实体。实例连接恰好在示例中称为EnumTestEntities。

Order Repository类的精简版本如下所示:

public class OrderRepository
{
  private EnumTestEntities entities = new EnumTestEntities();

  // Query Methods
  public Order Get(int id)
  {
    return entities.Orders.SingleOrDefault(d => d.OrderID == id);
  }

  // Persistence
  public void Save()
  {
     entities.SaveChanges();
  }
}

MVC2应用程序使用Entity Framework模型来驱动视图。我正在使用MVC2的EditorFor功能来驱动编辑视图。

当回复对模型的任何更改时,将调用以下代码:

[HttpPost]
public ActionResult Edit(int id, FormCollection formValues)
{
  // Get the current Order out of the database by ID
  Order order = orderRepository.Get(id);
  var orderItems = order.OrderItems;

  try
  {
    // Update the Order from the values posted from the View
    UpdateModel(order, "");
    // Without the ValueProvider suffix it does not attempt to update the order items
    UpdateModel(order.OrderItems, "OrderItems.OrderItems");

    // All the Save() does is call SaveChanges() on the database context
    orderRepository.Save();

    return RedirectToAction("Details", new { id = order.OrderID });
  }
  catch (Exception e)
  {
    return View(order); // Inserted while debugging
  }
}

对UpdateModel的第二次调用具有ValueProvider后缀,该后缀与MVC2为View中的OrderItem的外键集合生成的自动生成的HTML输入名称前缀相匹配。

使用UpdateModel更新Order的OrderItems集合后,对数据库上下文调用SaveChanges()会产生以下异常:

"The operation failed: The relationship could not be changed because one or more 
of the foreign-key properties is non-nullable. When a change is made to a 
relationship, the related foreign-key property is set to a null value. If the 
foreign-key does not support null values, a new relationship must be defined, 
the foreign-key property must be assigned another non-null value, or the 
unrelated object must be deleted."

通过此代码进行调试时,我仍然可以看到EntityKeys不是null,并且看起来与它们应该是相同的值。当您不从数据库更改任何提取的订单详细信息时,仍会发生这种情况。此外,数据库的实体连接在Get和SaveChanges的行为之间不会发生变化,因此它似乎也不是上下文问题。

任何可能导致此问题的想法?我知道EF4已经完成了外键属性的工作,但任何人都可以了解如何使用EF4和MVC2来简化更新;而不是必须手动填充每个属性。我曾希望EditorFor和DisplayFor的简单性也会扩展到Controllers更新数据。

谢谢

3 个答案:

答案 0 :(得分:2)

我怀疑它正在绊倒OrderItemStatus。 formValues无法包含必要的OrderItem对象,因此对UpdateModel的调用无法正确更新它。也许它将它设置为null并在进程中孤立OrderItemStatus对象。

在调用保存更改之前,转储ObjectStateEntries的内容并使用以下代码查看它尝试保存回数据库的内容:

var items = context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added | System.Data.EntityState.Modified);
foreach (var item in items)
{
  ...

您还可以下载ASP.NET MVC2代码,将其链接并逐步执行UpdateModel以查看它正在执行的操作。

答案 1 :(得分:1)

您不应该直接使用表单更新您的实体,我认为这也意味着您将订单实体发送到GET中的视图?

将属性映射到Dto / Resource / ViewModel /<在此处插入其他重载,滥用的术语>。

这样,UpdateModel的神奇之处就是设置它不应该设置的东西(包括阻止黑客设置你不想要的属性)

我怀疑这会解决您的问题,因为UpdateModel可能会让您的实体失望。

首先从最佳实践开始,看看你得到了什么;)

答案 2 :(得分:1)

我写了一个非常简单的automapper-esque工具,它将实体的属性复制到模型,反之亦然。它看起来很长,但是这些线条都是评论。如果您使用它,您可以指定(实际上,您必须指定)允许将哪些属性从模型复制到实体,以防止格式错误的条目攻击。您可以在此处查看更多内容:http://kendoll.net/entity_framework_to_mvc2_model_conversion

/// <summary>
/// This is a class with a couple of static methods that make it easy to copy
/// an MVC2 model's property values to an Entity-Framework entity.
/// </summary>
/// <typeparam name="T">The type of the model declared in your MVC2 project.</typeparam>
/// <typeparam name="t">The type of the entity declared in your Entity Framework project.</typeparam>
public class EntityModeler<T, t>
    where T : EntityModel<t>
    where t : System.Data.Objects.DataClasses.EntityObject
{
    /// <summary>
    /// A new model of type T is created whose properties will have the same
    /// value as the entity parameter respective of the property name.
    /// </summary>
    /// <param name="entity">
    /// The entity whose property values should be copied to the model.
    /// </param>
    /// <returns>The new model object of type T.</returns>
    public static T ModelEntity(t entity)
    {
        // create an object of type model and populate its properties
        // with the value of the entity's properties. Then return the
        // model.
        var model = System.Activator.CreateInstance<T>();
        model.UpdateModel(entity);
        return model;
    }

    /// <summary>
    /// A new IEnumerable set of models is created whose properties have the
    /// same value as the entities' properties in the parameter.
    /// </summary>
    /// <param name="entities">
    /// The entities whose properties should be copied to the models.
    /// </param>
    /// <returns>An IEnumerable set of models of type T.</returns>
    public static IEnumerable<T> ModelEntities(IEnumerable<t> entities)
    {
        // Loop through all the entities in the entity list.
        // Create a model type for each entity. This model will have its
        // UpdateModel method called to copy the entity property values to
        // the model. Then just add the model to a list and return the
        // list.
        var list = new List<T>();
        foreach (var entity in entities)
            list.Add(EntityModeler<T, t>.ModelEntity(entity));
        return list;
    }
}

/// <summary>
/// This is the base class for all MVC2 models that can be converted to an entity defined
/// by a class in an entity framework. Each child class should define its entity's type
/// using the "T" type parameter. This class provides methods to update its model properties
/// based on an entity and vice versa.
/// </summary>
/// <typeparam name="T">
/// The type of the entity that corresponds to the model that inherits this base class. This
/// object must descend from the base EntityObject type.
/// </typeparam>
public abstract class EntityModel<T> 
    where T : System.Data.Objects.DataClasses.EntityObject
{
    /// <summary>
    /// This method updates the inheriting model's properties to correspond with the 
    /// properties of an entity. The entity must be of the type defined by the inheriting
    /// model. Reflection is used to get the public, readable properties from the entity
    /// and the public, writable properties from the model. Then, we just write the entity's
    /// value to the model for each property that shares the same name.
    /// </summary>
    /// <remarks>
    /// Please note that this method will only copy an entity's public properties. Fields
    /// are ignored (though they could easily be added). Also, this method will only perform
    /// a shallow copy of the entity's properties.
    /// </remarks>
    /// <param name="entity">
    /// The entity object whose properties should be copied to this model.
    /// </param>
    public void UpdateModel(T entity)
    {
        // get all the public properties of this model in an array
        var myProperties = this.GetType().GetProperties(
            BindingFlags.Instance |
            BindingFlags.Public
            );
        // now get the type of the entity
        var entityType = entity.GetType();
        // loop through the properties
        foreach (var myProp in myProperties)
        {
            // try to get the property with the same name from the entity.
            // If we can read from the entity property and write to this
            // object's property, then set this object's property according
            // to the value of the entity property. But first, check if the
            // value is null. If the value is null, then the corresponding
            // property in this object must be of type Nullable.
            var entityProp = entityType.GetProperty(myProp.Name);
            if (entityProp != null && entityProp.CanRead && myProp.CanWrite)
            {
                var val = entityProp.GetValue(entity, null);
                if (val == null)
                    if (myProp.PropertyType != typeof(Nullable))
                        continue;
                myProp.SetValue(this, entityProp.GetValue(entity, null), null);
            }
        }
    }

    /// <summary>
    /// This method updates the properties of an entity based on the values of
    /// this model's properties. Reflection is used to get the properties of the
    /// entity and the properties of this model, then the values of the model's
    /// properties are copied to those of the entity that share the same name.
    /// </summary>
    /// <remarks>
    /// Please note that this method only works on public properties. Fields are
    /// ignored (though they could easily be added). Also, this method will only
    /// perform a shallow copy of the model's properties.
    /// </remarks>
    /// <param name="entity">
    /// This is the entity object whose properties should be updated. It must be
    /// of the type defined by the inheriting class.
    /// </param>
    /// <param name="includeProperties">
    /// This is an array of string values that represent the names of the properties
    /// that the method should copy. If a property's name does not exist in this
    /// array, then its value will not be copied.
    /// </param>
    public void UpdateEntity(ref T entity, params string[] includeProperties)
    {
        // get all the public properties of this model in an array
        var propertyInfo = this.GetType().GetProperties(
            BindingFlags.Instance |
            BindingFlags.Public
            );
        // now get the type of the entity.
        var entityType = entity.GetType();
        // loop through each of the properties in the property array for the model type
        foreach (var myProp in propertyInfo)
        {
            // check if this property is in the list of properties to include
            if (includeProperties.Contains(myProp.Name))
            {
                // now try to get the property of the entity with the same name as the
                // property of this type.
                var entityProp = entityType.GetProperty(myProp.Name);
                // check that the entity property exists and that we can write to it.
                if (entityProp != null && entityProp.CanWrite && myProp.CanRead)
                {
                    // get the current value of this property in the model object.
                    // If this property can't be read, the value will be set to null.
                    var val = myProp.GetValue(this, null);
                    // if the value is null, make sure the corresponding property of the
                    // entity is nullable. If it isn't, do not try to set the value. Just
                    // go to the next value in the array.
                    // TODO: We need a better method to determine if an entity property
                    // is nullable. This method won't allow NULL to be written to nullable
                    // columns.
                    if (val == null)
                        if (entityProp.PropertyType != typeof(Nullable))
                            continue;
                    // we're here, so the value should be ok to set. Set the value of the
                    // entity object to the value of the model object.
                    entityProp.SetValue(entity, val, null);
                }
            }
        }
    }

    /// <summary>
    /// This method works similarly to the one above, only it creates a new entity
    /// instead of updating an existing one. The entity will be of the type T which
    /// was declared by the inheriting class.
    /// </summary>
    /// <param name="includeProperties">
    /// This is an array of string values that represent the names of the properties
    /// that the method should copy. If a property's name does not exist in this
    /// array, then its value will not be copied. 
    /// </param>
    /// <returns>
    /// The return value is the new entity with its properties set equal to
    /// the respective properties of this model (well, at least those that were
    /// specified in the parameter).
    /// </returns>
    public T CreateEntity(params string[] includeProperties)
    {
        var entity = System.Activator.CreateInstance<T>();
        this.UpdateEntity(ref entity, includeProperties);
        return entity as T;
    }
}

以下是关于如何使用它的简短示例:

    // Create your model with any properties you choose. You won't
    // have to worry about people hacking your forms, because you'll
    // define the properties that are allowed to be inserted/updated
    // as you convert the model to an entity.
    public class PersonModel : EntityModel<PersonEntity>
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Address { get; set; }
        public string SocialSecurity { get; set; }
    }

    // Create your MVC2 controller with an Index and Edit page.
    public class PersonController : Controller
    {
        public ActionResult Index()
        {
            // Create a list of models based on every Person entity in your
            // data set.
            var models =
                EntityModeler<PersonModel, PersonEntity>.ModelEntities(
                    YourEntities.PersonSet
                    );
            return View("Index", models);
        }

        [HttpPost]
        public ActionResult Edit(PersonModel model)
        {
            if (!ModelState.IsValid)
                return View(model);
            // Create an Person entity based on the Person model submitted
            // by a user, allowing only the FirstName, LastName, and Address
            // properties to be copied. This will protect the properties in
            // your entity that shouldn't be editable by users (for example,
            // a SocialSecurity property).
            var entity = model.CreateEntity(
                "FirstName",
                "LastName",
                "Address"
                );

            // The entity is ready to be used by your Entity Framework project,
            // so you can add your UPDATE logic and error checking here.
            // YourEntities.AddToPersonSet(entity);
            // ...
            // ...

            return RedirectToAction("Details", new { id = entity.Id });
        }
    }