Reputation: 24572
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");
});
}
}
Upvotes: 11
Views: 5025
Reputation: 3361
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);
Upvotes: 0
Reputation: 1511
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 :)
Upvotes: 7
Reputation: 900
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);
...
Upvotes: 3
Reputation: 16498
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();
}
}
Upvotes: 6