Как я могу использовать EF6 для обновления таблицы многих?

У меня есть два класса:

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

У меня есть объект ObjectiveDetail от пользователя:

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

И ObjectiveDetail из базы данных:

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

С Entity Framework 6 Я знаю, что могу обновить текст в классе ObjectiveDetail, используя:

_uow.ObjectiveDetails.Update(web));

Но как я могу обновить ссылки на ObjectiveDetail и SubTopics во многих таблицах, которые объединяют эти две таблицы. Здесь, к примеру, я бы хотел, чтобы для ObjectiveDetail 1 многие-многие были изменены на ссылки subTopicId 1 и 3 вместо значений 1 и 2. Обратите внимание, что ObjectiveDetail и SubTopic хранятся в таблицах с другой таблицей между ними. Здесь 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),
);

Здесь EF-сопоставление, которое у меня есть:

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

    }
}

Ответ 1

Я думаю, вы пытаетесь имитировать автономный режим, работающий для ваших пользователей. Поэтому, когда вы получаете что-то от своих пользователей, вы хотите синхронизировать базу данных с пользовательскими данными. Я делаю пример и делаю ваш вопрос на один шаг дальше:) Я добавил Subtopic, который необходимо обновить в базе данных. Итак, вот код:

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 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 equal in your Unit of Work.
}

Результат:

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

Что это. Вы можете синхронизировать данные своей базы данных и пользователя так. А также AutoMapper и ValueInjecter оба очень полезные и мощные инструменты, я настоятельно рекомендую вам взглянуть на них. Надеюсь, вам понравилось, счастливое кодирование:)

Ответ 2

Здесь используется метод, который принимает целевой идентификатор ObjectiveDetail и IEnumerable<int> идентификаторов SubTopic, которые вы хотите добавить в целевой 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();
    }
}

Ответ 3

Я сначала использовал EF с кодом, и чтобы определить 3 таблицы, вы либо определяете все 3 таблицы, либо просто определяете 2 таблицы с коллекцией в каждом, подобном этому

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

Если у вас есть 3 таблицы, легко просто обновить среднюю таблицу новыми идентификаторами. Вы должны получить все ObjectiveTopics, которые хотите обновить, и изменить идентификаторы, а затем выполнить обновление

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

Если у вас нет третьей таблицы, определенной как сущность, и у вас есть только доступ к таблицам ObjectiveDetail и SubTopic, тогда вы можете получить оба объекта и удалить тот, который вам больше не нужен, и добавить тот, который вы хотите.

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

Если вы хотите (и, вероятно, должны), вы можете использовать linq на объекте D, чтобы получить сущность из коллекции, а не извлекать ее из базы данных.

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

Ответ 4

Вы можете использовать общий метод, чтобы его можно было использовать для отношений многих или многих. Вы просто указали бы этот список целых чисел, представляющих идентификаторы поля, которое вы хотите обновить в коллекции "многие ко многим", от вашего основного объекта:

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

Вы бы назвали это следующим образом:

UpdateManyToMany(db, objectiveDetail.SubTopics, subTopicIDList);