简体   繁体   中英

How do I insert entities with a 1:n relationship in Azure App Service

I need a 1:n relationship using the Azure App Service. I followed this tutorial for the entities.

My client entities (same as tutorial, except I added a constructor):

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; }
}

My server entities (same as tutorial, except I added a constructor):

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; }
}

The ZooController table controller (stock scaffolded code, except I added attributes):

[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);
  }

There is a similar table controller for Animal .

Now for the problem. I perform an insert and an update from the client:

// 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();

This throws exceptions. On the server, it throws with StatusCode: 409, ReasonPhrase: 'Conflict' , and on the client it throws with "The operation failed due to a conflict: 'Violation of PRIMARY KEY constraint 'PK_dbo.Animals'. Cannot insert duplicate key in object 'dbo.Animals' . Basically, the server is saying that I'm trying to insert the same Animal record twice.

What am I doing wrong?

Please try as shown below.

// 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);

Note : Above code will automatically insert a record to the animalTable too due to FK relationship. You don't need to do it manually.

I'm pretty sure you're running into the Entity Framework "detatched entities" problem. See eg Many to Many Relationships not saving . The problem is that Entity Framework has not loaded the child item into its context, so it thinks that it needs to insert the child item as well as the parent. (There have been long-standing feature requests in Entity Framework to solve this, but the feature has never been added.)

On the server, you need to check if the referenced child entity already exists in the database, and if it does, just add a reference to the existing item, rather than inserting.

For anyone else struggling with this. Here is my solution based on @lindydonna's feedback.

[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);
}

This works. Though it's probably not performant.

You should probably also add:

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

...though for some reason for me it works without as well.

EDIT
Better way, which doesn't allow new child entities to be sent together with the parent entity. If that is allowed, then they won't have id , createdAt , etc. Those child entities should be inserted into the client-side context to populate those properties. Then, when performing a push, the SDK seems to be smart enough to POST them first before POSTing the parent.

[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);
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM