エンティティでGetById()を実行し、MVCビューから取得した新しいリストに子エンティティのコレクションを設定すると、このエラーが発生します。
操作が失敗しました:1つ以上の外部キープロパティがnull不可であるため、関係を変更できませんでした。関係が変更されると、関連する外部キープロパティがnull値に設定されます。外部キーがnull値をサポートしていない場合、新しい関係を定義するか、外部キープロパティに別のnull以外の値を割り当てるか、無関係なオブジェクトを削除する必要があります。
私はこの行をよく理解していません:
1つ以上の外部キープロパティがnull不可であるため、関係を変更できませんでした。
2つのエンティティ間の関係を変更するのはなぜですか?アプリケーション全体のライフタイムを通じて同じままである必要があります。
例外が発生するコードは、コレクション内の変更された子クラスを既存の親クラスに単純に割り当てることです。これは、子クラスの削除、新しいクラスの追加、および変更に対応できることを期待しています。 Entity Frameworkがこれを処理すると思っていたでしょう。
コードの行は次のように抽出できます。
var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
古い子アイテムthisParent.ChildItems
を1つずつ手動で削除する必要があります。 Entity Frameworkはそれを行いません。最後に、古い子アイテムで何をしたいかを決定することはできません-それらを捨てたい場合、またはそれらを保持して他の親エンティティに割り当てたい場合。 Entity Frameworkに決定を伝える必要があります。ただし、子エンティティは、データベース内の親への参照なしでは単独で生きることはできないため(外部キー制約のため)、これら2つの決定のうちの1つを行う必要があります。それは基本的に例外が言っていることです。
編集
子アイテムを追加、更新、削除できる場合の対処方法:
public void UpdateEntity(ParentItem parent)
{
// Load original parent including the child item collection
var originalParent = _dbContext.ParentItems
.Where(p => p.ID == parent.ID)
.Include(p => p.ChildItems)
.SingleOrDefault();
// We assume that the parent is still in the DB and don't check for null
// Update scalar properties of parent,
// can be omitted if we don't expect changes of the scalar properties
var parentEntry = _dbContext.Entry(originalParent);
parentEntry.CurrentValues.SetValues(parent);
foreach (var childItem in parent.ChildItems)
{
var originalChildItem = originalParent.ChildItems
.Where(c => c.ID == childItem.ID && c.ID != 0)
.SingleOrDefault();
// Is original child item with same ID in DB?
if (originalChildItem != null)
{
// Yes -> Update scalar properties of child item
var childEntry = _dbContext.Entry(originalChildItem);
childEntry.CurrentValues.SetValues(childItem);
}
else
{
// No -> It's a new child item -> Insert
childItem.ID = 0;
originalParent.ChildItems.Add(childItem);
}
}
// Don't consider the child items we have just added above.
// (We need to make a copy of the list by using .ToList() because
// _dbContext.ChildItems.Remove in this loop does not only delete
// from the context but also from the child collection. Without making
// the copy we would modify the collection we are just interating
// through - which is forbidden and would lead to an exception.)
foreach (var originalChildItem in
originalParent.ChildItems.Where(c => c.ID != 0).ToList())
{
// Are there child items in the DB which are NOT in the
// new child item collection anymore?
if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID))
// Yes -> It's a deleted child item -> Delete
_dbContext.ChildItems.Remove(originalChildItem);
}
_dbContext.SaveChanges();
}
注:これはテストされていません。子アイテムコレクションのタイプはICollection
であると想定しています。 (通常、IList
を持っているので、コードは少し違って見えます。)また、リポジトリを単純化するために、すべてのリポジトリの抽象化を取り除きました。
それが良い解決策かどうかはわかりませんが、ナビゲーションコレクションのすべての種類の変更を処理するには、これらの線に沿って何らかのハードワークを行う必要があると思います。また、もっと簡単な方法を見つけてうれしいです。
これに直面している理由は、compositionとaggregationの違いによるものです。
構成では、子オブジェクトは親が作成されるときに作成され、親が破棄されると破棄されます。そのため、その有効期間は親によって制御されます。例えばブログ投稿とそのコメント。投稿が削除された場合、そのコメントは削除する必要があります。存在しない投稿にコメントを付けても意味がありません。注文と注文品目についても同じです。
集計では、子オブジェクトは親に関係なく存在できます。親が破棄された場合、後で別の親に追加される可能性があるため、子オブジェクトは引き続き存在できます。例:プレイリストとそのプレイリスト内の曲との関係。プレイリストが削除された場合、曲は削除されません。別のプレイリストに追加される場合があります。
Entity Frameworkが集約と構成の関係を区別する方法は次のとおりです。
合成の場合:子オブジェクトには複合主キー(ParentID、ChildID)が必要です。子のIDは親のスコープ内にある必要があるため、これは仕様です。
集約の場合:子オブジェクトの外部キープロパティがNULL可能であることを期待します。
したがって、この問題が発生する理由は、子テーブルで主キーをどのように設定したかによるものです。コンポジットにする必要がありますが、そうではありません。したがって、Entity Frameworkはこの関連付けを集計と見なします。つまり、子オブジェクトを削除またはクリアしても、子レコードは削除されません。単に関連付けを削除し、対応する外部キー列をNULLに設定します(したがって、これらの子レコードは後で別の親に関連付けることができます)。列はNULLを許可しないため、前述の例外が発生します。
解決策:
1-複合キーを使用したくないという強力な理由がある場合、子オブジェクトを明示的に削除する必要があります。そして、これは前に提案したソリューションよりも簡単に行うことができます。
context.Children.RemoveRange(parent.Children);
2-それ以外の場合、子テーブルに適切な主キーを設定することにより、コードはより意味のあるものになります。
parent.Children.Clear();
これは非常に大きな問題です。あなたのコードで実際に起こることはこれです:
Parent
をロードし、添付されたエンティティを取得します今、解決策はあなたが何をしたいのか、そしてどのようにそれをしたいのかに本当に依存していますか?
ASP.NET MVCを使用している場合は、 pdateModelまたはTryUpdateModel を使用してみてください。
既存の子を手動で更新するだけの場合は、次のようにするだけです。
foreach (var child in modifiedParent.ChildItems)
{
context.Childs.Attach(child);
context.Entry(child).State = EntityState.Modified;
}
context.SaveChanges();
アタッチは実際には必要ありません(状態をModified
に設定するとエンティティもアタッチされます)が、プロセスがより明確になるので気に入っています。
既存の変更、既存の削除、新しい子の挿入を行う場合は、次のようにする必要があります。
var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well
foreach(var child in modifiedParent.ChildItems)
{
var attachedChild = FindChild(parent, child.Id);
if (attachedChild != null)
{
// Existing child - apply new values
context.Entry(attachedChild).CurrentValues.SetValues(child);
}
else
{
// New child
// Don't insert original object. It will attach whole detached graph
parent.ChildItems.Add(child.Clone());
}
}
// Now you must delete all entities present in parent.ChildItems but missing
// in modifiedParent.ChildItems
// ToList should make copy of the collection because we can't modify collection
// iterated by foreach
foreach(var child in parent.ChildItems.ToList())
{
var detachedChild = FindChild(modifiedParent, child.Id);
if (detachedChild == null)
{
parent.ChildItems.Remove(child);
context.Childs.Remove(child);
}
}
context.SaveChanges();
同じエラーに対して this answerの方がはるかに役立つことがわかりました。 EFは、削除するときは気に入らないようで、削除を優先します。
このようなレコードに添付されたレコードのコレクションを削除できます。
order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);
この例では、注文に添付されているすべての詳細レコードの状態が削除に設定されています。 (注文更新の一部として、更新された詳細を追加して戻す準備中)
他の2つの答えがなぜそんなに人気があるのか、私にはわかりません!
ORMフレームワークがそれを処理するはずであるとあなたは正しかったと思います-結局のところ、それが提供することを約束しているものです。そうしないと、永続性の問題によりドメインモデルが破損します。カスケード設定を正しくセットアップすれば、NHibernateはこれをうまく管理します。 Entity Frameworkでは、データベースモデルをセットアップするとき、特に何をカスケードする必要があるかを推測する必要がある場合に、より優れた標準に従うことを期待しています。
「 関係を識別する 」を使用して、 親と子の関係を定義する を正しく行う必要があります。
これを行うと、Entity Frameworkは、子オブジェクトが親によってidentifiedであることを認識します。したがって、「cascade-delete-orphans」状況でなければなりません。
上記以外に、mightする必要があります(NHibernateの経験から)
thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
リスト全体を置き換える代わりに。
UPDATE
@Slaumaのコメントは、独立したエンティティが全体的な問題の別の部分であることを思い出させてくれました。これを解決するには、コンテキストからモデルをロードして、モデルを構築するカスタムモデルバインダーを使用する方法を使用できます。 このブログ投稿 は、私が意味するものの例を示しています。
同じクラスのEntity FrameworkでAutoMapperを使用している場合、この問題が発生する可能性があります。たとえば、クラスが
class A
{
public ClassB ClassB { get; set; }
public int ClassBId { get; set; }
}
AutoMapper.Map<A, A>(input, destination);
これにより、両方のプロパティがコピーされます。この場合、ClassBIdはNull不可です。 AutoMapperはdestination.ClassB = input.ClassB;
をコピーするため、これにより問題が発生します。
AutoMapperをClassB
プロパティを無視するように設定します。
cfg.CreateMap<A, A>()
.ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
これは、子エンティティが削除済みではなく変更済みとしてマークされているために発生します。
そして、parent.Remove(child)
が実行されたときにEFが子エンティティに対して行う変更は、単に親への参照をnull
に設定することです。
SaveChanges()
を実行した後、例外が発生したときにVisual Studioのイミディエイトウィンドウに次のコードを入力すると、子のEntityStateを確認できます。
_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity
xは、削除されたエンティティに置き換えられる必要があります。
_context.ChildEntity.Remove(child)
を実行するObjectContext
にアクセスできない場合は、外部キーを子テーブルの主キーの一部にすることでこの問題を解決できます。
Parent
________________
| PK IdParent |
| Name |
|________________|
Child
________________
| PK IdChild |
| PK,FK IdParent |
| Name |
|________________|
このように、parent.Remove(child)
を実行すると、EFはエンティティを削除済みとして正しくマークします。
同じエラーが発生しました。親子関係を持つ2つのテーブルがありますが、子テーブルのテーブル定義の外部キー列に「削除カスケード」を構成しました。したがって、データベース内の親行を(SQLを介して)手動で削除すると、子行が自動的に削除されます。
しかし、これはEFでは機能しませんでした。このスレッドで説明されているエラーが表示されました。これは、エンティティデータモデル(edmxファイル)で、親テーブルと子テーブルの間の関連付けのプロパティが正しくなかったためです。 End1 OnDelete
オプションはnone
になるように構成されました(私のモデルの「End1」は、多重度が1の終わりです)。
End1 OnDelete
オプションを手動でCascade
に変更しましたが、動作しませんでした。データベースからモデルを更新するときに、EFがこれを選択できない理由がわかりません(データベースが最初のモデルです)。
完全を期すために、これは私の削除するコードがどのように見えるかです:
public void Delete(int id)
{
MyType myObject = _context.MyTypes.Find(id);
_context.MyTypes.Remove(myObject);
_context.SaveChanges();
}
カスケード削除が定義されていない場合、親行を削除する前に子行を手動で削除する必要があります。
ChildItemsコレクションを手動でクリアし、新しいアイテムを追加する必要があります。
thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
その後、孤立したエンティティを処理するDeleteOrphans拡張メソッドを呼び出すことができます(DetectChangesメソッドとSaveChangesメソッドの間で呼び出す必要があります)。
public static class DbContextExtensions
{
private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>();
public static void DeleteOrphans( this DbContext source )
{
var context = ((IObjectContextAdapter)source).ObjectContext;
foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
{
var entityType = entry.EntitySet.ElementType as EntityType;
if (entityType == null)
continue;
var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap);
var props = entry.GetModifiedProperties().ToArray();
foreach (var prop in props)
{
NavigationProperty navProp;
if (!navPropMap.TryGetValue(prop, out navProp))
continue;
var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
var enumerator = related.GetEnumerator();
if (enumerator.MoveNext() && enumerator.Current != null)
continue;
entry.Delete();
break;
}
}
}
private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type )
{
var result = type.NavigationProperties
.Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
.Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType()))
.Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() })
.Where(v => v.DependentProperties.Length == 1)
.ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty);
return new ReadOnlyDictionary<string, NavigationProperty>(result);
}
}
今日、この問題に遭遇し、自分のソリューションを共有したいと考えました。私の場合、解決策は、データベースから親を取得する前に子アイテムを削除することでした。
以前は、次のコードのように実行していました。この質問にリストされているのと同じエラーが表示されます。
var Parent = GetParent(parentId);
var children = Parent.Children;
foreach (var c in children )
{
Context.Children.Remove(c);
}
Context.SaveChanges();
私にとってうまくいったのは、まずparentId(外部キー)を使用して子アイテムを取得し、それらのアイテムを削除することです。その後、データベースから親を取得できます。その時点で、子アイテムがなくなり、新しい子アイテムを追加できます。
var children = GetChildren(parentId);
foreach (var c in children )
{
Context.Children.Remove(c);
}
Context.SaveChanges();
var Parent = GetParent(parentId);
Parent.Children = //assign new entities/items here
これらのソリューションや他の多くのソリューションを試しましたが、どれもうまくいきませんでした。これはグーグルでの最初の回答なので、ここにソリューションを追加します。
私のためにうまくいった方法は、コミット中に関係を写真から取り除くことでした。そのため、EFが失敗することはありませんでした。これを行うには、DBContextで親オブジェクトを再検索し、削除します。再検出されたオブジェクトのナビゲーションプロパティはすべてnullであるため、子の関係はコミット中に無視されます。
var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();
これは、外部キーがON DELETE CASCADEで設定されていることを前提としているため、親行が削除されると、子はデータベースによってクリーンアップされることに注意してください。
このタイプのソリューションは私のためにトリックをしました:
Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID);
db.Childs.RemoveRange(original.Childs);
updated.Childs.ToList().ForEach(c => original.Childs.Add(c));
db.Entry<Parent>(original).CurrentValues.SetValues(updated);
これにより、すべてのレコードが削除され、再び挿入されると言うことが重要です。しかし、私の場合(10未満)は大丈夫です。
役に立てば幸いです。
Moshのソリューション を使用しましたが、最初にコードで構成キーを正しく実装する方法は明らかではありませんでした。
だからここに解決策があります:
public class Holiday
{
[Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int HolidayId { get; set; }
[Key, Column(Order = 1), ForeignKey("Location")]
public LocationEnum LocationId { get; set; }
public virtual Location Location { get; set; }
public DateTime Date { get; set; }
public string Name { get; set; }
}
また、 Moshの答え で問題を解決しました。 PeterBの答え は、外部キーとして列挙型を使用していたので、少しだと思いました。このコードを追加した後、新しい移行を追加する必要があることに注意してください。
他のソリューションのためにこのブログ投稿をお勧めすることもできます。
http://www.kianryan.co.uk/2013/03/orphaned-child/
コード:
public class Child
{
[Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Heading { get; set; }
//Add other properties here.
[Key, Column(Order = 1)]
public int ParentId { get; set; }
public virtual Parent Parent { get; set; }
}
私は数時間前にこの問題に遭遇し、すべてを試しましたが、私の場合、解決策は上記のリストとは異なりました。
データベースから既に取得したエンティティを使用して、その子を変更しようとすると、エラーが発生しますが、データベースからエンティティの新しいコピーを取得しても問題はありません。これを使用しないでください:
public void CheckUsersCount(CompanyProduct companyProduct)
{
companyProduct.Name = "Test";
}
これを使って:
public void CheckUsersCount(Guid companyProductId)
{
CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
companyProduct.Name = "Test";
}
Slaumaのソリューションを使用して、子オブジェクトと子オブジェクトのコレクションを更新するためのいくつかの汎用関数を作成しました。
すべての永続オブジェクトはこのインターフェイスを実装します
/// <summary>
/// Base interface for all persisted entries
/// </summary>
public interface IBase
{
/// <summary>
/// The Id
/// </summary>
int Id { get; set; }
}
これにより、これら2つの機能をリポジトリに実装しました
/// <summary>
/// Check if orgEntry is set update it's values, otherwise add it
/// </summary>
/// <param name="set">The collection</param>
/// <param name="entry">The entry</param>
/// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
/// <returns>The added or updated entry</returns>
public T AddOrUpdateEntry<T>(DbSet<T> set, T entry, T orgEntry) where T : class, IBase
{
if (entry.Id == 0 || orgEntry == null)
{
entry.Id = 0;
return set.Add(entry);
}
else
{
Context.Entry(orgEntry).CurrentValues.SetValues(entry);
return orgEntry;
}
}
/// <summary>
/// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
/// all entries found in the orignal list that are not in the new list are removed
/// </summary>
/// <typeparam name="T">The type of entry</typeparam>
/// <param name="set">The database set</param>
/// <param name="newList">The new list</param>
/// <param name="orgList">The original list</param>
public void AddOrUpdateCollection<T>(DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
{
// attach or update all entries in the new list
foreach (T entry in newList)
{
// Find out if we had the entry already in the list
var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);
AddOrUpdateEntry(set, entry, orgEntry);
}
// Remove all entries from the original list that are no longer in the new list
foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
{
if (!newList.Any(e => e.Id == orgEntry.Id))
{
set.Remove(orgEntry);
}
}
}
それを使用するには、次の手順を実行します。
var originalParent = _dbContext.ParentItems
.Where(p => p.Id == parent.Id)
.Include(p => p.ChildItems)
.Include(p => p.ChildItems2)
.SingleOrDefault();
// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);
// Update each collection
AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);
お役に立てれば
追加:別のDbContextExtentions(または独自のコンテキストインターフェイス)クラスを作成することもできます。
public static void DbContextExtentions {
/// <summary>
/// Check if orgEntry is set update it's values, otherwise add it
/// </summary>
/// <param name="_dbContext">The context object</param>
/// <param name="set">The collection</param>
/// <param name="entry">The entry</param>
/// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
/// <returns>The added or updated entry</returns>
public static T AddOrUpdateEntry<T>(this DbContext _dbContext, DbSet<T> set, T entry, T orgEntry) where T : class, IBase
{
if (entry.IsNew || orgEntry == null) // New or not found in context
{
entry.Id = 0;
return set.Add(entry);
}
else
{
_dbContext.Entry(orgEntry).CurrentValues.SetValues(entry);
return orgEntry;
}
}
/// <summary>
/// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
/// all entries found in the orignal list that are not in the new list are removed
/// </summary>
/// <typeparam name="T">The type of entry</typeparam>
/// <param name="_dbContext">The context object</param>
/// <param name="set">The database set</param>
/// <param name="newList">The new list</param>
/// <param name="orgList">The original list</param>
public static void AddOrUpdateCollection<T>(this DbContext _dbContext, DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
{
// attach or update all entries in the new list
foreach (T entry in newList)
{
// Find out if we had the entry already in the list
var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);
AddOrUpdateEntry(_dbContext, set, entry, orgEntry);
}
// Remove all entries from the original list that are no longer in the new list
foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
{
if (!newList.Any(e => e.Id == orgEntry.Id))
{
set.Remove(orgEntry);
}
}
}
}
次のように使用します:
var originalParent = _dbContext.ParentItems
.Where(p => p.Id == parent.Id)
.Include(p => p.ChildItems)
.Include(p => p.ChildItems2)
.SingleOrDefault();
// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = _dbContext.AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);
// Update each collection
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);
この問題は、親テーブルを削除しようとしても子テーブルのデータが存在するために発生します。カスケード削除を使用して問題を解決します。
dbcontextクラスのモデル作成メソッド内
modelBuilder.Entity<Job>()
.HasMany<JobSportsMapping>(C => C.JobSportsMappings)
.WithRequired(C => C.Job)
.HasForeignKey(C => C.JobId).WillCascadeOnDelete(true);
modelBuilder.Entity<Sport>()
.HasMany<JobSportsMapping>(C => C.JobSportsMappings)
.WithRequired(C => C.Sport)
.HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);
その後、API呼び出しで
var JobList = Context.Job
.Include(x => x.JobSportsMappings) .ToList();
Context.Job.RemoveRange(JobList);
Context.SaveChanges();
カスケード削除オプションは、このシンプルなコードで親と同様に親に関連する子テーブルを削除します。この簡単な方法で試してください。
データベース内のレコードのリストの削除に使用した範囲の削除Thanks
いくつかの問題が発生したよりもレコードを削除しようとすると同じ問題に直面しました。この問題の解決策は、ヘッダー/マスターレコードを削除する前に何かを失うよりもレコードを削除しようとすると、コードに書き込む必要があることですヘッダー/マスターの前に詳細を削除してください。問題が解決することを願っています。