Cách thêm / cập nhật thực thể con khi cập nhật thực thể cha mẹ trong EF


151

Hai thực thể là mối quan hệ một-nhiều (được xây dựng bằng mã api lưu loát đầu tiên).

public class Parent
{
    public Parent()
    {
        this.Children = new List<Child>();
    }

    public int Id { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int Id { get; set; }

    public int ParentId { get; set; }

    public string Data { get; set; }
}

Trong bộ điều khiển WebApi của tôi, tôi có các hành động để tạo một thực thể mẹ (đang hoạt động tốt) và cập nhật một thực thể cha mẹ (có một số vấn đề). Hành động cập nhật trông như sau:

public void Update(UpdateParentModel model)
{
    //what should be done here?
}

Hiện tại tôi có hai ý tưởng:

  1. Nhận một thực thể cha được theo dõi được đặt tên existingtheo model.Idvà gán các giá trị modeltừng cái một cho thực thể. Điều này nghe có vẻ ngu ngốc. Và trong model.Childrentôi không biết đứa trẻ nào mới, đứa trẻ nào được sửa đổi (hoặc thậm chí bị xóa).

  2. Tạo một thực thể cha mẹ mới thông qua modelvà đính kèm nó vào DbContext và lưu nó. Nhưng làm thế nào để DbContext có thể biết trạng thái của trẻ em (thêm / xóa / sửa đổi mới)?

Cách chính xác để thực hiện tính năng này là gì?


Xem thêm ví dụ với GraphDiff trong một câu hỏi trùng lặp stackoverflow.com/questions/29351401/
triệt

Câu trả lời:


219

Vì mô hình được đăng lên bộ điều khiển WebApi được tách ra khỏi bất kỳ bối cảnh khung thực thể (EF) nào, nên tùy chọn duy nhất là tải biểu đồ đối tượng (cha mẹ bao gồm cả con của nó) từ cơ sở dữ liệu và so sánh con nào đã được thêm, xóa hoặc cập nhật. (Trừ khi bạn sẽ theo dõi các thay đổi với cơ chế theo dõi của riêng mình trong trạng thái tách rời (trong trình duyệt hoặc bất cứ nơi nào) mà theo tôi thì phức tạp hơn những điều sau đây.) Nó có thể như thế này:

public void Update(UpdateParentModel model)
{
    var existingParent = _dbContext.Parents
        .Where(p => p.Id == model.Id)
        .Include(p => p.Children)
        .SingleOrDefault();

    if (existingParent != null)
    {
        // Update parent
        _dbContext.Entry(existingParent).CurrentValues.SetValues(model);

        // Delete children
        foreach (var existingChild in existingParent.Children.ToList())
        {
            if (!model.Children.Any(c => c.Id == existingChild.Id))
                _dbContext.Children.Remove(existingChild);
        }

        // Update and Insert children
        foreach (var childModel in model.Children)
        {
            var existingChild = existingParent.Children
                .Where(c => c.Id == childModel.Id)
                .SingleOrDefault();

            if (existingChild != null)
                // Update child
                _dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
            else
            {
                // Insert child
                var newChild = new Child
                {
                    Data = childModel.Data,
                    //...
                };
                existingParent.Children.Add(newChild);
            }
        }

        _dbContext.SaveChanges();
    }
}

...CurrentValues.SetValuescó thể lấy bất kỳ đối tượng và ánh xạ giá trị thuộc tính nào cho thực thể đính kèm dựa trên tên thuộc tính. Nếu tên thuộc tính trong mô hình của bạn khác với tên trong thực thể, bạn không thể sử dụng phương thức này và phải gán từng giá trị một.


35
Nhưng tại sao ef không có cách "xuất sắc" hơn? Tôi nghĩ rằng ef có thể phát hiện nếu đứa trẻ bị sửa đổi / xóa / thêm, IMO mã của bạn ở trên có thể là một phần của khung EF và trở thành một giải pháp chung hơn.
Cheng Chen

7
@DannyChen: Đây thực sự là một yêu cầu lâu dài rằng việc cập nhật các thực thể bị ngắt kết nối nên được hỗ trợ bởi EF theo cách thoải mái hơn ( entityframework.codeplex.com/workitem/864 ) nhưng nó vẫn không phải là một phần của khung. Hiện tại, bạn chỉ có thể thử lib "GraphDiff" của bên thứ ba được đề cập trong văn bản mã hóa đó hoặc viết mã thủ công như trong câu trả lời của tôi ở trên.
Slauma

7
Một điều cần thêm: Trong phần giới thiệu về cập nhật và chèn con, bạn không thể thực hiện existingParent.Children.Add(newChild)vì sau đó tìm kiếm linq hiện tại sẽ trả về thực thể được thêm gần đây và do đó thực thể đó sẽ được cập nhật. Bạn chỉ cần chèn vào một danh sách tạm thời và sau đó thêm.
Erre Efe

3
@ RandolfRincónFadul Tôi chỉ gặp vấn đề này. Cách khắc phục của tôi, một nỗ lực ít hơn một chút là thay đổi mệnh đề where trong existingChildtruy vấn LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward

2
@RalphWillgoss Cách khắc phục trong 2.2 mà bạn đang nói đến?
Jan Paolo Đi

11

Tôi đã loay hoay với thứ gì đó như thế này ...

protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
    {
        var dbItems = selector(dbItem).ToList();
        var newItems = selector(newItem).ToList();

        if (dbItems == null && newItems == null)
            return;

        var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
        var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();

        var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
        var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();

        var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
        toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));

        var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
        toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
    }

mà bạn có thể gọi với một cái gì đó như:

UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)

Thật không may, loại này rơi xuống nếu có các thuộc tính bộ sưu tập trên loại con cũng cần được cập nhật. Xem xét việc cố gắng giải quyết vấn đề này bằng cách thông qua một IRep repository (với các phương thức CRUD cơ bản) có thể tự chịu trách nhiệm gọi UpdateChildCollection. Sẽ gọi repo thay vì gọi trực tiếp đến DbContext.Entry.

Không biết làm thế nào tất cả sẽ thực hiện ở quy mô, nhưng không chắc chắn những gì khác để làm với vấn đề này.


1
Giải pháp tuyệt vời! Nhưng không thành công nếu thêm nhiều mục mới, từ điển cập nhật không thể có id hai lần. Cần một số công việc arround. Và cũng thất bại nếu mối quan hệ là N -> N, trên thực tế, mục được thêm vào cơ sở dữ liệu, nhưng bảng N -> N không được sửa đổi.
RenanStr

1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));nên giải bài toán n -> n.
RenanStr

10

Được rồi các chàng trai. Tôi đã có câu trả lời này một lần nhưng mất nó trên đường đi. tra tấn tuyệt đối khi bạn biết có một cách tốt hơn nhưng không thể nhớ hoặc tìm thấy nó! Nó rất đơn giản. Tôi chỉ thử nó nhiều cách.

var parent = _dbContext.Parents
  .Where(p => p.Id == model.Id)
  .Include(p => p.Children)
  .FirstOrDefault();

parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;

_dbContext.SaveChanges();

Bạn có thể thay thế toàn bộ danh sách bằng một cái mới! Mã SQL sẽ loại bỏ và thêm các thực thể khi cần thiết. Không cần phải quan tâm đến điều đó. Hãy chắc chắn bao gồm bộ sưu tập trẻ em hoặc không có xúc xắc. Chúc may mắn!


Chỉ cần những gì tôi cần, vì số lượng trẻ em trong mô hình của tôi thường khá ít, vì vậy, giả sử Linq sẽ xóa tất cả trẻ em ban đầu khỏi bảng và sau đó thêm tất cả những cái mới, tác động hiệu suất không phải là vấn đề.
William T. Mallard

@ Charles McIntosh. Tôi không hủy bỏ lý do tại sao bạn đặt lại Trẻ em trong khi bạn Bao gồm nó trong truy vấn ban đầu?
pantonis

1
@pantonis Tôi bao gồm bộ sưu tập con để có thể tải nó để chỉnh sửa. Nếu tôi dựa vào tải lười biếng để tìm ra nó thì nó không hoạt động. Tôi đặt con (một lần) vì thay vì xóa thủ công và thêm các mục vào bộ sưu tập, tôi có thể chỉ cần thay thế danh sách và thực thể sẽ thêm và xóa các mục cho tôi. Điều quan trọng là thiết lập trạng thái của thực thể thành sửa đổi và cho phép thực thể thực hiện công việc nặng.
Charles McIntosh

@CharlesMcIntosh Tôi vẫn không hiểu những gì bạn đang cố gắng đạt được với những đứa trẻ ở đó. Bạn đã đưa nó vào yêu cầu đầu tiên (Bao gồm (p => p.Trẻ em). Tại sao bạn lại yêu cầu nó?
pantonis

@pantonis, tôi đã phải kéo danh sách cũ bằng cách sử dụng .include () để nó được tải và đính kèm dưới dạng một bộ sưu tập từ cơ sở dữ liệu. Đó là cách lười tải được gọi. không có nó, mọi thay đổi trong danh sách sẽ không được theo dõi khi tôi sử dụng entitystate.modified. để nhắc lại, những gì tôi đang làm là đặt bộ sưu tập con hiện tại thành một bộ sưu tập con khác. giống như nếu một người quản lý có một loạt các nhân viên mới hoặc mất một vài. Tôi sẽ sử dụng một truy vấn để bao gồm hoặc loại trừ những nhân viên mới đó và chỉ cần thay thế danh sách cũ bằng một danh sách mới sau đó cho phép EF thêm hoặc xóa khi cần từ phía cơ sở dữ liệu.
Charles McIntosh

9

Nếu bạn đang sử dụng EntityFrameworkCore, bạn có thể thực hiện các thao tác sau trong hành động đăng bài điều khiển của mình ( Phương thức Đính kèm theo cách đệ quy các thuộc tính điều hướng bao gồm các bộ sưu tập):

_context.Attach(modelPostedToController);

IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);

foreach(EntityEntry ee in unchangedEntities){
     ee.State = EntityState.Modified;
}

await _context.SaveChangesAsync();

Giả định rằng mỗi thực thể được cập nhật có tất cả các thuộc tính được đặt và được cung cấp trong dữ liệu bài đăng từ máy khách (ví dụ: sẽ không hoạt động để cập nhật một phần thực thể).

Bạn cũng cần đảm bảo rằng bạn đang sử dụng bối cảnh cơ sở dữ liệu khung thực thể mới / dành riêng cho thao tác này.


5
public async Task<IHttpActionResult> PutParent(int id, Parent parent)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != parent.Id)
            {
                return BadRequest();
            }

            db.Entry(parent).State = EntityState.Modified;

            foreach (Child child in parent.Children)
            {
                db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
            }

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ParentExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Ok(db.Parents.Find(id));
        }

Đây là cách tôi giải quyết vấn đề này. Bằng cách này, EF biết nên thêm cái nào để cập nhật.


Làm việc như người ở! Cảm ơn.
Inktkiller

2

Có một vài dự án ngoài kia làm cho sự tương tác giữa máy khách và máy chủ trở nên dễ dàng hơn khi nó liên quan đến việc lưu toàn bộ biểu đồ đối tượng.

Đây là hai bạn muốn xem xét:

Cả hai dự án trên đều nhận ra các thực thể bị ngắt kết nối khi nó được trả về máy chủ, phát hiện và lưu các thay đổi và trả về dữ liệu bị ảnh hưởng của máy khách.


1

Chỉ cần bằng chứng về khái niệm Controler.UpdateModel sẽ không hoạt động chính xác.

Lớp đầy đủ ở đây :

const string PK = "Id";
protected Models.Entities con;
protected System.Data.Entity.DbSet<T> model;

private void TestUpdate(object item)
{
    var props = item.GetType().GetProperties();
    foreach (var prop in props)
    {
        object value = prop.GetValue(item);
        if (prop.PropertyType.IsInterface && value != null)
        {
            foreach (var iItem in (System.Collections.IEnumerable)value)
            {
                TestUpdate(iItem);
            }
        }
    }

    int id = (int)item.GetType().GetProperty(PK).GetValue(item);
    if (id == 0)
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Added;
    }
    else
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Modified;
    }

}

0

@Charles McIntosh thực sự đã cho tôi câu trả lời cho tình huống của tôi trong đó mô hình được thông qua đã bị tách ra. Đối với tôi, điều cuối cùng đã làm là tiết kiệm được thông qua mô hình trước tiên ... sau đó tiếp tục thêm các con như tôi đã có trước đây:

public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
{
    db.Entry(order).State = EntityState.Modified;
    db.SaveChanges();
  ...
}

0

Đối với nhà phát triển VB.NET Sử dụng phụ chung này để đánh dấu trạng thái con, dễ sử dụng

Ghi chú:

  • PromatCon: đối tượng thực thể
  • amList: là danh sách con mà bạn muốn thêm hoặc sửa đổi
  • rList: là danh sách con mà bạn muốn xóa
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
        If amList IsNot Nothing Then
            For Each obj In amList
                Dim x = PromatCon.Entry(obj).GetDatabaseValues()
                If x Is Nothing Then
                    PromatCon.Entry(obj).State = EntityState.Added
                Else
                    PromatCon.Entry(obj).State = EntityState.Modified
                End If
            Next
        End If

        If rList IsNot Nothing Then
            For Each obj In rList.ToList
                PromatCon.Entry(obj).State = EntityState.Deleted
            Next
        End If
End Sub
PromatCon.SaveChanges()

0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

nguồn


0

Đây là mã của tôi hoạt động tốt.

public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
        decimal motohours, int driverID, List<int> commission,
        string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
        bool isTransportation, string violationConditions, DateTime shutdownStartTime,
        DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
        {
            try
            {
                using (var db = new GJobEntities())
                {
                    var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);

                    if (isExisting != null)
                    {
                        isExisting.AreaID = areaID;
                        isExisting.DriverID = driverID;
                        isExisting.IsTransportation = isTransportation;
                        isExisting.Mileage = mileage;
                        isExisting.Motohours = motohours;
                        isExisting.Notes = notes;                    
                        isExisting.DeviceShutdownDesc = deviceShutdownDesc;
                        isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
                        isExisting.ShutdownAtTime = shutdownAtTime;
                        isExisting.ShutdownEndTime = shutdownEndTime;
                        isExisting.ShutdownStartTime = shutdownStartTime;
                        isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
                        isExisting.ViolationConditions = violationConditions;

                        // Delete children
                        foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
                        {
                            db.DeviceShutdownFaults.Remove(existingChild);
                        }

                        if (faultIDs != null && faultIDs.Any())
                        {
                            foreach (var faultItem in faultIDs)
                            {
                                var newChild = new DeviceShutdownFault
                                {
                                    ID = Guid.NewGuid(),
                                    DDFaultID = faultItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownFaults.Add(newChild);
                            }
                        }

                        // Delete all children
                        foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
                        {
                            db.DeviceShutdownComissions.Remove(existingChild);
                        }

                        // Add all new children
                        if (commission != null && commission.Any())
                        {
                            foreach (var cItem in commission)
                            {
                                var newChild = new DeviceShutdownComission
                                {
                                    ID = Guid.NewGuid(),
                                    PersonalID = cItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownComissions.Add(newChild);
                            }
                        }

                        await db.SaveChangesAsync();

                        return true;
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex);
            }

            return false;
        }
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.