Làm cách nào để ngăn Entity Framework cố gắng lưu / chèn các đối tượng con?


100

Khi tôi lưu một thực thể với khung thực thể, tôi tự nhiên cho rằng nó sẽ chỉ cố gắng lưu thực thể được chỉ định. Tuy nhiên, nó cũng đang cố gắng cứu các thực thể con của thực thể đó. Điều này gây ra tất cả các loại vấn đề về tính toàn vẹn. Làm cách nào để buộc EF chỉ lưu đối tượng tôi muốn lưu và do đó bỏ qua tất cả các đối tượng con?

Nếu tôi đặt các thuộc tính thành null theo cách thủ công, tôi sẽ gặp lỗi "Thao tác không thành công: 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ể null." Điều này cực kỳ phản tác dụng vì tôi đặt đối tượng con thành null cụ thể nên EF sẽ để nó yên.

Tại sao tôi không muốn lưu / chèn các đối tượng con?

Vì điều này đang được thảo luận qua lại trong các phần bình luận, nên tôi sẽ đưa ra một số lý do giải thích tại sao tôi muốn các đồ vật con của mình được để yên.

Trong ứng dụng mà tôi đang xây dựng, mô hình đối tượng EF không được tải từ cơ sở dữ liệu mà được sử dụng như các đối tượng dữ liệu mà tôi đang điền trong khi phân tích cú pháp một tệp phẳng. Trong trường hợp của các đối tượng con, nhiều trong số này đề cập đến các bảng tra cứu xác định các thuộc tính khác nhau của bảng mẹ. Ví dụ: vị trí địa lý của thực thể chính.

Vì tôi đã tự điền các đối tượng này nên EF giả định đây là các đối tượng mới và cần được chèn cùng với đối tượng mẹ. Tuy nhiên, những định nghĩa này đã tồn tại và tôi không muốn tạo bản sao trong cơ sở dữ liệu. Tôi chỉ sử dụng đối tượng EF để thực hiện tra cứu và điền khóa ngoại vào thực thể bảng chính của mình.

Ngay cả với các đối tượng con là dữ liệu thực, trước tiên tôi cần phải lưu đối tượng chính và lấy khóa chính, nếu không EF dường như chỉ tạo ra một mớ hỗn độn. Hy vọng điều này đưa ra một số lời giải thích.


Theo như tôi biết, bạn sẽ phải vô hiệu hóa các đối tượng con.
Johan

Chào Johan. Không hoạt động. Nó ném lỗi nếu tôi làm trống bộ sưu tập. Tùy thuộc vào cách tôi làm điều đó, nó phàn nàn về việc các khóa bị rỗng hoặc bộ sưu tập của tôi đã bị sửa đổi. Rõ ràng, những điều đó là sự thật, nhưng tôi đã cố tình làm điều đó nên nó sẽ để yên những đồ vật mà nó không được phép chạm vào.
Mark Micallef

Hưng phấn, điều đó hoàn toàn vô ích.
Mark Micallef

@Euphoric Ngay cả khi không thay đổi các đối tượng con, EF vẫn cố gắng chèn chúng theo mặc định và không bỏ qua hoặc cập nhật chúng.
Johan

Điều thực sự làm tôi khó chịu là nếu tôi cố gắng thực sự vô hiệu hóa các đối tượng đó, thì nó sẽ phàn nàn hơn là nhận ra rằng tôi muốn nó chỉ để chúng yên. Vì các đối tượng con đó đều là tùy chọn (có thể vô hiệu trong cơ sở dữ liệu), có cách nào để buộc EF quên rằng tôi đã có các đối tượng đó không? tức là xóa ngữ cảnh của nó hoặc bộ nhớ cache bằng cách nào đó?
Mark Micallef

Câu trả lời:


55

Theo như tôi biết, bạn có hai lựa chọn.

Lựa chọn 1)

Làm trống tất cả các đối tượng con, điều này sẽ đảm bảo EF không thêm bất cứ thứ gì. Nó cũng sẽ không xóa bất kỳ thứ gì khỏi cơ sở dữ liệu của bạn.

Lựa chọn 2)

Đặt các đối tượng con là tách rời khỏi ngữ cảnh bằng cách sử dụng mã sau

 context.Entry(yourObject).State = EntityState.Detached

Lưu ý rằng bạn không thể tách a List/ Collection. Bạn sẽ phải lặp lại danh sách của mình và tách từng mục trong danh sách của bạn như vậy

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}

Xin chào Johan, tôi đã thử phát hiện một trong các bộ sưu tập và nó gặp lỗi sau: Loại thực thể HashSet`1 không phải là một phần của mô hình cho ngữ cảnh hiện tại.
Mark Micallef

@MarkyMark Không tách bộ sưu tập. Bạn sẽ phải lặp lại bộ sưu tập và tách đối tượng của nó cho đối tượng (Tôi sẽ cập nhật câu trả lời của mình ngay bây giờ).
Johan

11
Thật không may, ngay cả với danh sách các vòng lặp để phát hiện mọi thứ, EF dường như vẫn đang cố gắng chèn vào một số bảng có liên quan. Tại thời điểm này, tôi đã sẵn sàng tách EF ra và chuyển trở lại SQL mà ít nhất là hoạt động hợp lý. Thật là một nỗi đau.
Mark Micallef

2
Tôi có thể sử dụng context.Entry (yourObject) .State trước khi thêm nó không?
Thomas Klammer

1
@ mirind4 Tôi không sử dụng trạng thái. Nếu tôi chèn một đối tượng, tôi đảm bảo rằng tất cả các phần tử con đều rỗng. Trên bản cập nhật, tôi nhận được đối tượng đầu tiên mà không có con.
Thomas Klammer

41

Câu chuyện ngắn: Sử dụng chìa khóa Ngoại và nó sẽ tiết kiệm một ngày của bạn.

Giả sử bạn có thực thể Trường học và thực thể Thành phố , và đây là mối quan hệ nhiều đối một trong đó Thành phố có nhiều Trường học và Trường thuộc về Thành phố. Và giả sử các Thành phố đã tồn tại trong bảng tra cứu, vì vậy bạn KHÔNG muốn chúng được chèn lại khi chèn trường mới.

Ban đầu, bạn có thể xác định cho mình các thực thể như sau:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

Và bạn có thể thực hiện việc chèn Trường học như thế này (giả sử bạn đã gán thuộc tính Thành phố cho Vật tổ mới ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

Cách tiếp cận trên có thể hoạt động hoàn hảo trong trường hợp này, tuy nhiên, tôi thích cách tiếp cận Khóa ngoại mà đối với tôi là rõ ràng và linh hoạt hơn. Xem giải pháp cập nhật bên dưới:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

Bằng cách này, bạn xác định rõ ràng rằng Trường có khóa ngoài City_Id và nó tham chiếu đến thực thể Thành phố . Vì vậy, khi nói đến việc chèn Trường học , bạn có thể làm:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

Trong trường hợp này, bạn chỉ định rõ ràng City_Id của bản ghi mới và xóa Thành phố khỏi biểu đồ để EF không bận tâm thêm nó vào ngữ cảnh cùng với Trường học .

Mặc dù ở ấn tượng đầu tiên, cách tiếp cận Khóa ngoại có vẻ phức tạp hơn, nhưng hãy tin tôi rằng tâm lý này sẽ giúp bạn tiết kiệm rất nhiều thời gian khi nói đến việc chèn mối quan hệ nhiều-nhiều (hình dung bạn có mối quan hệ Trường học và Sinh viên, và Sinh viên có tài sản Thành phố) và như vậy.

Hy vọng điều này là hữu ích cho bạn.


Câu trả lời tuyệt vời, đã giúp tôi rất nhiều! Nhưng sẽ không tốt hơn nếu nêu giá trị nhỏ nhất là 1 cho City_Id bằng cách sử dụng [Range(1, int.MaxValue)]thuộc tính?
Dan Rayson

Như một giấc mơ!! Cảm ơn rất nhiều!
CJH

Điều này sẽ xóa giá trị khỏi đối tượng Trường trong trình gọi. SaveChanges có tải lại giá trị của các thuộc tính điều hướng null không? Nếu không, người gọi nên tải lại đối tượng City sau khi gọi phương thức Insert () nếu nó cần thông tin đó. Đây là mẫu tôi đã sử dụng thường xuyên, nhưng tôi vẫn để ngỏ những mẫu đẹp hơn nếu ai có cái tốt.
Lopsided

1
Điều này thực sự hữu ích đối với tôi. EntityState.Unchaged là chính xác những gì tôi cần để gán một đối tượng đại diện cho một khóa ngoại của bảng tra cứu trong một biểu đồ đối tượng lớn mà tôi đã lưu dưới dạng một giao dịch duy nhất. EF Core đưa ra thông báo lỗi IMO ít trực quan hơn. Tôi đã lưu vào bộ nhớ cache các bảng tra cứu của mình mà hiếm khi thay đổi vì lý do hiệu suất. Bạn đang giả định là như vậy, bởi vì PK của đối tượng bảng tra cứu giống với những gì trong cơ sở dữ liệu đã biết rằng nó không thay đổi và chỉ gán FK cho mục hiện có. Thay vì cố gắng chèn đối tượng bảng tra cứu như mới.
tnk479 19/09/19

Không có sự kết hợp nào của việc làm trống hoặc đặt trạng thái thành không thay đổi đang hoạt động đối với tôi. EF khẳng định nó cần phải chèn một bản ghi con mới khi tôi chỉ muốn cập nhật trường vô hướng trong trường cha.
BobRz

21

Nếu bạn chỉ muốn lưu trữ các thay đổi đối với một đối tượng mẹ và tránh lưu trữ các thay đổi đối với bất kỳ đối tượng con , thì tại sao không chỉ cần làm như sau:

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

Dòng đầu tiên gắn đối tượng cha và toàn bộ đồ thị của các đối tượng con phụ thuộc của nó vào ngữ cảnh ở Unchangedtrạng thái.

Dòng thứ hai chỉ thay đổi trạng thái cho đối tượng cha, để lại Unchangedtrạng thái con của nó .

Lưu ý rằng tôi sử dụng ngữ cảnh mới được tạo, vì vậy điều này tránh lưu bất kỳ thay đổi nào khác vào cơ sở dữ liệu.


2
Câu trả lời này có lợi thế là nếu ai đó đến sau và thêm một đối tượng con, nó sẽ không phá vỡ mã hiện có. Đây là một giải pháp "chọn tham gia" trong đó các giải pháp khác yêu cầu bạn loại trừ các đối tượng con một cách rõ ràng.
Jim

Điều này không còn hoạt động trong lõi. "Đính kèm: Đính kèm mọi đối tượng có thể truy cập, ngoại trừ trường hợp đối tượng có thể truy cập có khóa do cửa hàng tạo và không có giá trị khóa nào được chỉ định; chúng sẽ được đánh dấu là đã thêm." Nếu trẻ em là người mới cũng sẽ được thêm vào.
mmix

14

Một trong những giải pháp được đề xuất là gán thuộc tính điều hướng từ cùng ngữ cảnh cơ sở dữ liệu. Trong giải pháp này, thuộc tính điều hướng được gán từ bên ngoài ngữ cảnh cơ sở dữ liệu sẽ được thay thế. Vui lòng xem ví dụ sau để minh họa.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Lưu vào cơ sở dữ liệu:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft sử dụng tính năng này như một tính năng, tuy nhiên tôi thấy điều này thật khó chịu. Nếu đối tượng phòng ban được liên kết với đối tượng công ty có Id đã tồn tại trong cơ sở dữ liệu, thì tại sao EF không kết hợp đối tượng công ty với đối tượng cơ sở dữ liệu? Tại sao chúng ta cần phải tự mình chăm sóc hội? Việc chăm sóc thuộc tính điều hướng trong quá trình thêm đối tượng mới giống như chuyển các hoạt động cơ sở dữ liệu từ SQL sang C #, gây khó khăn cho các nhà phát triển.


Tôi đồng ý 100% thật là nực cười khi EF cố gắng tạo một bản ghi mới khi ID được cung cấp. Nếu có một cách để điền vào các tùy chọn danh sách đã chọn với đối tượng thực tế mà chúng tôi sẽ kinh doanh.
T3.0

10

Trước tiên, bạn cần biết rằng có hai cách để cập nhật thực thể trong EF.

  • Đối tượng đính kèm

Khi bạn thay đổi mối quan hệ của các đối tượng được gắn với ngữ cảnh đối tượng bằng cách sử dụng một trong các phương pháp được mô tả ở trên, Khung thực thể cần giữ đồng bộ khóa ngoại, tham chiếu và tập hợp.

  • Các đối tượng bị ngắt kết nối

Nếu bạn đang làm việc với các đối tượng bị ngắt kết nối, bạn phải quản lý việc đồng bộ hóa theo cách thủ công.

Trong ứng dụng mà tôi đang xây dựng, mô hình đối tượng EF không được tải từ cơ sở dữ liệu mà được sử dụng như các đối tượng dữ liệu mà tôi đang điền trong khi phân tích cú pháp một tệp phẳng.

Điều đó có nghĩa là bạn đang làm việc với đối tượng bị ngắt kết nối, nhưng không rõ bạn đang sử dụng liên kết độc lập hay liên kết khóa ngoại .

  • Thêm vào

    Khi thêm thực thể mới với đối tượng con hiện có (đối tượng tồn tại trong cơ sở dữ liệu), nếu đối tượng con không được EF theo dõi, đối tượng con sẽ được chèn lại. Trừ khi bạn đính kèm thủ công đối tượng con trước.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Cập nhật

    Bạn chỉ có thể đánh dấu đối tượng là đã sửa đổi, sau đó tất cả các thuộc tính vô hướng sẽ được cập nhật và các thuộc tính điều hướng sẽ đơn giản bị bỏ qua.

      db.Entity(entity).State = EntityState.Modified;

Đồ thị khác biệt

Nếu bạn muốn đơn giản hóa mã khi làm việc với đối tượng bị ngắt kết nối, bạn có thể thử vẽ biểu đồ thư viện khác biệt .

Đây là phần giới thiệu, Giới thiệu GraphDiff for Entity Framework Code First - Cho phép cập nhật tự động biểu đồ của các thực thể tách rời .

Mã mẫu

  • Chèn thực thể nếu nó không tồn tại, nếu không, hãy cập nhật.

      db.UpdateGraph(entity);
  • Chèn thực thể nếu nó không tồn tại, nếu không thì cập nhật chèn đối tượng con nếu nó không tồn tại, nếu không thì cập nhật.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));

3

Cách tốt nhất để làm điều này là ghi đè hàm SaveChanges trong văn bản dữ liệu của bạn.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }

2

Tôi đã gặp phải vấn đề tương tự khi cố gắng lưu hồ sơ, tôi đã chào bàn và mới tạo hồ sơ. Khi tôi chèn hồ sơ, nó cũng chèn vào lời chào. Vì vậy, tôi đã thử như thế này trước khi savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;


1

Điều này đã làm việc cho tôi:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;

0

Những gì chúng tôi đã làm là trước khi thêm cha mẹ vào dbset, ngắt kết nối các tập hợp con khỏi cha mẹ, đảm bảo đẩy các tập hợp hiện có sang các biến khác để cho phép làm việc với chúng sau này, và sau đó thay thế các tập hợp con hiện tại bằng các tập hợp trống mới. Đặt các tập hợp con thành null / dường như không có gì thất bại đối với chúng tôi. Sau khi làm điều đó, sau đó thêm cha mẹ vào dbset. Bằng cách này, những đứa trẻ sẽ không được thêm vào cho đến khi bạn muốn chúng.


0

Tôi biết đó là bài viết cũ nhưng nếu bạn đang sử dụng phương pháp tiếp cận mã đầu tiên, bạn có thể đạt được kết quả mong muốn bằng cách sử dụng mã sau trong tệp ánh xạ của bạn.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Về cơ bản, điều này sẽ yêu cầu EF loại trừ thuộc tính "ChildObjectOrCollection" khỏi mô hình để nó không được ánh xạ tới cơ sở dữ liệu.


"Bỏ qua" được sử dụng trong ngữ cảnh nào? Nó dường như không tồn tại trong bối cảnh giả định.
T3.0

0

Tôi đã gặp một thử thách tương tự khi sử dụng Entity Framework Core 3.1.0, logic repo của tôi khá chung chung.

Điều này đã làm việc cho tôi:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Xin lưu ý "ParentEntityId" là tên cột khóa ngoại trên thực thể con. Tôi đã thêm dòng mã được đề cập ở trên vào phương pháp này:

protected override void OnModelCreating(ModelBuilder builder)...
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.