问题
I am encountering a recurring problem that just makes no sense, and hoping someone (in the Breeze team?) can shed some light.
The following model illustrates the entities in question.
As you can see, I'm adhering pretty strictly to Entity Framework conventions in my property names, and as a result, if I check in SQL the cascade on delete rules are set by EF code first when it creates the db.
Now, when I try to delete a BusUnit
manually in SQL, the delete cascades correctly and the corresponding BusUnitDimensions
are also deleted, as it should be. Likewise, if I delete a Dimension
in SQL, the corresponding BusUnitDimensions
are also deleted.
However, in my application, if I mark a BusUnit
as setDeleted
with Breeze and then try saveChanges
, I get the following error.
The operation failed: The relationship could not be changed because one
or more of the foreign-key properties is non-nullable. When a change is
made to a relationship, the related foreign-key property is set to a null
value. If the foreign-key does not support null values, a new relationship
must be defined, the foreign-key property must be assigned another
non-null value, or the unrelated object must be deleted.
Strangely though, if I mark a Dimension
for deletion and then save (within Breeze), the cascaded delete works correctly and both the Dimension
and its corresponding BusUnitDimensions
are deleted.
So, why the inconsistency? Why are the cascaded delete rules in SQL not being applied for BusUnits
but yet they're working for Dimensions
? I've read elsewhere that Breeze does not support cascaded deletes, but then why is my Dimensions
case working?
EDIT:
I've removed my previous edits as they weren't relevant. The changes below follow on from Ward's answer...
My model now looks like this, and BusUnitDims
now uses BusUnitId
and DimId
as a compound key, and I've added a bool
, IsBud
for the purposes of payload.
I haven't yet implemented deletes for BusUnits, but already if I try delete a Dim, I'm getting the same error message:
The operation failed: The relationship could not be changed because one
or more of the foreign-key properties is non-nullable. When a change is
made to a relationship, the related foreign-key property is set to a null
value. If the foreign-key does not support null values, a new relationship
must be defined, the foreign-key property must be assigned another
non-null value, or the unrelated object must be deleted.
I have noticed that cascaded deletes is no longer enabled, and in fact, to get EF to build the database I to add the following configuration:
modelBuilder.Entity<BusUnitDim>()
.HasRequired(bud => bud.BusUnit)
.WithMany(bu => bu.BusUnitDims)
.HasForeignKey(bud => bud.BusUnitId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<BusUnitDim>()
.HasRequired(bud => bud.Dim)
.WithMany(d => d.BusUnitDims)
.HasForeignKey(bud => bud.DimId)
.WillCascadeOnDelete(false);
So, with cascading now explicitly not in place, I can understand why the error occurs. Does that imply that in the controller, one has to specifically mark each map for deletion when deleting a parent Dim or BusUnit and before saveChanges is called, or is there some way to configure EF to take advantage of cascaded deletes as this would hugely simplify the code in my controller?
(PS: it gets even more complex, because BusUnitDims
ends up having a further join table of its own, MetricBusUnitDims
to accommodate yet another entity in the model and their relationships. This is why I'm trying to get the principles right early on)
EDIT: (A CONTROLLER SOLUTION FOR BUSUNITS)
So, the following approach works for BusUnits
:
function deleteBusUnit(busUnitVm) { // note that you pass in the item viewmodel, not the entity
var busUnit = busUnitVm.busUnit;
var mapVms = busUnitVm.dimMapVms;
var dimHash = createBusUnitDimHash(busUnit);
mapVms.forEach(function (mapVm) {
var map = dimHash[mapVm.dim.id];
if (map) {
datacontext.markDeleted(map);
}
});
datacontext.markDeleted(busUnit);
save().then(function() { getDBoardConfig(); });
}
}
Is this the correct approach? if so, I'll still have to figure out the following:
- How to approach
Dims
. These are different becuase the item viewmodel is defined for BusUnits. - How to approach the situation where there is a join tabel one level down, e.g.
MetricBusUnitDIm
.
EDIT: (A CONTROLLER SOLUTION FOR DIMS)
function deleteDim(dim) {
return bsDialog.deleteDialog(dim.name, true)
.then(function () {
vm.busUnitVms.forEach(function (busUnitVm) {
busUnitVm.busUnit.busUnitDims.forEach(function (bud) {
if (bud.dimId === dim.id) {
datacontext.markDeleted(bud);
}
});
});
datacontext.markDeleted(dim);
save().then(function () { getDboardConfig(); });
});
}
回答1:
I believe your problems are traceable to the fact that your mapping table BusUnitDimension
has its own primary key, Id
, as opposed to the more typical approach in which the BusUnitId
and DimensionId
FK properties together comprise the compound primary key of BusUnitDimension
.
Observe that
OrderDetails
in Northwind and theHeroPoweMap
in the Breeze many-to-many example have compound keys.
Your choice creates complications.
First, it becomes possible to create multiple BusUnitDimension
entities representing the same association between BusUnit
and Dimension
(i.e., they all have the same pair of FKs). The database may be able to prevent this (it's been a long time since I looked) but whether it does or doesn't, it won't prevent you from creating those duplicates in Breeze ... and maybe not in EF either.
Secondly, it opens you up to the problem you're currently facing. If those mapping entities are in the DbContext
when you perform the delete, EF may (apparently does) try to null their FK properties as it sets either BusUnit
or Dimension
to the deleted state.
You can get around this, as has been suggested, by making both the BusUnitId
and DimensionId
FK properties nullable. But that is contrary to the semantics as a BusUnitDimension
must link a real BusUnit
to a real Dimension
; they aren't optional. The practical consequence may be that you don't get cascade delete from the EF perspective if you do this (not sure if the DB will enforce that either). That means you'd have orphaned BusUnitDimension
rows in your database with one or both FKs being null. I speculate because I'm not used to getting into this kind of trouble.
Another approach would be to set their FK values to zero (I think Breeze does this for you). Of course this implies the existence of BusUnit
and Dimension
table rows with Id == 0
, if only during the delete operation.
Btw, you could actually have such "sentinel entities" in your DB.
You must make sure that these BusUnitDimension
are in the deleted state or EF (and the DB) will either reject them (referential integrity constraint) or orphan them (you'll have BusUnitDimension
rows in your database with one or both FKs being zero).
Alternatively, if you know that the DB will cascade delete them, you can simply remove them from the DbContext
(remove from the EntityInfoMap
in the EFContextProvider
). But now you have to tell the Breeze client to get rid of them too if it happens to have them hanging around.
Enough Already!
These wandering thoughts should tell you that you've got yourself in a jam here with way too much bookkeeping ... and all because you gave BusUnitDimension
its own Id
primary key.
It gets a lot easier if you give BusUnitDimension
the compound key, {BusUnitId
, DimensionId
}. You must also give it a payload property (anything will do) to prevent EF from hiding it in its "many-to-many" implementation because Breeze doesn't handle that. Adding any nonsense property will do the trick.
HTH
回答2:
That has nothing to do with Breeze.. The originating message is from Entity Framework..
inside BusUnitDimension
Model update BusUnitId
property to:
public Nullable<int> BusUnitId { get; set; }
Notice the Nullable struct..
来源:https://stackoverflow.com/questions/21679073/entity-framework-object-graph-deletion-with-breeze