Tôi có thể cập nhật một đối tượng đính kèm bằng cách sử dụng một đối tượng tách rời nhưng bằng nhau không?


10

Tôi lấy dữ liệu phim từ một API bên ngoài. Trong giai đoạn đầu tiên, tôi sẽ cạo từng bộ phim và chèn nó vào cơ sở dữ liệu của riêng tôi. Trong giai đoạn thứ hai, tôi sẽ định kỳ cập nhật cơ sở dữ liệu của mình bằng cách sử dụng API "Thay đổi" của API mà tôi có thể truy vấn để xem phim nào đã thay đổi thông tin của họ.

Lớp ORM của tôi là Entity-Framework. Lớp Phim trông như thế này:

class Movie
{
    public virtual ICollection<Language> SpokenLanguages { get; set; }
    public virtual ICollection<Genre> Genres { get; set; }
    public virtual ICollection<Keyword> Keywords { get; set; }
}

Vấn đề phát sinh khi tôi có một bộ phim cần được cập nhật: cơ sở dữ liệu của tôi sẽ nghĩ về đối tượng được theo dõi và đối tượng mới mà tôi nhận được từ lệnh gọi API cập nhật là các đối tượng khác nhau, không để ý .Equals().

Điều này gây ra sự cố bởi vì bây giờ tôi cố gắng cập nhật cơ sở dữ liệu với phim được cập nhật, nó sẽ chèn nó thay vì cập nhật Phim hiện có.

Tôi đã gặp vấn đề này trước đây với các ngôn ngữ và giải pháp của tôi là tìm kiếm các đối tượng ngôn ngữ đính kèm, tách chúng khỏi ngữ cảnh, di chuyển PK của chúng sang đối tượng được cập nhật và gắn nó vào ngữ cảnh. Khi SaveChanges()được thực thi, về cơ bản nó sẽ thay thế nó.

Đây là một cách tiếp cận khá có mùi vì nếu tôi tiếp tục cách tiếp cận này với Movieđối tượng của mình , điều đó có nghĩa là tôi sẽ phải tách phim, ngôn ngữ, thể loại và từ khóa, tra cứu từng người trong cơ sở dữ liệu, chuyển ID của họ và chèn đối tượng mới.

Có cách nào để làm điều này thanh lịch hơn? Lý tưởng nhất là tôi chỉ muốn chuyển bộ phim được cập nhật vào bối cảnh và chọn bộ phim chính xác để cập nhật dựa trên Equals()phương thức, cập nhật tất cả các trường của nó và cho từng đối tượng phức tạp: sử dụng lại bản ghi hiện có dựa trên Equals()phương thức riêng của nó và chèn nếu nó chưa tồn tại

Tôi có thể bỏ qua việc tách / đính kèm bằng cách cung cấp .Update()các phương thức trên từng đối tượng phức tạp mà tôi có thể sử dụng kết hợp với việc truy xuất tất cả các đối tượng được đính kèm nhưng điều này vẫn sẽ yêu cầu tôi truy xuất mọi đối tượng hiện có để cập nhật nó.


Tại sao bạn không thể cập nhật thực thể được theo dõi từ dữ liệu API của mình và lưu các thay đổi mà không thay thế / tách / khớp / đính kèm các thực thể?
Si-N

@ Si-N: bạn có thể mở rộng chính xác như thế nào sau đó không?
Jeroen Vannevel 6/03/2015

Ok bây giờ bạn đã thêm rằng đoạn cuối nó có ý nghĩa hơn, bạn đang cố gắng tránh truy xuất các thực thể trước khi cập nhật chúng? Không có gì có thể là PK của bạn trong lớp Phim của bạn? Làm thế nào bạn phù hợp với các bộ phim từ api bên ngoài với các thực thể của bạn? Dù sao, bạn có thể truy xuất tất cả các thực thể bạn cần cập nhật trong một cuộc gọi db, đây có phải là giải pháp đơn giản hơn không gây ra hiệu suất lớn (trừ khi bạn đang nói về một số lượng lớn phim cần cập nhật)?
Si-N

Lớp Phim của tôi có PK idvà các phim từ API bên ngoài được khớp với các phim cục bộ sử dụng trường này tmdbid. Tôi không thể truy xuất tất cả các thực thể cần được cập nhật trong một cuộc gọi vì đó là về phim, thể loại, ngôn ngữ, từ khóa, v.v ... Mỗi thực thể này đều có PK và có thể đã tồn tại trong cơ sở dữ liệu.
Jeroen Vannevel 8/03/2015

Câu trả lời:


8

Tôi đã không tìm thấy những gì tôi đã hy vọng nhưng tôi đã tìm thấy một sự cải thiện so với trình tự chọn-tách-cập nhật-đính kèm hiện có.

Phương thức tiện ích mở rộng AddOrUpdate(this DbSet)cho phép bạn thực hiện chính xác những gì tôi muốn làm: Chèn nếu nó không ở đó và cập nhật nếu nó tìm thấy một giá trị hiện có. Tôi đã không nhận ra việc sử dụng này sớm hơn vì tôi thực sự chỉ thấy nó được sử dụng trong seed()phương thức kết hợp với Di chuyển. Nếu có bất kỳ lý do nào tôi không nên sử dụng, hãy cho tôi biết.

Một cái gì đó hữu ích cần lưu ý: Có một sự quá tải có sẵn cho phép bạn chọn cụ thể cách xác định sự bình đẳng. Ở đây tôi có thể đã sử dụng TMDbIdnhưng thay vào đó tôi đã chọn hoàn toàn bỏ qua ID của chính mình và thay vào đó sử dụng PK trên TMDbId kết hợp với DatabaseGeneratedOption.None. Tôi sử dụng phương pháp này trên mỗi bộ sưu tập là tốt, khi thích hợp.

Phần thú vị của nguồn :

internalSet.InternalContext.Owner.Entry(existing).CurrentValues.SetValues(entity);

đó là cách dữ liệu thực sự được cập nhật dưới mui xe.

Tất cả những gì còn lại là kêu gọi AddOrUpdatetừng đối tượng mà tôi muốn bị ảnh hưởng bởi điều này:

public void InsertOrUpdate(Movie movie)
{
    _context.Movies.AddOrUpdate(movie);
    _context.Languages.AddOrUpdate(movie.SpokenLanguages.ToArray());
    // Other objects/collections
    _context.SaveChanges();
}

Nó không sạch như tôi hy vọng vì tôi phải chỉ định thủ công từng phần của đối tượng của mình cần được cập nhật nhưng nó sẽ gần như nó sẽ nhận được.

Đọc liên quan: /programming/15336248/entity-framework-5-updating-a-record


Cập nhật:

Hóa ra các bài kiểm tra của tôi không đủ nghiêm ngặt. Sau khi sử dụng kỹ thuật này, tôi nhận thấy rằng trong khi ngôn ngữ mới được thêm vào, nó không được kết nối với phim. trong bảng nhiều-nhiều. Đây là một vấn đề được biết đến nhưng có vẻ ưu tiên thấp và chưa được khắc phục theo như tôi biết.

Cuối cùng, tôi quyết định đi theo cách tiếp cận nơi tôi có Update(T)các phương thức cho từng loại và theo dõi chuỗi sự kiện này:

  • Lặp lại các bộ sưu tập trong đối tượng mới
  • Đối với mỗi mục trong mỗi bộ sưu tập, hãy tìm nó trong cơ sở dữ liệu
  • Nếu nó tồn tại, sử dụng Update()phương thức để cập nhật nó với các giá trị mới
  • Nếu nó không tồn tại, hãy thêm nó vào Dbset thích hợp
  • Trả về các đối tượng đính kèm và thay thế các bộ sưu tập trong đối tượng gốc bằng các bộ sưu tập của các đối tượng đính kèm
  • Tìm và cập nhật đối tượng gốc

Đó là rất nhiều công việc thủ công và nó xấu, vì vậy nó sẽ trải qua một vài lần tái cấu trúc nhưng bây giờ các thử nghiệm của tôi cho thấy nó sẽ hoạt động cho các kịch bản nghiêm ngặt hơn.


Sau khi làm sạch nó thêm, bây giờ tôi sử dụng phương pháp này:

private IEnumerable<T> InsertOrUpdate<T, TKey>(IEnumerable<T> entities, Func<T, TKey> idExpression) where T : class
{
    foreach (var entity in entities)
    {
        var existingEntity = _context.Set<T>().Find(idExpression(entity));
        if (existingEntity != null)
        {
            _context.Entry(existingEntity).CurrentValues.SetValues(entity);
            yield return existingEntity;
        }
        else
        {
            _context.Set<T>().Add(entity);
            yield return entity;
        }
    }
    _context.SaveChanges();
}

Điều này cho phép tôi gọi nó như thế này và chèn / cập nhật các bộ sưu tập cơ bản:

movie.Genres = new List<Genre>(InsertOrUpdate(movie.Genres, x => x.TmdbId));

Lưu ý cách tôi gán lại giá trị được truy xuất cho đối tượng gốc ban đầu: bây giờ nó được kết nối với từng đối tượng đính kèm. Cập nhật đối tượng gốc (phim) được thực hiện theo cách tương tự:

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}

Làm thế nào để bạn xử lý Xóa trong mối quan hệ 1-M? ví dụ: 1-Movie có thể có nhiều ngôn ngữ; nếu một trong những ngôn ngữ bị xóa, mã của bạn có bị xóa không? Có vẻ như giải pháp của bạn chỉ chèn và cập nhật (nhưng không xóa?)
joedotnot

0

Vì bạn đang xử lý các trường khác nhau idtmbid, tôi khuyên bạn nên cập nhật API để tạo một chỉ mục duy nhất và riêng biệt của tất cả các thông tin như thể loại, ngôn ngữ, từ khóa, v.v ... Và sau đó thực hiện cuộc gọi để lập chỉ mục và kiểm tra thông tin thay vì thu thập toàn bộ thông tin về đối tượng cụ thể trong lớp Phim của bạn.


1
Tôi không theo dòng suy nghĩ ở đây. Bạn có thể mở rộng? Lưu ý rằng API bên ngoài hoàn toàn nằm ngoài tầm kiểm soát của tôi.
Jeroen Vannevel
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.