简体   繁体   中英

How can I use EF6 to update a many to many table

I have two classes:

public partial class ObjectiveDetail {
    public ObjectiveDetail() {
        this.SubTopics = new List<SubTopic>();
    }
    public int ObjectiveDetailId { get; set; }
    public int Number { get; set; }
    public string Text { get; set; }
    public virtual ICollection<SubTopic> SubTopics { get; set; }
}
public partial class SubTopic {
    public int SubTopicId { get; set; }
    public string Name { get; set; }
}

I have an ObjectiveDetail object from the user:

var web = {
 "objectiveDetailId":1,
 "number":1,
 "text":"datafromweb",
 "subTopics":[
              {"subTopicId":1,
               "name":"one"
              },
              {"subTopicId":3,
               "name":"three",
              }
             ]
}

And an ObjectiveDetail from the database:

var db = {
 "objectiveDetailId":1,
 "number":1,
 "text":"datafromdb",
 "subTopics":[
              {"subTopicId":1,
               "name":"one"
              },
              {"subTopicId":2,
               "name":"two",
              }
             ]
}

With Entity Framework 6 I know I can update the text in the ObjectiveDetail class using:

_uow.ObjectiveDetails.Update(web));

But how can I update the references to ObjectiveDetail and SubTopics in the many to many table that joins these two table. Here for example I would want it so that for ObjectiveDetail 1 the many-many is changed to reference subTopicId 1 and 3 instead of the values 1 and 2. Note that ObjectiveDetail and SubTopic are stored in tables with another table between them. Here's the DDL:

CREATE TABLE [dbo].[ObjectiveDetail] (
    [ObjectiveDetailId] INT            IDENTITY (1, 1) NOT NULL,
    [Text]              NVARCHAR (MAX) NOT NULL,
    [ObjectiveTopicId]  INT            NULL,
    CONSTRAINT [PK_ObjectiveDetail] PRIMARY KEY CLUSTERED ([ObjectiveDetailId] ASC),
);

CREATE TABLE [dbo].[ObjectiveTopic] (
    [ObjectiveDetailId] INT NOT NULL,
    [SubTopicId]        INT NOT NULL,
    CONSTRAINT [FK_ObjectiveTopicObjectiveDetail] FOREIGN KEY ([ObjectiveDetailId]) REFERENCES [dbo].[ObjectiveDetail] ([ObjectiveDetailId]),
    CONSTRAINT [FK_ObjectiveTopicSubTopic] FOREIGN KEY ([SubTopicId]) REFERENCES [dbo].[SubTopic] ([SubTopicId])
);

CREATE TABLE [dbo].[SubTopic] (
    [SubTopicId] INT             IDENTITY (1, 1) NOT NULL,
    [Name]       NVARCHAR (150)  NOT NULL,
    CONSTRAINT [PK_SubTopic] PRIMARY KEY CLUSTERED ([SubTopicId] ASC),
);

Here's the EF Mapping that I have:

public class ObjectiveDetailMap : EntityTypeConfiguration<ObjectiveDetail>
{
    public ObjectiveDetailMap()
    {
        // Primary Key
        this.HasKey(t => t.ObjectiveDetailId);
        // Relationships
        this.HasMany(t => t.SubTopics)
           .WithMany(t => t.ObjectiveDetails)
           .Map(m =>
           {
               m.ToTable("ObjectiveTopic");
               m.MapLeftKey("ObjectiveDetailId");
               m.MapRightKey("SubTopicId");
           });

    }
}

I think you are trying to simulating offline mode working for your users. So when you get something from your users, you want to sync database with user data. I make an example and take your question one step beyond :) I added a Subtopic which needs to be updated in database. Ok here is the code:

static void Main(string[] args)
{
    //the database
    var ObjectiveDetails = new List<ObjectiveDetail>()
    {
        new ObjectiveDetail()
        {
            ObjectiveDetailId = 1,
            Number = 1,
            Text = "datafromdb",
            SubTopics = new List<SubTopic>()
            {
                new SubTopic(){ SubTopicId = 1, Name="one"}, //no change
                new SubTopic(){ SubTopicId = 2, Name="two"}, //to be deleted
                new SubTopic(){ SubTopicId = 4, Name="four"} //to be updated
            }
        }
    };

    //the object comes as json and serialized to defined object.
    var web = new ObjectiveDetail()
    {
        ObjectiveDetailId = 1,
        Number = 1,
        Text = "datafromweb",
        SubTopics = new List<SubTopic>()
        {
            new SubTopic(){ SubTopicId = 1, Name="one"}, //no change
            new SubTopic(){ SubTopicId = 3, Name="three"}, //new row
            new SubTopic(){ SubTopicId = 4, Name="new four"} //must be updated
        }
    };

    var objDet = ObjectiveDetails.FirstOrDefault(x => x.ObjectiveDetailId == web.ObjectiveDetailId);
    if (objDet != null)
    {
        //you can use AutoMapper or ValueInjecter for mapping and binding same objects
        //but it is out of scope of this question
        //update ObjectDetail
        objDet.Number = web.Number;
        objDet.Text = web.Text;
        var subtops = objDet.SubTopics.ToList();

        //Delete removed parameters from database
        //Entity framework can handle it for you via change tracking
        //subtopicId = 2 has been deleted 
        subtops.RemoveAll(x => !web.SubTopics.Select(y => y.SubTopicId).Contains(x.SubTopicId));

        //adds new items which comes from web
        //adds subtopicId = 3 to the list
        var newItems = web.SubTopics.Where(x => !subtops.Select(y => y.SubTopicId).Contains(x.SubTopicId)).ToList();
        subtops.AddRange(newItems);

        //this items must be updated
        var updatedItems = web.SubTopics.Except(newItems).ToList();

        foreach (var item in updatedItems)
        {
            var dbItem = subtops.First(x => x.SubTopicId == item.SubTopicId);
            dbItem.Name = item.Name;
        }

        //let's see is it working
        Console.WriteLine("{0}:\t{1}\t{2}\n---------",objDet.ObjectiveDetailId, objDet.Number, objDet.Text);
        foreach (var item in subtops)
        {
            Console.WriteLine("{0}: {1}", item.SubTopicId, item.Name);
        }
    }
    else
    {
         //insert new ObjectiveDetail
    }

    //In real scenario after doing everything you need to call SaveChanges or it's equal in your Unit of Work.
}

The result:

1:      1       datafromweb
---------
1: one
4: new four
3: three

That's it. You can sync your database and user data like this. And also AutoMapper and ValueInjecter both are very useful and powerful tools, I deeply recommend you to take a look at those. I hope you enjoyed, happy coding :)

Here's a method that takes the target ObjectiveDetail 's ID and an IEnumerable<int> of SubTopic IDs that you want to add to the target ObjectiveDetail .

public void UpdateSubTopics( int objectiveDetailId, IEnumerable<int> newSubTopicIds )
{
    using( var db = new YourDbContext() )
    {
        // load SubTopics to add from DB
        var subTopicsToAdd = db.SubTopics
            .Where( st => newSubTopicIds.Contains( st.SubTopicId ) );

        // load target ObjectiveDetail from DB
        var targetObjDetail = db.ObjectiveDetail.Find( objectiveDetailId );

        // should check for targetObjDetail == null here

        // remove currently referenced SubTopics not found in subTopicsToAdd 
        foreach( var cst in targetObjDetail.SubTopics.Except( subTopicsToAdd ) )
        {
            cst.SubTopics.Remove( cst );
        }

        // add subTopicsToAdd not currently found in referenced SubTopics
        foreach( var nst in subTopicsToAdd.Except( targetObjDetail.SubTopics ) )
        {
            targetObjDetail.SubTopics.Add( nst );
        }

        // save changes
        db.SaveChanges();
    }
}

I've only used EF with Code first, and to define the 3 tables you either define all 3 tables or you just define the 2 tables with a collection in each like this

public class ObjectiveDetail 
{
    public ObjectiveDetail() {
        this.SubTopics = new HashSet<SubTopic>();
    }
    public int ObjectiveDetailId { get; set; }
    public int Number { get; set; }
    public string Text { get; set; }
    public virtual ICollection<SubTopic> SubTopics { get; set; }
}

public partial class SubTopic 
{
    public SubTopic() {
        this.ObjectiveDetail = new HashSet<ObjectiveDetail>();
    }
    public int SubTopicId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<ObjectiveDetail> ObjectiveDetails { get; set; }
}

If you have the 3 tables, it's easy to just update the middle table with new ids. You have to get all the ObjectiveTopics that you wish to update, and change the ids, and then do the update

ObjectiveTopic objectiveTopic = _uow.ObjectiveTopic.Get(1);
ObjectiveTopic.SubTopicId = 2;
ObjectiveTopic.ObjectiveDetailId = 1;
_uow.ObjectiveTopic.Update(objectiveTopic);

If you don't have the third table defined as an entity, and you only have access to ObjectiveDetail and SubTopic tables, then you can get a hold of both entities and remove the one you don't want anymore, and add the one that you want.

ObjectiveDetail objectiveD = _uow.ObjectiveDetail.Get(1);
SubTopic subTopic = _uow.SubTopic.Get(1); //SubTopic to remove
SubTopic topicToAdd = _uow.SubTopic.Get(2); //SubTopic to add

ObjectiveDetail.SubTopics.Remove(subTopic); //Remove the entity from the ObjectiveTopic table
ObjectiveDetail.SubTopics.Add(topicToAdd); //Add the new entity, will create a new row in ObjectiveTopic Table
_uow.ObjectiveDetail.Update(objectiveD);

If you want to (and probably should), you can use linq on the objectiveD to get the entity from the collection instead of retrieving it from the database.

SubTopic subTopic = objectiveD.SubTopics.Single(x => x.SubTopicId == 1); //Instead of _uow.SubTopic.Get(1);
...

You could use a generic method, so that it can be used for any many to many relationship. You would just give this list of integers representing the IDs of the field you want to update in the many-to-many collection off your main object:

protected void UpdateManyToMany<T>(YourDBContext db, ICollection<T> collection, List<int> idList) where T : class
{
  //update a many to many collection given a list of key IDs
  collection.Clear();      
  var source = db.Set<T>();

  if (idList != null)
  {
    foreach (int i in idList)
    {
      var record = source.Find(i);
      collection.Add(record);
    }
  }
}

You would call it like this:

UpdateManyToMany(db, objectiveDetail.SubTopics, subTopicIDList);

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