Reputation: 15345
We are developing a system using C# 4 and SQL Server 2008 R2, and following domain-driven design principles. No ORM is used. There are situations where we have an entity that holds a collection of another entity, such as:
public class Project
{
public IdCollection ParticipantUsers
{
get
{
//...
}
}
public void AddParticipantUser(Id idUser)
{
//...
}
public void RemoveParticipantUser(Id idUser)
{
//...
}
}
The ParticipantUsers
property of Project
returns a collection of the ids of the users that participate in it. The AddParticipantUser
and RemoveParticipantUser
methods, well, you get it.
The problem we found is as follows. When a project instance is sent to the repository to be made updated on the database, the ParticipantUsers
collection of ids may have changed from the last time it was retrieved: some ids may have been added, some may have been removed, and some may be exactly the same. We can use a brute force approach and delete all participant users for this project from the database, and then re-insert the current ones, but I would like to explore better approaches.
What better approaches can you think of, that allow me to only insert those ids that have been added, delete those that have been removed, and leave untouched those that are still there in the collection?
Upvotes: 3
Views: 473
Reputation: 3373
Have you considered serializing all the id's into a single column? This way, you're just updating one record. If you're worried about collections having changed in the meanwhile, you might want to implement optimistic concurrency.
Upvotes: 1
Reputation: 88092
Our approach (using SQL Server 2008 R2) is to utilize a table-value parameter (TVP) in combination with a merge statement.
This way the code passes the list of items as a table to a stored procedure. The proc then uses that table in a merge statement which does the insert/update/delete.
You need a User-Defined Table Type, for example:
CREATE TYPE [dbo].[Participants] AS TABLE(
[ProjectId] [int] NOT NULL,
[ParticipantId] [int] NOT NULL,
PRIMARY KEY CLUSTERED
(
[ProjectId] ASC,
[ParticipantId] ASC
)WITH (IGNORE_DUP_KEY = OFF)
)
GO
Then you'll need a C# class to hold the data. Just modify your IdCollection
to be similar to:
public class IdCollection : List<ParticipantUsers>, IEnumerable<SqlDataRecord> {
#region IEnumerable<SqlDataRecord> Members
IEnumerator<SqlDataRecord> IEnumerable<SqlDataRecord>.GetEnumerator() {
SqlDataRecord rec = new SqlDataRecord(
new SqlMetaData("ProjectId", System.Data.SqlDbType.Int),
new SqlMetaData("ParticipantId", System.Data.SqlDbType.Int)
);
foreach (ParticipantUser ans in this) {
rec.SetInt32(0, ans.ProjectId);
rec.SetInt32(1, ans.ParticipantId);
yield return rec;
}
}
#endregion
}
Your stored procedure will look something like this:
CREATE PROCEDURE [dbo].[ParticpantUpdate]
@Particpants core.Participants READONLY
AS
BEGIN
SET NOCOUNT ON;
MERGE INTO dbo.ProjectParticipants pp
USING @Particpants RG
ON (RG.ProjectId = pp.ProjectId)
AND (RG.ParticipantId = pp.ParticipantId)
WHEN NOT MATCHED BY TARGET THEN
INSERT( ProjectId, ParticipantId)
VALUES( RG.ProjectId, RG.ParticipantId)
WHEN NOT MATCHED BY SOURCE
AND target.ProjectId in (SELECT ProjectId from @Participants) THEN
DELETE;
;
END
To call the proc (using Enterprise Library):
SqlDatabase db = DatabaseFactory.CreateDatabase("myconnstr") as SqlDatabase;
using ( DbCommand dbCommand = db.GetStoredProcCommand("dbo.ParticipantUpdate") ) {
db.AddInParameter(dbCommand, "Participants", SqlDbType.Structured, participantList);
db.ExecuteNonQuery(dbCommand);
} // using dbCommand
Upvotes: 1
Reputation: 13256
I guess event sourcing would be ideal if you do much of this sort of thing but typically we, as developers, don't get to use new techniques much :)
Since I am no fan of an ORM what I have done previously is simply keep a list of the changes along the same lines as a unit of work would do. So each time a participant is added I would add it to the AddedParticipants
collection and for removed participants I would add to the RemovedParticipants
collection. I guess one could take it further to a single list with type of change for each entry. But I am sure you get the idea.
The repository would then use the tracked changes in performing the relevant data manipulation. May not be the prettiest solution but it gets the job done.
I might add that for shorter lists I do simply delete and re-insert but, as you mentioned, it may be somewhat heavy in this scenario and one size does not fit all :)
Of course the reloading the list and comparing would work just fine as Arie has suggested.
Upvotes: 2
Reputation: 7283
Actually I think the delete-reinsert approach is better than any solution based on comparison.
Firstly, it's easy to implement.
Secondly, it avoids another data fetch for comparison.
Could you explain further about why this approach is not preferred, maybe I don't catch it.
Upvotes: 2
Reputation: 5373
orgData - is the list/table/other enumerable object that contains your original data from database (IdCollection ParticipantUsers before changes in this case)
newData - is the list/table/other enumerable object that contains your data to update
var orgList = orgData.Select(a => a.Id);
var newList = newData.Select(a => a.Id);
var itemsToAdd = newData.Where(a => !orgList.Contains(a.Id));
var itemsToDel = orgData.Where(a => !newList.Contains(a.Id));
Upvotes: 0