Attached is my current patch to make this work. It doesn't work.
The approach looks at all properties of the object we're
inserting/updating, looking for EntitySet<T>'s, and if any are found it
then inserts/updates the items found within the EntitySet<T>'s. This
appears to work properly.
The current problem is that after the Category is inserted,
Category.CategoryID is updated. However, the Product doesn't see the
new CategoryID value, so it tries to insert using 0 as the CategoryID,
which fails due to a Foreign Key constraint violation.
What I'm currently wondering is how the Products attached to the
Category are notified that the CategoryID has changed. The
DbLinq-generated Category and Product classes seem to be similar enough,
and INotifyPropertyChanged/etc. aren't used within the file, so I'm not
sure what mechanism is used to update all Products attached to the
Category.
Thoughts?
- Jon
On Mon, 2009-05-11 at 20:22 +0100, Giacomo Tesio wrote:
> Than probably its related with the InsertOnSubmit().
>
> Table<T> refer to the datacontext and surely register the entity.
> While registering it's probably deeply visited to locate any EntitySet
> and EntityRef filled.
> The entities filled are then registered (note that entity registration
> is idempotent: you should be able to register an entity many time
> without any problem)
>
> I try to take a look at it, now...
>
>
> Giacomo
>
>
> On Mon, May 11, 2009 at 6:29 PM, Jonathan Pryor <[email protected]>
> wrote:
>
> It's not a dumb question, and yes it passes on linq to sql.
>
> WriteTest.cs (which contains this test) is part of the
> DbLinq.SqlServer_test_strict project, which builds against SQL
> Server. These tests pass within that project.
>
> I'm not sure how Linq to SQL does it (is it EntitySet<T> that
> does it, or something else), but it is supported. I doubt
> that EntitySet<T> is directly involved, though, since there
> isn't any connection between EntitySet<T> and DataContext (so
> there'd be no way afaik for EntitySet<T>.Add() to implicitly
> register values).
>
> - Jon
>
>
>
>
> On Mon, 2009-05-11 at 17:26 +0200, Giacomo Tesio wrote:
>
> > I know it's a dumb question, but... do such a test pass on
> > linq to sql?
> >
> > I would imagine that you should add the product to the
> > product list too, to make it submitted.
> > I mean you should
> >
> > db.Products.InsertOnSubmit(p);
> >
> >
> >
> > The alternative would be the EntitySet keeping track of
> > entities added... (or asking to the datacontext to register
> > each added entities...
> >
> >
> > If it should work, it should not be too complex to do..
> >
> >
> > Giacomo
> >
> >
> > On Mon, May 11, 2009 at 4:55 PM, Jonathan Pryor
> > <[email protected]> wrote:
> >
> > Actually, it's worse than I thought. (Which is why
> > I should add more assertions to my tests...) The
> > real problem is that inserting and updating only
> > inserts/updates the immediate object, and not all
> > referenced objects. Thus, this (paraphrased from
> > WriteTest.cs):
> >
> > Category c = new Category {...};
> > Product p = new Product {...};
> > c.Products.Add(p);
> > db.Categories.InsertOnSubmit(c);
> > db.SubmitChanges();
> >
> > // This works: the Category IS inserted
> > Assert.AreEqual(1, db.Categories.Where(c =>
> c.CategoryName == ...).Count());
> > // This fails: the referenced Product is NOT
> inserted
> > Assert.AreEqual(1, db.Products.Where(p =>
> p.ProductName == ...).Count());
> >
> > Looks like QueryBuilder.GetInsertQuery() and
> > QueryBuilder.GetUpdateQuery() are at fault,
> > especially considering that they appear to assume
> > that only one statement will be required (while more
> > than one may be required when updating an entire
> > object graph, as the above example requires).
> >
> > - Jon
> >
> >
> >
> > On Mon, 2009-05-11 at 01:10 -0400, Jonathan Pryor
> > wrote:
> >
> > > After more debugging, I'm heading to bed, but this
> > > is what I've found out: our change tracking isn't
> > > tracking changes. :-)
> > >
> > > Specifically, when you have code like this (using
> > > something NUnit-testable instead of from
> > > NerdDinner):
> > >
> > > Category c = GetExistingCategory();
> > > Product p = new Product {...};
> > > c.Products.Add(p);
> > > db.SubmitChanges();
> > >
> > > the newly added product isn't actually added to
> > > the database. The problem appears to be because
> > > of MemberModificationHandler: it only tracks
> > > INotifyPropertyChanged and not
> > > INotifyPropertyChanging (see
> > > MemberModificationHandler.RegisterNotification()).
> > >
> > > So, what happens is c is a tracked object (yay).
> > > As such, it is registered with the EntityTracker
> > > (double yay). However, for change tracking to
> > > work, MemberModificationHandler uses the
> > > INotifyPropertyChanged interface. In this case,
> > > c.Products.Add(p) doesn't actually invoke
> > > MemberModificationHandler.OnPropertyChangedEvent(),
> because it isn't registered.
> > >
> > > Specifically, the registered Category fires the
> > > INotifyPropertyChanging.PropertyChanging event,
> > > because (as far as it knows) c.Products will be
> > > changing. However, c doesn't know what, if
> > > anything, will change. c.Products.Add(p)
> > > eventually calls into Category.attach_Products(),
> > > which then effectively does 'p.Category = c'.
> > > This last expression is what fires the
> > > INotifyPropertyChanged.PropertyChanged event, but
> > > only on p, which hasn't been registered with
> > > anything, and thus there are no handlers
> > > registered.
> > >
> > > Oops.
> > >
> > > The only solution I can think of is that
> > > MemberModificationHandler needs to listen to the
> > > INotifyPropertyChanging.PropertyChanging event (if
> > > available), and if it's fired it needs to check
> > > for changes on all properties of the changing
> > > object.
> > >
> > > So I did this, and it still fails (see attached
> > > patch, which includes the unit test). It properly
> > > detects that 'c' may have changed, but
> > > QueryRunner.Update() doesn't do a "deep" update;
> > > that is, it only updates the Category properties
> > > (CategoryName, Description, etc.) but not the
> > > values of anything attached to it (like the
> > > Category.Products collection).
> > >
> > > Any ideas on how to resolve this?
> > >
> > > Thanks,
> > > - Jon
> > >
> > > On Sun, 2009-05-10 at 22:02 -0400, Jonathan Pryor
> > > wrote:
> > >
> > > > I already fixed this, and it has been committed
> > > > to svn. DataContext.SetEntitySetQueries() was
> > > > being called because of
> > > > DataContext._GetOrRegisterEntity(), which in
> > > > turn was called by DataContext.Register(), which
> > > > was called by DataContext.SubmitChanges().
> > > > (Just check the callstack in the original
> > > > message...) The problem was twofold: (1)
> > > > DbLinq's EntitySet<T> was screwy (though I blame
> > > > a lot of that screwiness on .NET's EntitySet<T>
> > > > -- see the new EntitySetTest.cs file, and laugh
> > > > hysterically when you see when the
> > > > EntitySet<T>.ListChanged event is raised), and
> > > > (2) calling dinner.RSVPs.Add(rsvp) caused the
> > > > EntitySet<RSVP> source to be created, and
> > > > EntitySet<T>.SetSource() cannot be called again
> > > > once EntitySet<T>.HasLoadedOrAssignedValues is
> > > > true. Since DataContext.SetEntitySetsQueries()
> > > > tried to do just that, it threw an exception.
> > > >
> > > > Now I'm working on a related bug: subsequent
> > > > updates aren't tracked. To wit:
> > > >
> > > > Dinner dinner = new Dinner {...};
> > > > RSVP rsvp = new RSVP();
> > > > rsvp.AttendeeName = User.Identity.Name;
> > > > dinner.RSVPs.Add(rsvp);
> > > > db.Dinners.InsertOnSubmit(dinner);
> > > > db.SubmitChanges();
> > > >
> > > > // The above now works. Now...
> > > >
> > > > RSVP rsvp2 = new RSVP();
> > > > rsvp2.AttendeeName = "whatever";
> > > > dinner.RSVPs.Add(rsvp2);
> > > > db.SubmitChanges();
> > > > // rsvp2 is NOT submitted to the database.
> > > >
> > > > I'm still tracking down why this happens. The
> > > > attached patch file recreates this same scenario
> > > > within the unit tests, and also (thankfully)
> > > > fails.
> > > >
> > > > What I would appreciate is if you could look
> > > > into the cache issues that I'm seeing. :-)
> > > >
> > > > Thanks,
> > > > - Jon
> > > >
> > > > On Sun, 2009-05-10 at 23:41 +0100, Giacomo Tesio
> > > > wrote:
> > > >
> > > > > I think this is due to the
> > > > > "dinner.RSVPs.Add(rsvp);" line where you
> > > > > actually fill the EntitySet.
> > > > >
> > > > > The DataContext.SetEntitySetQueries() should
> > > > > not run on just inserted entity, since they
> > > > > are yet filled.
> > > > >
> > > > >
> > > > > Tomorrow I'll try to fix this...
> > > > >
> > > > >
> > > > > Giacomo
> > > > >
> > > > >
> > > > > On Fri, May 8, 2009 at 4:59 AM, Jonathan Pryor
> > > > > <[email protected]> wrote:
> > > > >
> > > > > Still continuing with my "NerdDinner
> > > > > on Mono" effort, and hitting the
> > > > > following issue:
> > > > >
> > > > > When I attempt to add a new Dinner,
> > > > > NerdDinner effectively does:
> > > > >
> > > > > Dinner dinner = new Dinner {...};
> > > > > RSVP rsvp = new RSVP();
> > > > > rsvp.AttendeeName =
> User.Identity.Name;
> > > > > dinner.RSVPs.Add(rsvp);
> > > > > db.Dinners.InsertOnSubmit(dinner);
> > > > > db.SubmitChanges();
> > > > >
> > > > > This fails, with:
> > > > >
> > > > >
> System.Reflection.TargetInvocationException: Exception has been thrown by the
> target of an invocation. ---> System.InvalidOperationException: The EntitySet
> is already loaded and the source cannot be changed.
> > > > > at
> System.Data.Linq.EntitySet`1[NerdDinner.Models.RSVP].SetSource (IEnumerable`1
> entitySource) [0x00000]
> > > > > at (wrapper managed-to-native)
> System.Reflection.MonoMethod:InternalInvoke
> (object,object[],System.Exception&)
> > > > > at
> System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags
> invokeAttr, System.Reflection.Binder binder, System.Object[] parameters,
> System.Globalization.CultureInfo culture) [0x000ca] in
> /home/jon/Development/mono-HEAD/mcs/class/corlib/System.Reflection/MonoMethod.cs:169
>
> > > > > --- End of inner exception stack
> trace ---
> > > > > at
> System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags
> invokeAttr, System.Reflection.Binder binder, System.Object[] parameters,
> System.Globalization.CultureInfo culture) [0x000e5] in
> /home/jon/Development/mono-HEAD/mcs/class/corlib/System.Reflection/MonoMethod.cs:179
>
> > > > > at
> System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[]
> parameters) [0x00000] in
> /home/jon/Development/mono-HEAD/mcs/class/corlib/System.Reflection/MethodBase.cs:111
>
> > > > > at
> System.Data.Linq.DataContext.SetEntitySetsQueries (System.Object entity)
> [0x00173] in
> /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:608
>
> > > > > at
> System.Data.Linq.DataContext._GetOrRegisterEntity (System.Object entity)
> [0x00015] in
> /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:468
>
> > > > > at
> System.Data.Linq.DataContext.Register (System.Object entity) [0x0000d] in
> /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:675
>
> > > > > at
> System.Data.Linq.DataContext.SubmitChanges (ConflictMode failureMode)
> [0x000a6] in
> /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:378
>
> > > > > at
> System.Data.Linq.DataContext.SubmitChanges () [0x00000] in
> /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:339
>
> > > > > at
> NerdDinner.Models.DinnerRepository.Save () [0x00000]
> > > > > at
> NerdDinner.Controllers.DinnersController.Create (NerdDinner.Models.Dinner
> dinner) [0x00000]
> > > > >
> > > > > Suffice it to say, this doesn't happen
> > > > > under .NET's System.Data.Linq. I'm
> > > > > wondering if anyone has seen a similar
> > > > > error to this before, and/or knows
> > > > > what an appropriate fix would be.
> > > > >
> > > > > Thanks,
> > > > > - Jon
> > > > >
> > > > >
> > > > >
> > > > >
> > > > >
> > > > >
> > > > >
> > > > >
> > > >
> > > >
> > > >
> > >
> > >
> > >
> >
> >
> >
> >
> >
> >
> >
> >
>
>
>
>
>
>
>
>
> >
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"DbLinq" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/dblinq?hl=en
-~----------~----~----~----~------~----~------~--~---
Index: src/DbLinq/Test/Providers/WriteTest.cs
===================================================================
--- src/DbLinq/Test/Providers/WriteTest.cs (revision 1072)
+++ src/DbLinq/Test/Providers/WriteTest.cs (working copy)
@@ -768,27 +768,54 @@
[Test]
public void InsertAndDeleteWithDependencies()
{
+ const string newCategoryName = "Test Category";
+ const string newProduct1 = "My First Test Product";
+ const string newProduct2 = "My Second Test Product";
+
var db = CreateDB();
var product = new Product
{
Discontinued = true,
- ProductName = "This is a test product",
+ ProductName = newProduct1,
};
var category = new Category
{
- CategoryName = "My New Category",
+ CategoryName = newCategoryName,
Description = "Insert Description Here",
};
category.Products.Add(product);
+ Assert.AreEqual(0, category.CategoryID);
+ Assert.AreEqual(0, product.CategoryID.Value);
+
db.Categories.InsertOnSubmit(category);
db.SubmitChanges();
+ Assert.AreEqual(1, db.Categories.Where(c => c.CategoryName == newCategoryName).Count());
+ Assert.AreNotEqual(0, category.CategoryID);
+ Assert.AreEqual(1, db.Products.Where(p => p.ProductName == newProduct1).Count());
+ Assert.AreEqual(category.CategoryID, product.CategoryID.Value);
+
+ var p2 = new Product
+ {
+ Discontinued = true,
+ ProductName = newProduct2
+ };
+ category.Products.Add(p2);
+ db.SubmitChanges();
+
+ Assert.AreEqual(1, db.Products.Where(p => p.ProductName == newProduct2).Count());
+
db.Products.DeleteOnSubmit(product);
+ db.Products.DeleteOnSubmit(p2);
db.Categories.DeleteOnSubmit(category);
db.SubmitChanges();
+
+ Assert.AreEqual(0, db.Categories.Where(c => c.CategoryName == newCategoryName).Count());
+ Assert.AreEqual(0, db.Products.Where(p => p.ProductName == newProduct1).Count());
+ Assert.AreEqual(0, db.Products.Where(p => p.ProductName == newProduct2).Count());
}
}
}
Index: src/DbLinq/Data/Linq/DataContext.cs
===================================================================
--- src/DbLinq/Data/Linq/DataContext.cs (revision 1072)
+++ src/DbLinq/Data/Linq/DataContext.cs (working copy)
@@ -373,25 +373,34 @@
switch (entityTrack.EntityState)
{
case EntityState.ToInsert:
- var insertQuery = QueryBuilder.GetInsertQuery(entityTrack.Entity, queryContext);
- QueryRunner.Insert(entityTrack.Entity, insertQuery);
- Register(entityTrack.Entity);
+ foreach (var toInsert in GetReferencedObjects(entityTrack.Entity))
+ {
+ var insertQuery = QueryBuilder.GetInsertQuery(toInsert, queryContext);
+ QueryRunner.Insert(toInsert, insertQuery);
+ Register(toInsert);
+ }
break;
case EntityState.ToWatch:
- if (MemberModificationHandler.IsModified(entityTrack.Entity, Mapping))
+ foreach (var toUpdate in GetReferencedObjects(entityTrack.Entity))
{
- var modifiedMembers = MemberModificationHandler.GetModifiedProperties(entityTrack.Entity, Mapping);
- var updateQuery = QueryBuilder.GetUpdateQuery(entityTrack.Entity, modifiedMembers, queryContext);
- QueryRunner.Update(entityTrack.Entity, updateQuery, modifiedMembers);
+ if (MemberModificationHandler.IsModified(toUpdate, Mapping))
+ {
+ var modifiedMembers = MemberModificationHandler.GetModifiedProperties(toUpdate, Mapping);
+ var updateQuery = QueryBuilder.GetUpdateQuery(toUpdate, modifiedMembers, queryContext);
+ QueryRunner.Update(toUpdate, updateQuery, modifiedMembers);
- RegisterUpdateAgain(entityTrack.Entity);
+ RegisterUpdateAgain(toUpdate);
+ }
}
break;
case EntityState.ToDelete:
- var deleteQuery = QueryBuilder.GetDeleteQuery(entityTrack.Entity, queryContext);
- QueryRunner.Delete(entityTrack.Entity, deleteQuery);
+ foreach (var toDelete in GetReferencedObjects(entityTrack.Entity))
+ {
+ var deleteQuery = QueryBuilder.GetDeleteQuery(toDelete, queryContext);
+ QueryRunner.Delete(toDelete, deleteQuery);
- UnregisterDelete(entityTrack.Entity);
+ UnregisterDelete(entityTrack.Entity);
+ }
break;
default:
throw new ArgumentOutOfRangeException();
@@ -402,6 +411,35 @@
}
}
+ private static IEnumerable<object> GetReferencedObjects(object value)
+ {
+ var values = new List<object>();
+ FillReferencedObjects(value, values);
+ return values;
+ }
+
+ private static void FillReferencedObjects(object value, List<object> values)
+ {
+ if (value == null)
+ return;
+ values.Add(value);
+ foreach (var p in value.GetType().GetProperties())
+ {
+ var type = p.PropertyType.IsGenericType
+ ? p.PropertyType.GetGenericTypeDefinition()
+ : null;
+ if (type != null && p.CanRead && type == typeof(EntitySet<>) &&
+ p.GetGetMethod().GetParameters().Length == 0)
+ {
+ var set = p.GetValue(value, null);
+ if (set == null)
+ continue;
+ foreach (var o in ((IEnumerable)set))
+ FillReferencedObjects(o, values);
+ }
+ }
+ }
+
/// <summary>
/// TODO - allow generated methods to call into stored procedures
/// </summary>
Index: src/DbLinq/Data/Linq/Implementation/MemberModificationHandler.cs
===================================================================
--- src/DbLinq/Data/Linq/Implementation/MemberModificationHandler.cs (revision 1072)
+++ src/DbLinq/Data/Linq/Implementation/MemberModificationHandler.cs (working copy)
@@ -178,10 +178,18 @@
return;
modifiedProperties[entity] = new Dictionary<string, MemberInfo>();
- if (entity is INotifyPropertyChanged)
+ var entityChanged = entity as INotifyPropertyChanged;
+ if (entityChanged != null)
{
- ((INotifyPropertyChanged)entity).PropertyChanged += (OnPropertyChangedEvent);
+ entityChanged.PropertyChanged += OnPropertyChangedEvent;
}
+
+ var entityChanging = entity as INotifyPropertyChanging;
+ if (entityChanging != null)
+ {
+ entityChanging.PropertyChanging += OnPropertyChangingEvent;
+ }
+
// then check all properties, and note them as changed if they already did
if (!ReferenceEquals(entity, entityOriginalState)) // only if we specified another original entity
{
@@ -208,6 +216,12 @@
SetPropertyChanged(sender, e.PropertyName);
}
+ private void OnPropertyChangingEvent(object entity, PropertyChangingEventArgs e)
+ {
+ foreach (var p in entity.GetType().GetProperties())
+ modifiedProperties[entity][p.Name] = p;
+ }
+
/// <summary>
/// Unregisters an entity.
/// This is useful when it is switched from update to delete list