【问题标题】:How do I insert entities with a 1:n relationship in Azure App Service如何在 Azure 应用服务中插入具有 1:n 关系的实体
【发布时间】:2016-08-24 13:30:15
【问题描述】:

我需要使用 Azure 应用服务的 1:n 关系。 I followed this tutorial 用于实体。

我的客户端实体(与教程相同,除了我添加了一个构造函数):

public abstract class Entity {
  // defines id, version, createdAt, updatedAt, deleted
}
public class Animal : Entity {
  public string Name { get; set; }
}
public class Zoo : Entity {
  public Zoo() { Animals = new List<Animal>(); }
  public string Name { get; set; }
  public List<Animal> Animals { get; set; }
}

我的服务器实体(与教程相同,除了我添加了一个构造函数):

public class Animal : EntityData {
  public string Name { get; set; }
}
public class Zoo : EntityData {
  public Zoo() { Animals = new List<Animal>(); }
  public string Name { get; set; }
  public virtual ICollection<Animal> Animals { get; set; }
}

ZooController 表控制器(库存脚手架代码,除了我添加了属性):

[HttpPatch]
public Task<Zoo> PatchZoo(string id, Delta<Zoo> patch) {
  return UpdateAsync(id, patch);
 }
[HttpPost]
public async Task<IHttpActionResult> PostZoo(Zoo item) {
  Zoo current = await InsertAsync(item);
  return CreatedAtRoute("Tables", new { id = current.Id }, current);
  }

Animal 有一个类似的表控制器。

现在解决问题。我从客户端执行插入和更新:

// create animal (child)
var animal = new Animal() { Name = "Dumbo" };
await animalTable.InsertAsync(animal);
// create zoo (parent)
var zoo = new Zoo() { Name = "Tokyo National Zoo" };
await zooTable.InsertAsync(zoo);
// add animal to zoo
zoo.Animals.Add(animal);
await zooTable.UpdateAsync(zoo);
// push
await zooTable.MobileServiceClient.SyncContext.PushAsync();

这会引发异常。在服务器上,它以StatusCode: 409, ReasonPhrase: 'Conflict' 抛出,在客户端它以"The operation failed due to a conflict: 'Violation of PRIMARY KEY constraint 'PK_dbo.Animals'. Cannot insert duplicate key in object 'dbo.Animals' 抛出。基本上,服务器说我试图插入相同的Animal 记录两次。

我做错了什么?

【问题讨论】:

  • 我做了一些调试,注意到Animal控制器的POST方法被调用,然后Zoo控制器的POST方法被调用。所以第二个 POST 操作是发生双插入的地方。但是我该如何阻止呢?
  • 你试过省略animalTable.InsertAsync()吗?只是我的一个想法。

标签: c# entity-framework azure azure-mobile-services


【解决方案1】:

我很确定您遇到了实体框架“分离实体”的问题。参见例如Many to Many Relationships not saving。问题是实体框架没有将子项加载到其上下文中,因此它认为它需要插入子项以及父项。 (Entity Framework 中长期以来一直有功能请求来解决这个问题,但从未添加过该功能。)

在服务器上,你需要检查被引用的子实体是否已经存在于数据库中,如果存在,只需添加对现有项的引用,而不是插入。

【讨论】:

  • 啊哈!是的,你是对的。那个教程让我觉得表控制器中发生了一些神奇的事情,但实际上它是普通的旧 EF。 SDK 的魔力似乎在客户端而不是服务器上效果最佳! :-)
【解决方案2】:

请尝试如下所示。

// create animal (child)
var animal = new Animal() { Name = "Dumbo" };

// create zoo (parent)
var zoo = new Zoo() { Name = "Tokyo National Zoo" };
zoo.Animals.Add(animal);
await zooTable.InsertAsync(zoo);

注意:由于FK的关系,上面的代码也会自动插入一条记录到animalTable。您无需手动操作。

【讨论】:

  • 是的,如果我使用常规 EF,我会这样做,但在这里我相信实体必须插入到客户端的上下文中。如果不是,那么idcreatedAt等属性都为null。我可以在 POST 方法中在服务器上设置这些值,但是客户端不会知道这些值...
  • 您必须为客户端方案找出解决方法。但是你不能做你对服务器所做的事情。这就是它显示错误的原因。如果你有 FK 关系,那么你必须在我上面提到的地方做。没有其他方法。这就是 EF 的工作方式。
  • 好的,但是如果你在不同的时间添加实体呢?这样,它们必须作为原子操作来完成。如果您想稍后添加它,就像在我的示例中一样,它会失败...
  • 这是错误的地方。// add animal to zoo zoo.Animals.Add(animal); await zooTable.UpdateAsync(zoo);
  • 你在做同样的事情两次。
【解决方案3】:

对于其他为此苦苦挣扎的人。这是我根据@lindydonna 的反馈提出的解决方案。

[HttpPost]
public async Task<IHttpActionResult> PostZoo(Zoo item) {

  // replace child entities with those from the context (so they are tracked)
  if (item.Animals.Any()) {
    // find animals which are already in the db
    var ids = item.Animals.Select(x => x.Id);
    var oldItems = context.Animals.Where(x => ids.Contains(x.Id)).ToList();    
    var newItems = item.Animals.Where(x => !oldItems.Select(y => y.Id).Contains(x.Id));

    // add new animals if there are any
    if (newItems.Any()) {
      foreach (var i in newItems) {
        context.Animals.Add(i);
      }
      await context.SaveChangesAsync();
    }

    // replace
    item.Animals.Clear();
    foreach (var i in oldItems) item.Animals.Add(i);
    foreach (var i in newItems) item.Animals.Add(i);
  }

  // now add the Zoo (which contains tracked Animal entities)
  Zoo current = await InsertAsync(item);
  return CreatedAtRoute("Tables", new { id = current.Id }, current);
}

这行得通。虽然它可能性能不佳。

您可能还应该添加:

modelBuilder.Entity<Zoo>().HasMany(e => e.Animals);

...尽管出于某种原因对我来说它也可以正常工作。

编辑
更好的方法,它不允许新的子实体与父实体一起发送。如果允许,那么它们将不会有idcreatedAt 等。应该将这些子实体插入到客户端上下文中以填充这些属性。然后,在执行推送时,SDK 似乎足够聪明,可以在 POST 父之前先发布它们。

[HttpPost]
public async Task<IHttpActionResult> PostZoo(Zoo item) {

  // replace child entities with those from the context (so they are tracked)
  if ((item.Animals != null) && item.Animals.Any()) {
    // find animals which are already in the db
    var ids = item.Animals.Select(x => x.Id);
    var oldAnimals = _context.Animals.Where(x => ids.Contains(x.Id)).ToList();
    var newAnimals = item.Animals.Where(x => !oldAnimals.Select(y => y.Id).Contains(x.Id));

    // don't allow new animal entities
    if (tagsNew.Any()) {
      var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
      var body = new JObject(new JProperty("error", "New Animals must be added first"));
      response.Content = new StringContent(body.ToString(), Encoding.UTF8, "application/json");
      throw new HttpResponseException(response);
      }

    // replace 
    item.Animals.Clear();
    foreach (var i in oldAnimals) item.Animals.Add(i);
    }

  // now add the Zoo (which contains tracked Animal entities)
  Zoo current = await InsertAsync(item);
  return CreatedAtRoute("Tables", new { id = current.Id }, current);
  }

【讨论】:

  • 这可能可以使用上下文的Attach() 方法清除。
猜你喜欢
  • 2013-11-20
  • 2019-07-24
  • 1970-01-01
  • 2020-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-28
  • 2020-03-21
相关资源
最近更新 更多