views:

1048

answers:

7

I have two tables in my database connected by foreign keys: Page (PageId, other data) and PageTag (PageId, Tag). I've used LINQ to generate classes for these tables, with the page as the parent and the Tag as the child collection (one to many relationship). Is there any way to mark PageTag records for deletion from the database from within the Page class?

Quick Clearification:

I want the child objects to be deleted when the parent DataContext calls SubmitChanges(), not before. I want TagString to behave exactly like any of the other properties of the Page object.

I would like to enable code like the following:

Page page = mDataContext.Pages.Where(page => page.pageId = 1);
page.TagString = "new set of tags";

//Changes have not been written to the database at this point.

mDataContext.SubmitChanges();

//All changes should now be saved to the database.

Here is my situation in detail:
In order to make working with the collection of tags easier, I've added a property to the Page object that treats the Tag collection as a string:

public string TagString {
 get {
  StringBuilder output = new StringBuilder();
  foreach (PageTag tag in PageTags) {
   output.Append(tag.Tag + " ");
  }

  if (output.Length > 0) {
   output.Remove(output.Length - 1, 1);
  }

  return output.ToString();
 }
 set {
  string[] tags = value.Split(' ');
  PageTags.Clear();
  foreach (string tag in tags) {
   PageTag pageTag = new PageTag();
   pageTag.Tag = tag;
   PageTags.Add(pageTag);
  }
 }
}

Basically, the idea is that when a string of tags is sent to this property, the current tags of the object are deleted and a new set is generated in their place.

The problem I'm encountering is that this line:

PageTags.Clear();

Doesn't actually delete the old tags from the database when changes are submitted.

Looking around, the "proper" way to delete things seems to be to call the DeleteOnSubmit method of the data context class. But I don't appear to have access to the DataContext class from within the Page class.

Does anyone know of a way to mark the child elements for deletion from the database from within the Page class?

A: 

Do you have a relationship, in your Linq to SQL entity diagram, linking the Page and PageTags tables? If you don't, that is why you can't see the PageTags class from the Page class.

If the foreign key in the PageTags database table is set to Allow Nulls, Linq to SQL will not create the link when you drag the tables into the designer, even if you created a relationship on the SQL Server.

Robert Harvey
The association is in place, and I can access the child collection programatically. The problem is that changes made to the collection (specifically the removal of objects) do not get persisted to the database.
AaronSieb
Silly question, but are you committing changes?
Robert Harvey
Yes. When I commit the changes I get a duplicate key error (because the new tags overlap the old tags).
AaronSieb
A: 

This is one of those areas where OR mapping can get kind of hairy. Providing this TagString property makes things a bit more convenient, but in the long run it obfuscates what is really happening when someone utilizes the TagString property. By hiding the fact that your performing data modification, someone can very easily come along and set the TagString without using your Page entity within the scope of a DataContext, which could lead to some difficult to find bugs.

A better solution would be to add a Tags property on the Page class with the L2S model designer, and require that the PageTags be edited directly on the Tags property, within the scope of a DataContext. Make the TagString property read only, so it can be genreated (and still provide some convenience), but eliminate the confusion and difficulty around setting that property. This kind of change clarifies intent, and makes it obvious what is happening and what is required by consumers of the Page object to make it happen.

Since Tags is a property of your Page object, as long as it is attached to a DataContext, any changes to that collection will properly trigger deletions or insertions in the database in response to Remove or Add calls.

jrista
I am not looking to have the PageTag deleted immediately upon its removal from the collection. I'm looking to have it be removed when the parent DataContext calls SubmitChanges(). The update semantics of the PageTagString property are not intended to be different from any of the other data properties of the object.
AaronSieb
A: 

Aaron,

Apparently you have to loop thru your PageTag records, calling DeleteOnSubmit for each one. Linq to SQL should create an aggregate query to delete all of the records at once when you call SubmitChanges, so overhead should be minimal.

replace

PageTags.Clear();

with

foreach (PageTag tag in PageTags)
    myDataContext.DeleteOnSubmit(tag);
Robert Harvey
How would I access the DataContext from within my Page class?
AaronSieb
Add a DataContext member to your PageTag partial class. partial class PageTag { DataClassesDataContext myDataContext = new DataClassesDataContext(); public string TagString {..etc.
Robert Harvey
A: 

Aaron:

Add a DataContext member to your PageTag partial class.

partial class PageTag 
{ 
    DataClassesDataContext myDataContext = new DataClassesDataContext(); 

    public string TagString {

..etc.

Robert Harvey
Wouldn't this be an independent DataContext from the parent DataContext used to retrieve the PageTag? I don't think that the delete will happen when I call SubmitChanges on the *parent* DataContext.
AaronSieb
It will work. Think of the DataContext as just another connection to the database.The reason this looks strange is because, from an architectural standpoint, you probably should be manipulating your PageTag objects outside of your PageTag partial class, rather than inside of it.
Robert Harvey
I receive the exception "Cannot remove an entity that has not been attached." when I attempt to remove them from the second DataConnection. This doesn't look like it will satisfy the requirement of deferring changes until the original DataContext submits.
AaronSieb
Can I see your code?
Robert Harvey
Code sample posted as an answer to this question.
AaronSieb
Aaron, you should be able to reference the original data context through your object, like this: tag.dataContext (or whatever your original data context variable was named).
Robert Harvey
A: 

Larger code sample posted at Robert Harvey's request:

DataContext.cs file:

namespace MyProject.Library.Model
{
 using Tome.Library.Parsing;
 using System.Text;

 partial class Page
 {
  //Part of Robert Harvey's proposed solution.
  MyDataContext mDataContext = new TomeDataContext();

  public string TagString {
   get {
    StringBuilder output = new StringBuilder();
    foreach (PageTag tag in PageTags) {
     output.Append(tag.Tag + " ");
    }

    if (output.Length > 0) {
     output.Remove(output.Length - 1, 1);
    }

    return output.ToString();
   }
   set {
    string[] tags = value.Split(' ');
    //Original code, fails to mark for deletion.
    //PageTags.Clear();

    //Robert Harvey's suggestion, thorws exception "Cannot remove an entity that has not been attached."
    foreach (PageTag tag in PageTags) {
     mDataContext.PageTags.DeleteOnSubmit(tag);
    }

    foreach (string tag in tags) {
     PageTag PageTag = new PageTag();
     PageTag.Tag = tag;
     PageTags.Add(PageTag);
    }
   }
  }

  private bool mIsNew;
  public bool IsNew {
   get {
    return mIsNew;
   }
  }

  partial void OnCreated() {
   mIsNew = true;
  }

  partial void OnLoaded() {
   mIsNew = false;
  }
 }
}

Repository Methods:

public void Save() {
 mDataContext.SubmitChanges();
}

public Page GetPage(string pageName) {
 Page page =
  (from p in mDataContext.Pages
  where p.FileName == pageName
  select p).SingleOrDefault();

 return page;
}

Usage:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(string pageName, FormCollection formValues) {
 Page updatedPage = mRepository.GetPage(pageName);

 //TagString is a Form value, and is set via UpdateModel.
 UpdateModel(updatedPage, formValues.ToValueProvider());
 updatedPage.FileName = pageName;

 //At this point NO changes should have been written to the database.

 mRepository.Save();

 //All changes should NOW be saved to the database.

 return RedirectToAction("Index", "Pages", new { PageName = pageName });
}
AaronSieb
+1  A: 

Sorry, my bad. That won't work.

It really looks like you need to be doing this in your repository, rather than in your Page class. There, you have access to your original data context.

There is a way to "attach" the original data context, but by the time you do that, it has become quite the code smell.

Robert Harvey
Thank you for your input. It's looking like LINQ isn't capable of this kind of structure :/
AaronSieb
+3  A: 

After some more research, I believe I've managed to find a solution. Marking an object for deletion when it's removed from a collection is controlled by the DeleteOnNull parameter of the Association attribute.

This parameter is set to true when the relationship between two tables is marked with OnDelete Cascade.

Unfortunately, there is no way to set this attribute from within the designer, and no way to set it from within the partial class in the *DataContext.cs file. The only way to set it without enabling cascading deletes is to manually edit the *DataContext.designer.cs file.

In my case, this meant finding the Page association, and adding the DeleteOnNull property:

[Association(Name="Page_PageTag", Storage="_Page", ThisKey="PageId", OtherKey="iPageId", IsForeignKey=true)]
public Page Page
{
    ...
}

And adding the DeleteOnNull attribute:

[Association(Name="Page_PageTag", Storage="_Page", ThisKey="PageId", OtherKey="iPageId", IsForeignKey=true, DeleteOnNull = true)]
public Page Page
{
    ...
}

Note that the attribute needed to be added to the Page property of the PageTag class, not the other way around.

See also:
Beth Massi -- LINQ to SQL and One-To-Many Relationships
Dave Brace -- LINQ to SQL: DeleteOnNull

AaronSieb
Aaron, thanks for the insight. But wouldn't it be easier to just write two lines of code in your repository to mark the records for deletion?
Robert Harvey
It doesn't end up being two lines of code in the repository. I COULD add an UpdateTags(Page page, string tagString) method to the repository, but that wouldn't work as cleanly with model binding.
AaronSieb
This is also pretty easy to setup, now that I know that it's linked to On Delete Cascade, and can avoid hand-editing the designer file.
AaronSieb