Không thể thay đổi mối quan hệ vì một hoặc nhiều thuộc tính khóa ngoài là không thể rỗng


192

Tôi gặp lỗi này khi tôi GetById () trên một thực thể và sau đó đặt bộ sưu tập các thực thể con vào danh sách mới của tôi xuất phát từ chế độ xem MVC.

Thao tác thất bại: Không thể thay đổi mối quan hệ vì một hoặc nhiều thuộc tính khóa ngoài là không thể rỗng. Khi một thay đổi được thực hiện cho một mối quan hệ, thuộc tính khóa ngoài có liên quan được đặt thành giá trị null. Nếu khóa ngoài không hỗ trợ giá trị null, phải xác định mối quan hệ mới, thuộc tính khóa ngoài phải được gán một giá trị khác null hoặc phải xóa đối tượng không liên quan.

Tôi không hiểu lắm về dòng này:

Mối quan hệ không thể thay đổi vì một hoặc nhiều thuộc tính khóa ngoài là không thể rỗng.

Tại sao tôi thay đổi mối quan hệ giữa 2 thực thể? Nó sẽ giữ nguyên trong suốt vòng đời của toàn bộ ứng dụng.

Mã ngoại lệ xảy ra trên là đơn giản gán các lớp con đã sửa đổi trong một bộ sưu tập cho lớp cha hiện có. Điều này hy vọng sẽ phục vụ cho việc loại bỏ các lớp trẻ em, bổ sung các lớp mới và sửa đổi. Tôi đã nghĩ Entity Framework xử lý việc này.

Các dòng mã có thể được chưng cất để:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();

Tôi đã tìm thấy câu trả lời của mình bằng cách sử dụng giải pháp số 2 trong bài viết dưới đây, về cơ bản tôi đã tạo thêm một khóa chính vào bảng con để tham chiếu đến bảng cha (vì vậy nó có 2 khóa chính (khóa ngoại cho bảng cha và ID cho bảng con). c-sharpcorner.com/UploadFile/ff2f08/ triệt
yougotiger

@jaffa, tôi đã tìm thấy câu trả lời của mình ở đây stackoverflow.com/questions/22858491/ Lời
antonio

Câu trả lời:


159

Bạn nên xóa từng mục cũ thisParent.ChildItemsmột cách thủ công. Entity Framework không làm điều đó cho bạn. Cuối cùng, nó không thể quyết định những gì bạn muốn làm với các mục con cũ - nếu bạn muốn vứt chúng đi hoặc nếu bạn muốn giữ và gán chúng cho các thực thể cha mẹ khác. Bạn phải nói với Entity Framework quyết định của bạn. Nhưng một trong hai quyết định này bạn phải đưa ra vì các thực thể con không thể sống một mình mà không có tham chiếu đến bất kỳ cha mẹ nào trong cơ sở dữ liệu (do ràng buộc khóa ngoài). Về cơ bản, đó là những gì ngoại lệ nói.

Biên tập

Tôi sẽ làm gì nếu các mục con có thể được thêm, cập nhật và xóa:

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();
}

Lưu ý: Điều này không được thử nghiệm. Nó giả định rằng bộ sưu tập vật phẩm con là loại ICollection. (Tôi thường có IListvà sau đó mã trông hơi khác một chút.) Tôi cũng đã loại bỏ tất cả các tóm tắt kho lưu trữ để giữ cho nó đơn giản.

Tôi không biết liệu đó có phải là một giải pháp tốt hay không, nhưng tôi tin rằng một số công việc khó khăn dọc theo các dòng này phải được thực hiện để xử lý tất cả các loại thay đổi trong bộ sưu tập điều hướng. Tôi cũng sẽ rất vui khi thấy một cách dễ dàng hơn để làm điều đó.


Vậy nếu một số chỉ được thay đổi thì sao? Điều đó có nghĩa là tôi vẫn phải loại bỏ chúng và thêm chúng một lần nữa?
jaffa

@Jon: Không, bạn cũng có thể cập nhật các mục hiện có. Tôi đã thêm một ví dụ về cách tôi có thể cập nhật bộ sưu tập con, xem phần Chỉnh sửa ở trên.
Slauma

@Slauma: Lol, nếu tôi biết rằng bạn sẽ sửa đổi câu trả lời của mình, tôi sẽ không viết câu trả lời của mình ...
Ladislav Mrnka

@Ladislav: Không, không, tôi rất vui vì bạn đã viết câu trả lời của riêng bạn. Bây giờ ít nhất tôi biết rằng nó không hoàn toàn vô nghĩa và quá phức tạp những gì tôi đã làm ở trên.
Slauma

1
Tôi sẽ thêm một điều kiện khi truy xuất bản gốcChildItem trong foreach: ... Trong đó (c => c.ID == childItem.ID && c.ID! = 0) nếu không nó sẽ trả về những đứa trẻ mới được thêm vào nếu childItem.ID == 0.
perfect_element

116

Lý do bạn phải đối mặt với điều này là do sự khác biệt giữa thành phầntổng hợp .

Trong thành phần, đối tượng con được tạo khi cha mẹ được tạo và bị hủy khi cha mẹ của nó bị hủy . Vì vậy, cuộc sống của nó được kiểm soát bởi cha mẹ của nó. ví dụ: Một bài đăng trên blog và bình luận của nó. Nếu một bài viết bị xóa, bình luận của nó sẽ bị xóa. Không có ý nghĩa để có ý kiến ​​cho một bài viết không tồn tại. Tương tự cho các đơn đặt hàng và các mặt hàng đặt hàng.

Trong tập hợp, đối tượng con có thể tồn tại không phân biệt cha mẹ của nó . Nếu cha mẹ bị phá hủy, đối tượng con vẫn có thể tồn tại, vì nó có thể được thêm vào một cha mẹ khác sau này. ví dụ: mối quan hệ giữa danh sách phát và các bài hát trong danh sách phát đó. Nếu danh sách phát bị xóa, các bài hát không nên bị xóa. Họ có thể được thêm vào một danh sách nhạc khác.

Cách Entity Framework phân biệt các mối quan hệ tổng hợp và thành phần như sau:

  • Đối với thành phần: nó hy vọng đối tượng con có khóa chính tổng hợp (ParentID, ChildID). Điều này là do thiết kế vì ID của trẻ em phải nằm trong phạm vi của cha mẹ chúng.

  • Để tổng hợp: nó hy vọng thuộc tính khóa ngoài trong đối tượng con là null.

Vì vậy, lý do bạn gặp phải vấn đề này là do cách bạn đặt khóa chính trong bảng con. Nó nên là composite, nhưng nó không phải là. Vì vậy, Entity Framework xem liên kết này là tập hợp, có nghĩa là, khi bạn xóa hoặc xóa các đối tượng con, nó sẽ không xóa các bản ghi con. Nó chỉ đơn giản là xóa liên kết và đặt cột khóa ngoại tương ứng thành NULL (để những bản ghi con này sau đó có thể được liên kết với cha mẹ khác). Vì cột của bạn không cho phép NULL, bạn nhận được ngoại lệ bạn đã đề cập.

Các giải pháp:

1- Nếu bạn có một lý do mạnh mẽ cho việc không muốn sử dụng khóa tổng hợp, bạn cần xóa các đối tượng con một cách rõ ràng. Và điều này có thể được thực hiện đơn giản hơn các giải pháp được đề xuất trước đó:

context.Children.RemoveRange(parent.Children);

2- Mặt khác, bằng cách đặt khóa chính thích hợp trên bảng con của bạn, mã của bạn sẽ trông có ý nghĩa hơn:

parent.Children.Clear();

9
Tôi thấy lời giải thích này hữu ích nhất.
Cậu bé Booji

7
Giải thích tốt cho thành phần so với tổng hợp và cách khung thực thể liên quan đến nó.
Chrysalis

# 1 là số lượng mã ít nhất cần thiết để khắc phục sự cố. Cảm ơn bạn!
ryanulit

73

Đây là một vấn đề rất lớn. Điều thực sự xảy ra trong mã của bạn là thế này:

  • Bạn tải Parenttừ cơ sở dữ liệu và nhận được một thực thể đính kèm
  • Bạn thay thế bộ sưu tập con của nó bằng bộ sưu tập mới của trẻ em
  • Bạn lưu các thay đổi nhưng trong quá trình hoạt động này, tất cả trẻ em được coi là đã thêm vì EF không biết về chúng cho đến thời điểm này. Vì vậy, EF cố gắng đặt null thành khóa ngoại của trẻ cũ và chèn tất cả trẻ mới => hàng trùng lặp.

Bây giờ giải pháp thực sự phụ thuộc vào những gì bạn muốn làm và bạn muốn làm như thế nào?

Nếu bạn đang sử dụng ASP.NET MVC, bạn có thể thử sử dụng UpdateModel hoặc TryUpdateModel .

Nếu bạn chỉ muốn cập nhật thủ công những đứa trẻ hiện có, bạn có thể chỉ cần làm một cái gì đó như:

foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();

Đính kèm là thực sự không cần thiết (thiết lập trạng thái Modifiedcũng sẽ đính kèm thực thể) nhưng tôi thích nó vì nó làm cho quá trình rõ ràng hơn.

Nếu bạn muốn sửa đổi hiện tại, xóa hiện tại và chèn con mới, bạn phải làm một cái gì đó như:

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();

1
Nhưng có nhận xét thú vị của bạn về việc sử dụng .Clone(). Bạn có nghĩ rằng a ChildItemcó các thuộc tính điều hướng con khác không? Nhưng trong trường hợp đó, chúng ta có muốn rằng toàn bộ biểu đồ con được gắn với bối cảnh không vì chúng ta mong đợi rằng tất cả các con con là đối tượng mới nếu bản thân đứa trẻ là mới? (Chà, có thể khác nhau từ mô hình này sang mô hình khác, nhưng hãy giả sử trường hợp rằng những đứa trẻ phụ là "phụ thuộc" từ đứa trẻ giống như những đứa trẻ phụ thuộc vào cha mẹ.)
Slauma

Nó có thể sẽ yêu cầu bản sao "thông minh".
Ladislav Mrnka

1
Điều gì nếu bạn không muốn có một bộ sưu tập của Trẻ em trong bối cảnh của bạn? http://stackoverflow.com/questions/20233994/do-i-need-to-create-a-dbset-for-every-table-so-that-i-can-persist-child-entitie
Kirsten Greed

1
cha mẹ.ChildItems.Remove (con); bối cảnh.Childs.Remove (con); Loại bỏ kép này cố định có thể phát hành, CẢM ƠN. Tại sao chúng ta cần loại bỏ cả hai? Tại sao chỉ xóa khỏi cha mẹ.ChildItems không phải là khó khăn vì trẻ chỉ sống như trẻ con?
Fernando Torres

40

Tôi thấy câu trả lời này hữu ích hơn nhiều cho cùng một lỗi. Có vẻ như EF không thích nó khi bạn Xóa, nó thích Xóa.

Bạn có thể xóa một bộ sưu tập các bản ghi được đính kèm vào một bản ghi như thế này.

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);

Trong ví dụ này, tất cả các bản ghi chi tiết được đính kèm trong Đơn hàng đều được đặt Trạng thái thành Xóa. (Để chuẩn bị Thêm lại chi tiết được cập nhật, như là một phần của cập nhật Đơn hàng)


Tôi tin rằng đó là câu trả lời thích hợp.
desmati

giải pháp hợp lý và đơn giản.
sairfan

19

Tôi không biết tại sao hai câu trả lời khác lại rất phổ biến!

Tôi tin rằng bạn đã đúng khi giả định khung ORM nên xử lý nó - sau tất cả, đó là những gì nó hứa sẽ cung cấp. Nếu không, mô hình miền của bạn bị hỏng bởi những mối quan tâm dai dẳng. NHibernate quản lý điều này một cách vui vẻ nếu bạn thiết lập cài đặt tầng chính xác. Trong Entity Framework cũng có thể, họ chỉ mong bạn tuân theo các tiêu chuẩn tốt hơn khi thiết lập mô hình cơ sở dữ liệu của bạn, đặc biệt là khi họ phải suy luận nên xếp tầng nào:

Bạn phải xác định chính xác mối quan hệ cha-con bằng cách sử dụng " xác định mối quan hệ ".

Nếu bạn làm điều này, Entity Framework sẽ biết đối tượng con được xác định bởi cha mẹ và do đó nó phải là một tình huống "thác-xóa-mồ côi".

Khác với những điều trên, bạn có thể cần (từ kinh nghiệm NHibernate)

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

thay vì thay thế danh sách hoàn toàn.

CẬP NHẬT

Nhận xét của @ Slauma nhắc nhở tôi rằng các thực thể tách rời là một phần khác của vấn đề tổng thể. Để giải quyết điều đó, bạn có thể sử dụng phương pháp sử dụng một chất kết dính mô hình tùy chỉnh để xây dựng các mô hình của bạn bằng cách tải nó từ ngữ cảnh. Bài đăng trên blog này cho thấy một ví dụ về những gì tôi có ý nghĩa.


Thiết lập như xác định mối quan hệ sẽ không giúp ích ở đây vì kịch bản trong câu hỏi phải xử lý các thực thể tách rời ( "danh sách mới của tôi xuất phát từ chế độ xem MVC" ). Bạn vẫn phải tải các bản gốc từ DB, tìm các mục đã xóa trong bộ sưu tập đó dựa trên bộ sưu tập tách ra và sau đó xóa khỏi DB. Sự khác biệt duy nhất là với một mối quan hệ xác định bạn có thể gọi parent.ChildItems.Removethay vì _dbContext.ChildItems.Remove. Vẫn còn (EF <= 6) không có hỗ trợ tích hợp từ EF để tránh mã dài như mã trong các câu trả lời khác.
Slauma

Tôi hiểu quan điểm của bạn. Tuy nhiên, tôi tin rằng với một chất kết dính mô hình tùy chỉnh tải thực thể từ ngữ cảnh hoặc trả về một thể hiện mới, cách tiếp cận ở trên sẽ hoạt động. Tôi sẽ cập nhật câu trả lời của tôi để đề xuất giải pháp đó.
Andre Luus

Có, bạn có thể sử dụng một chất kết dính mô hình nhưng bạn phải làm những thứ từ các câu trả lời khác trong chất kết dính mô hình bây giờ. Nó chỉ chuyển vấn đề từ lớp repo / dịch vụ sang chất kết dính mô hình. Ít nhất, tôi không thấy một sự đơn giản hóa thực sự.
Slauma

Việc đơn giản hóa là tự động xóa các thực thể mồ côi. Tất cả những gì bạn cần trong chất kết dính mô hình là tương đương chung vớireturn context.Items.Find(id) ?? new Item()
Andre Luus

Phản hồi tốt cho nhóm EF, nhưng giải pháp đề xuất của bạn không giải quyết được bất cứ điều gì trong đất EF.
Chris Moschini

9

Nếu bạn đang sử dụng AutoMapper với Entity Framework trên cùng một lớp, bạn có thể gặp phải sự cố này. Ví dụ nếu lớp của bạn là

class A
{
    public ClassB ClassB { get; set; }
    public int ClassBId { get; set; }
}

AutoMapper.Map<A, A>(input, destination);

Điều này sẽ cố gắng sao chép cả hai thuộc tính. Trong trường hợp này, ClassBId là không Nullable. Vì AutoMapper sẽ sao chép destination.ClassB = input.ClassB;nên điều này sẽ gây ra sự cố.

Đặt AutoMapper của bạn thành Bỏ qua thuộc ClassBtính.

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId

Tôi đang gặp vấn đề tương tự với AutoMapper, nhưng điều này không hiệu quả với tôi :( Xem stackoverflow.com/q/41430679/613605
J86

4

Tôi chỉ có lỗi tương tự. Tôi có hai bảng có mối quan hệ cha mẹ, nhưng tôi đã cấu hình "tầng xóa xóa" trên cột khóa ngoại trong định nghĩa bảng của bảng con. Vì vậy, khi tôi xóa thủ công hàng cha mẹ (thông qua SQL) trong cơ sở dữ liệu, nó sẽ tự động xóa các hàng con.

Tuy nhiên, điều này không hoạt động trong EF, lỗi được mô tả trong chủ đề này xuất hiện. Lý do cho điều này là, trong mô hình dữ liệu thực thể của tôi (tệp edmx), các thuộc tính của liên kết giữa cha mẹ và bảng con là không chính xác. Các End1 OnDeletetùy chọn được cấu hình để được none( "END1" trong mô hình của tôi là kết thúc trong đó có một đa dạng của 1).

Tôi tự thay đổi End1 OnDeletetùy chọn thành Cascadevà hơn nó hoạt động. Tôi không biết tại sao EF không thể chọn cái này, khi tôi cập nhật mô hình từ cơ sở dữ liệu (tôi có mô hình cơ sở dữ liệu đầu tiên).

Để hoàn thiện, đây là cách mã của tôi để xóa trông như sau:

   public void Delete(int id)
    {
        MyType myObject = _context.MyTypes.Find(id);

        _context.MyTypes.Remove(myObject);
        _context.SaveChanges(); 
   }    

Nếu tôi đã xác định xóa tầng theo tầng, tôi sẽ phải xóa các hàng con theo cách thủ công trước khi xóa hàng cha.


4

Điều này xảy ra vì Thực thể con được đánh dấu là Đã sửa đổi thay vì Đã xóa.

Và việc sửa đổi mà EF thực hiện đối với Thực thể con khi parent.Remove(child)được thực thi, chỉ đơn giản là đặt tham chiếu cho cha mẹ của nó null.

Bạn có thể kiểm tra EntityState của trẻ bằng cách nhập mã sau vào Cửa sổ ngay lập tức của Visual Studio khi xảy ra ngoại lệ, sau khi thực hiện SaveChanges():

_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity

Trong đó X nên được thay thế bằng Thực thể đã xóa.

Nếu bạn không có quyền truy cập để ObjectContextthực thi _context.ChildEntity.Remove(child), bạn có thể giải quyết vấn đề này bằng cách đặt khóa ngoại thành một phần của khóa chính trên bảng con.

Parent
 ________________
| PK    IdParent |
|       Name     |
|________________|

Child
 ________________
| PK    IdChild  |
| PK,FK IdParent |
|       Name     |
|________________|

Bằng cách này, nếu bạn thực thi parent.Remove(child), EF sẽ đánh dấu chính xác Thực thể là Đã xóa.


2

Loại giải pháp này đã lừa tôi:

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);

Điều quan trọng để nói rằng điều này xóa tất cả các hồ sơ và chèn chúng một lần nữa. Nhưng đối với trường hợp của tôi (ít hơn 10) thì nó ok.

Tôi hy vọng nó sẽ giúp.


Việc đặt lại có xảy ra với ID mới hay nó giữ ID trẻ em mà họ có ở vị trí đầu tiên?
Pepito Fernandez

2

Tôi gặp vấn đề này ngày hôm nay và muốn chia sẻ giải pháp của tôi. Trong trường hợp của tôi, giải pháp là xóa các mục Con trước khi lấy Phụ huynh khỏi cơ sở dữ liệu.

Trước đây tôi đã làm nó như trong mã dưới đây. Sau đó tôi sẽ nhận được cùng một lỗi được liệt kê trong câu hỏi này.

var Parent = GetParent(parentId);
var children = Parent.Children;
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

Điều làm việc cho tôi, là lấy các mục con trước, sử dụng cha mẹ (khóa ngoại) và sau đó xóa các mục đó. Sau đó, tôi có thể lấy Phụ huynh từ cơ sở dữ liệu và tại thời điểm đó, nó không nên có bất kỳ mục con nào nữa và tôi có thể thêm các mục con mới.

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

2

Bạn phải xóa thủ công bộ sưu tập ChildItems và nối thêm các mục mới vào đó:

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

Sau đó, bạn có thể gọi phương thức tiện ích mở rộng DeleteOrphans sẽ xử lý với các thực thể mồ côi (nó phải được gọi giữa các phương thức DetectChanges và 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);
    }
}

Điều này làm việc tốt cho tôi. Tôi chỉ cần thêm context.DetectChanges();.
Andy Edinborough

1

Tôi đã thử các giải pháp này và nhiều giải pháp khác, nhưng không có giải pháp nào trong số đó hoàn toàn hiệu quả. Vì đây là câu trả lời đầu tiên trên google, tôi sẽ thêm giải pháp của mình vào đây.

Phương pháp làm việc tốt với tôi là đưa các mối quan hệ ra khỏi bức tranh trong các lần cam kết, vì vậy không có gì để EF làm hỏng việc. Tôi đã làm điều này bằng cách tìm lại đối tượng cha trong DBContext và xóa nó. Vì các thuộc tính điều hướng của đối tượng được tìm thấy lại đều là null, nên các mối quan hệ của trẻ em bị bỏ qua trong quá trình xác nhận.

var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();

Lưu ý rằng điều này giả sử các khóa ngoại được thiết lập với ON DELETE CASCADE, vì vậy khi hàng cha mẹ bị xóa, các con sẽ được cơ sở dữ liệu dọn sạch.


1

Tôi đã sử dụng giải pháp của Mosh , nhưng trước tiên tôi không rõ cách triển khai khóa thành phần chính xác trong mã.

Vì vậy, đây là giải pháp:

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; }
}

1

Tôi có vấn đề tương tự, nhưng tôi biết nó đã hoạt động tốt trong các trường hợp khác, vì vậy tôi đã giảm bớt vấn đề này:

parent.OtherRelatedItems.Clear();  //this worked OK on SaveChanges() - items were being deleted from DB
parent.ProblematicItems.Clear();   // this was causing the mentioned exception on SaveChanges()
  • OtherRelatedItems có Khóa chính tổng hợp (ParentId + một số cột cục bộ) và hoạt động tốt
  • ProblematicItems có Khóa chính một cột của riêng họ và ParentId chỉ là một FK. Điều này đã gây ra ngoại lệ sau Clear ().

Tất cả những gì tôi phải làm là biến ParentId thành một phần của PK tổng hợp để chỉ ra rằng những đứa trẻ không thể tồn tại mà không có cha mẹ. Tôi đã sử dụng mô hình DB-First, thêm PK và đánh dấu cột ParentId là EntityKey (vì vậy, tôi phải cập nhật cả hai trong DB và EF - không chắc liệu một mình EF có đủ không).

Tôi đã thực hiện RequestId một phần của PK Và sau đó cập nhật mô hình EF, VÀ đặt thuộc tính khác làm một phần của Khóa thực thể

Khi bạn nghĩ về nó, đó là một sự khác biệt rất tao nhã mà EF sử dụng để quyết định xem trẻ em có "hợp lý" không có cha mẹ hay không (trong trường hợp này là Clear () sẽ không xóa chúng và ném ngoại lệ trừ khi bạn đặt ParentId thành một thứ khác / đặc biệt ), hoặc - giống như trong câu hỏi ban đầu - chúng tôi hy vọng các mục sẽ bị xóa sau khi chúng bị xóa khỏi cha mẹ.


0

Vấn đề này phát sinh do chúng tôi cố gắng xóa bảng cha vẫn còn dữ liệu bảng con. Chúng tôi giải quyết vấn đề với sự giúp đỡ của xóa tầng.

Trong mô hình Tạo phương thức trong lớp 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);

Sau đó, trong Cuộc gọi API của chúng tôi

var JobList = Context.Job                       
          .Include(x => x.JobSportsMappings)                                     .ToList();
Context.Job.RemoveRange(JobList);
Context.SaveChanges();

Cascade delete tùy chọn xóa phụ huynh cũng mẹ liên quan đến bảng con với mã đơn giản này. Làm cho nó thử theo cách đơn giản này.

Xóa Phạm vi được sử dụng để xóa danh sách các bản ghi trong cơ sở dữ liệu Cảm ơn


0

Tôi cũng đã giải quyết vấn đề của mình bằng câu trả lời của Mosh và tôi nghĩ câu trả lời của PeterB là một chút vì nó đã sử dụng một enum làm khóa ngoại. Hãy nhớ rằng bạn sẽ cần thêm một di chuyển mới sau khi thêm mã này.

Tôi cũng có thể giới thiệu bài viết trên blog này cho các giải pháp khác:

http://www.kianryan.co.uk/2013/03/orphaned-child/

Mã số:

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; }
}

0

Sử dụng giải pháp của Slauma, tôi đã tạo ra một số hàm chung để giúp cập nhật các đối tượng con và bộ sưu tập các đối tượng con.

Tất cả các đối tượng liên tục của tôi thực hiện giao diện này

/// <summary>
/// Base interface for all persisted entries
/// </summary>
public interface IBase
{
    /// <summary>
    /// The Id
    /// </summary>
    int Id { get; set; }
}

Với điều này, tôi đã triển khai hai chức năng này trong Kho lưu trữ của mình

    /// <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);
            }
        }
    }

Để sử dụng nó tôi làm như sau:

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);

Hi vọng điêu nay co ich


EXTRA: Bạn cũng có thể tạo một lớp DbContextExtentions (hoặc bối cảnh của riêng bạn):

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);
            }
        }
    }
}

và sử dụng nó như:

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);

Bạn cũng có thể tạo một lớp mở rộng cho ngữ cảnh của mình bằng các chức năng sau:
Bluemoon74

0

Tôi đã gặp phải vấn đề tương tự khi tôi sẽ xóa bản ghi của mình hơn một số vấn đề đã xảy ra, vì giải pháp cho vấn đề này là khi bạn sẽ xóa bản ghi của mình hơn là bạn thiếu một số điều trước khi xóa tiêu đề / bản ghi chính, bạn phải ghi vào mã cho xóa chi tiết của nó trước tiêu đề / Master Tôi hy vọng vấn đề của bạn sẽ được giải quyết.


-1

Tôi đã gặp vấn đề này trước vài giờ và thử mọi thứ, nhưng trong trường hợp của tôi, giải pháp khác với danh sách trên.

Nếu bạn sử dụng thực thể đã được truy xuất từ ​​cơ sở dữ liệu và cố gắng sửa đổi thì đó là lỗi trẻ em sẽ xảy ra, nhưng nếu bạn nhận được bản sao mới của thực thể từ cơ sở dữ liệu thì sẽ không có vấn đề gì. Không sử dụng cái này:

 public void CheckUsersCount(CompanyProduct companyProduct) 
 {
     companyProduct.Name = "Test";
 }

Dùng cái này:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.