DbSet trong Entity Framework Core

Lớp DbSet<TEntity> đại diện cho một tập hợp các thực thể trong mô hình và là nơi thực hiện các thao tác cơ sở dữ liệu cho từng thực thể. Lớp DbSet<TEntity> được thêm dưới dạng thuộc tính vào DbContext và được ánh xạ theo mặc định tới các bảng cơ sở dữ liệu lấy tên của thuộc tính DbSet<TEntity>. DbSet là một triển khai của mẫu Repository.

public class SampleContext : DbContext
{
    public DbSet<Book> Books { get; set; }
    public DbSet<Author> Authors { get; set; }
}

public class Author
{
    public int AuthorId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public ICollection<Book> Books { get; set; }
}

public class Book
{    public int BookId { get; set; }
    public string Title { get; set; }
    public Author Author { get; set; }
    public int AuthorId { get; set; }
}

Trong ví dụ trên, hai thuộc tính DbSet<TEntity> đã được thêm vào lớp DbContext. Thuộc tính DbSet đầu tiên đại diện cho một tập hợp các đối tượng Book, được ánh xạ theo quy ước vào một bảng cơ sở dữ liệu có tên “Books”. Thuộc tính DbSet thứ hai đại diện cho một tập hợp các đối tượng Author và được ánh xạ tới một bảng có tên “Authors” trong cơ sở dữ liệu.

Các thao tác cơ bản của DbSet

Các đối tượng DbSet đại diện cho tập hợp các thực thể trong bộ nhớ. Bất kỳ thay đổi nào bạn thực hiện đối với nội dung của một DbSet sẽ chỉ được lưu vào cơ sở dữ liệu nếu phương thức SaveChanges của DbContext được gọi.

Lớp DbSet có một số phương thức cho phép bạn thực hiện các thao tác CRUD (Create, Read, Update, Delete) cơ bản trên các thực thể.

Thêm một thực thể mới

Để thêm một thực thể mới vào tập hợp được đại diện bởi DbSet, bạn sử dụng phương thức DbSet.Add:

var author = new Author
{
    FirstName = "William",
    LastName = "Shakespeare"
};

using (var context = new SampleContext())
{
    context.Authors.Add(author); // adds the author to the DbSet in memory
    context.SaveChanges(); // commits the changes to the database
}

Truy xuất một thực thể

Nếu bạn muốn truy xuất một thực thể duy nhất, bạn có thể sử dụng phương thức First hoặc Single tùy thuộc vào việc bạn có mong đợi có nhiều hơn một hàng phù hợp với điều kiện hay không.

Phương thức Single sẽ dẫn đến một ngoại lệ được ném ra nếu có nhiều dữ liệu phù hợp với điều kiện. Nó chỉ nên được sử dụng khi truy vấn context cho các thực thể bằng khóa chính:

using (var context = new SampleContext())
{
    var author = context.Authors.Single(a => a.AuthorId == 1);
}

Nếu bạn không chắc chắn về việc truy xuất bất kỳ dữ liệu nào dựa trên tiêu chí bạn truyền vào, bạn nên sử dụng các phương thức FirstOrDefault hoặc SingleOrDefault, nó sẽ trả về null trong trường hợp không có dữ liệu nào phù hợp với tiêu chí tìm kiếm:

using (var context = new SampleContext())
{
    var author = context.Authors.FirstOrDefault(a => a.LastName == "Shakespeare");
}

Có sẵn một phương thức tiện lợi được gọi là phương thức Find được sử dụng để truy vấn context cho một thực thể theo khóa chính:

using (var context = new SampleContext())
{
    var author = context.Authors.Find(1);
}

Phương thức này là một trình bao bọc xung quanh phương thức SingleOrDefault và kiểm tra xem liệu một thực thể có khóa được chỉ định hiện đang được theo dõi bởi context hay không và nếu có, nó sẽ được trả về.

Nếu không, một truy vấn cơ sở dữ liệu sẽ được thực thi và thực thể sẽ được trả về nếu được tìm thấy. Nếu nó vẫn không được tìm thấy, phương thức Find sẽ trả về null.

Các phương thức Single, First, SingleOrDefaultFirstOrDefault cho kết quả thực hiện ngay lập tức bằng một truy vấn vào cơ sở dữ liệu.

Phiên bản không đồng bộ: SingleAsync, FirstAsync, SingleOrDefaultAsync, FirstOrDefaultAsyncFindAsync.

Truy xuất nhiều thực thể

Phương thức phổ biến nhất được sử dụng để trả về nhiều thực thể là phương thức ToList:

using (var context = new SampleContext())
{
    var author = context.Authors.ToList();
}

Sửa đổi một thực thể

Để cập nhật một thực thể, hãy thực hiện các thay đổi bắt buộc đối với giá trị của thuộc tính hoặc thuộc tính và gọi phương thức SaveChanges. Thực thể phải được theo dõi bởi context để được cập nhật:

using(var context = new SampleContext())
{
    var author = context.Authors.Find(1); // retrieve the entity
    author.LastName = "Jones"; // amend properties
    context.SaveChanges(); // commit the changes
}

Phiên bản không đồng bộ: SaveChangesAsync.

Xóa một thực thể

Lớp DbSet có phương thức Remove được sử dụng để xóa một thực thể. Thực thể phải được theo dõi theo context để xóa:

using(var context = new SampleContext())
{
   var author = context.Authors.Find(1); 
   context.Authors.Remove(author); 
   context.SaveChanges(); 
}

Cách tiếp cận này có thể dẫn đến hai truy vấn SQL được thực thi: một truy vấn được tạo bởi phương thức Single để lấy thực thể nếu nó chưa được context theo dõi và một truy vấn được tạo bởi phương thức Remove để xóa thực thể.

Thay vào đó, bạn có thể sử dụng một đối tượng sơ khai để ngăn chặn nhu cầu truy vấn SQL để truy xuất thực thể. Đối tượng sơ khai là đại diện của thực thể sẽ được sử dụng để thực thi. Tất cả những gì cần thiết cho một đối tượng sơ khai hợp lệ là khóa chính của thực thể.

using (var context = new SampleContext())
{
    var author = new Author { AuthorId = 1 };
    context.Authors.Attach(author);
    context.Authors.Remove(author);
    context.Savechanges();
}

Truy vấn dữ liệu bằng DbSet

Truy vấn dữ liệu trong Entity Framework Core được thực hiện dựa trên các thuộc tính DbSet của DbContext. DbSet đại diện cho một tập hợp các thực thể của một kiểu cụ thể – kiểu được chỉ định bởi tham số kiểu.

Truy vấn được chỉ định bằng cách sử dụng Truy vấn tích hợp ngôn ngữ (LINQ), một thành phần trong .NET Framework cung cấp khả năng truy vấn các tập hợp trong C# hoặc VB.NET.

Các truy vấn LINQ có thể được viết bằng cú pháp truy vấn hoặc cú pháp phương thức. Cú pháp truy vấn tương tự với cú pháp SQL. Nhà cung cấp Entity Framework Core mà bạn sử dụng chịu trách nhiệm dịch truy vấn LINQ thành truy vấn SQL để thực thi dựa trên cơ sở dữ liệu.

Ví dụ sau sử dụng cú pháp truy vấn để truy xuất tất cả các tác giả được sắp xếp theo họ của họ:

var data = from a in Authors select a orderby a.LastName

Cú pháp phương thức sử dụng một chuỗi các phương thức mở rộng. Nhiều tên phương thức cũng giống SQL. Ví dụ tiếp theo cho thấy truy vấn trước đó được biểu thị bằng cú pháp phương thức:

var data = context.Authors.OrderBy(a => a.LastName);

Hướng dẫn này sử dụng cú pháp phương thức trong các ví dụ truy vấn.

Truy xuất một đối tượng duy nhất

Truy vấn trả về thực thể duy nhất được thực hiện sử dụng các biến thể của phương thức First, FirstOrDefault, Single, SingleOrDefaultFind.

Ngoài ra, có các phiên bản không đồng bộ của từng loại ở trên là: SingleAsync, FirstAsync, SingleOrDefaultAsync, FirstOrDefaultAsyncFindAsync.

Phương thức First và FirstOrDefault của DbSet

Các phương thức FirstFirstOrDefault được sử dụng để trả về một kết quả từ nhiều kết quả phù hợp. Nếu bạn mong đợi ít nhất một bản ghi phù hợp với tiêu chí, bạn có thể sử dụng phương thức First.

Nếu có khả năng không có bản ghi nào phù hợp với tiêu chí, hãy sử dụng phương thức FirstOrDefault, phương thức này sẽ trả về null, mặc định, trong trường hợp không tìm thấy bản ghi nào.

Cả hai phương thức này đều dẫn đến việc thực thi truy vấn ngay lập tức, có nghĩa là câu lệnh SQL được tạo và thực thi dựa trên cơ sở dữ liệu ngay sau khi gọi phương thức.

Kết quả của phương thức First là truy vấn SELECT TOP(1) lấy tất cả các cột của từ bảng được ánh xạ đến DbSet:

var author = context.Authors.First();

Câu lệnh SQL kết quả:

SELECT TOP(1) [a].[AuthorId], [a].[FirstName], [a].[LastName]
FROM [Authors] AS [a]

Phương thức Single và SingleOrDefault của DbSet

Các phương thức SingleSingleOrDefault được sử dụng để trả về một bản ghi trong đó chỉ có một bản ghi phù hợp với các tiêu chí được chỉ định.

Phương thức Single sẽ tạo ra một truy vấn SELECT TOP(2). Nếu truy vấn trả về nhiều hơn một kết quả, thì một ngoại lệ InvalidOperationException được tạo với thông báo:

Chuỗi chứa nhiều hơn một phần tử.

Vì lý do này, bạn rất khó sử dụng phương thức Single mà không chỉ định một số tiêu chí, thường là khóa chính hoặc giá trị unique index. Bạn có thể chỉ định tiêu chí dưới dạng biểu thức lambda trong phương thức Where hoặc truyền trực tiếp tiêu chí đó vào phương thức Single:

var author = context.Authors.Where(a => a.AuthorId == 1).Single();
var author = context.Authors.Single(a => a.AuthorId == 1);

Cả hai cách tiếp cận đều dẫn đến việc tạo ra câu lệnh SQL giống hệt nhau:

SELECT TOP(2) [a].[AuthorId], [a].[FirstName], [a].[LastName]
FROM [Authors] AS [a]
WHERE [a].[AuthorId] = 1

Nếu truy vấn không trả về kết quả phù hợp, bạn nên sử dụng phương thức SingleOrDefault sẽ trả về null trong trường hợp này.

Phương thức Find của DbSet

Phương thức DbSet.Find quen thuộc với những người sử dụng phiên bản trước của Entity Framework có hỗ trợ các API DbSet.

Phương thức này yêu cầu tham số là khóa thực thể thay vì sử dụng biểu thức lambda. Nó cung cấp tùy chọn ít dài dòng hơn để truy xuất các thực thể đơn lẻ bằng khóa của chúng:

var author = context.Authors.Find(1);

Phương thức Find là một trình bao bọc xung quanh phương thức SingleOrDefault và kiểm tra xem liệu một thực thể có khóa được chỉ định hiện đang được theo dõi bởi context hay không và nếu có, nó sẽ được trả về.

Nếu không, một truy vấn cơ sở dữ liệu sẽ được thực thi và thực thể sẽ được trả về nếu được tìm thấy. Nếu nó vẫn không được tìm thấy, phương thức Find sẽ trả về null.

Thêm thực thể mới bằng DbSet

Các phương thức chính để thêm các thực thể bằng DbSet

  • Add<TEntity>(TEntity entity)
  • AddRange<TEntity>(IEnumerable<TEntity> entities)
  • AddRange<TEntity>(params TEntity[] entities)

Thông thường, bạn sẽ thấy các ví dụ về phiên bản generic của phương thức Add nhưng với tham số kiểu dữ liệu bị bỏ qua vì trình biên dịch sẽ suy ra nó. Hai ví dụ sau đây giống hệt nhau:

// with type parameter
var author = new Author
{ 
    FirstName = "William", 
    LastName = "Shakespeare" 
};
context.Authors.Add<Author>(author);

// without type parameter
var author = new Author
{ 
    FirstName = "William", 
    LastName = "Shakespeare" 
};
context.Authors.Add(author);

Context bắt đầu theo dõi thực thể đã được truyền vào phương thức Add và thiết lập giá trị cho thuộc tính EntityState của nó là Added.

Context cũng thiết lập giá trị Added cho thuộc tính EntityState cho tất cả các đối tượng khác trong biểu đồ đối tượng chưa được context theo dõi.

Trong ví dụ tiếp theo, trạng thái Added cũng được áp dụng cho các cuốn sách:

var context = new SampleContext();
var author = new Author 
{
    FirstName = "William",
    LastName = "Shakespeare",
    Books = new List<Book>
    {
        new Book { Title = "Hamlet"},
        new Book { Title = "Othello" },
        new Book { Title = "MacBeth" }
    }
};
context.Authors.Add(author);

Các cuốn sách được bổ sung bởi thực tế là chúng được tham chiếu thông qua thuộc tính Books của tác giả. Trong ví dụ tiếp theo, các cuốn sách sẽ không được thêm vào:

var author = new Author 
{ 
    FirstName = "William", 
    LastName = "Shakespeare" 
};
var hamlet = new Book 
{ 
    Title = "Hamlet", 
    Author = author 
};
var othello = new Book 
{ 
    Title = "Othello", 
    Author = author 
};
var macbeth = new Book 
{ 
    Title = "MacBeth", 
    Author = author 
};
context.Authors.Add(author);
context.SaveChanges();

Mặc dù tác giả đã được gán cho thuộc tính Author của mỗi cuốn sách đã được khởi tạo, thực thể author không biết về các mối quan hệ này. Đó là vì thuộc tính Books vẫn còn null và những cuốn sách không được thêm vào context.

Thêm nhiều thực thể mới bằng DbSet

Phương thức AddRange được sử dụng để thêm nhiều thực thể vào cơ sở dữ liệu trong một lời gọi phương thức. Mã trong ví dụ tiếp theo rất giống với ví dụ trước, nhưng phương thức AddRange được sử dụng để lưu tất cả sách và tác giả vào cơ sở dữ liệu trong một lần:

var context = new SampleContext();
var author = new Author 
{ 
    FirstName = "Stephen", 
    LastName = "King" 
};
var books = new List<Book> 
{
    new Book { Title = "It", Author = author },
    new Book { Title = "Carrie", Author = author },
    new Book { Title = "Misery", Author = author }
};
context.Books.AddRange(books);
context.SaveChanges();

Phương thức AddRange có tham số kiểu IEnumerable<object>. Tác giả là một phần của biểu đồ đối tượng cho ít nhất một trong những cuốn sách, vì vậy nó cũng được thêm vào.

Phiên bản khác của phương thức AddRange có tham số là một mảng tham số của các thực thể. Nó cho phép bạn truyền một số lượng các biến để thêm các thực thể vào cơ sở dữ liệu mà không cần tạo một danh sách cho chúng:

var context = new SampleContext();
var author = new Author 
{ 
    FirstName = "William", 
    LastName = "Shakespeare" 
};
var hamlet = new Book 
{ 
    Title = "Hamlet", 
    Author = author 
};
var othello = new Book 
{ 
    Title = "Othello", 
    Author = author 
};
var macbeth = new Book 
{ 
    Title = "MacBeth", 
    Author = author 
};
context.Books.AddRange(hamlet, othello, macbeth);
context.SaveChanges();

Khi phương thức SaveChanges được gọi là trên DbContext, tất cả các thực thể với trạng thái EntityStateAdded sẽ được chèn vào cơ sở dữ liệu.

Thứ tự của câu lệnh SQL là chèn các đối tượng được quản lý trước để đảm bảo rằng các giá trị khóa chính của chúng sẽ có sẵn để áp dụng cho khóa ngoại của các đối tượng phụ thuộc.

Sửa đổi thực thể bằng DbSet

Cách tiếp cận mà bạn áp dụng để sửa đổi các thực thể thông qua DbSet tùy thuộc vào việc liệu context có đang theo dõi các thực thể được sửa đổi hay không.

Trong ví dụ sau, thực thể được lấy bởi context, vì vậy context bắt đầu theo dõi nó ngay lập tức. Khi bạn thay đổi các giá trị thuộc tính trên thực thể được theo dõi, context sẽ thay đổi giá trị của EntityState thành Modified và ChangeTracker ghi lại giá trị thuộc tính cũ và giá trị thuộc tính mới.

Khi phương thức SaveChanges được gọi, một câu lệnh UPDATE được tạo và thực thi bởi cơ sở dữ liệu.

var author = context.Authors.Find(1);
author.FirstName = "Bill";
context.SaveChanges();

Vì ChangeTracker theo dõi những thuộc tính nào đã được sửa đổi, context sẽ đưa ra một câu lệnh SQL chỉ cập nhật những thuộc tính đã được thay đổi:

exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Authors] SET [FirstName] = @p0
WHERE [AuthorId] = @p1;
SELECT @@ROWCOUNT;
',N'@p1 int,@p0 nvarchar(4000)',@p1=1,@p0=N'Bill'

Trường hợp ngắt kết nối

Trong trường hợp bị ngắt kết nối chẳng hạn như ứng dụng ASP.NET, các thay đổi đối với các giá trị thuộc tính của thực thể hiện tại có thể diễn ra trong controller hoặc service, không thuộc context.

Trong những trường hợp này, context cần được thông báo rằng thực thể đang ở trạng thái đã được sửa đổi. Điều này có thể đạt được bằng cách sử dụng phương thức DbSet<T>.Update (tính năng mới trong Entity Framework Core).

Phương thức Update của DbSet

Lớp DbSet<T> cung cấp phương thức UpdateUpdateRange để làm việc với các thực thể.

public void Save(Author author)
{
    context.Authors.Update(author);
    context.SaveChanges();
}

Phương thức này dẫn đến thực thể được theo dõi bởi context và có trạng thái là Modified. Context không có bất kỳ cách nào để xác định giá trị thuộc tính nào đã được thay đổi và sẽ tạo câu lệnh SQL để cập nhật tất cả các thuộc tính của thực thể.

Bất kỳ thực thể nào có liên quan (chẳng hạn như một danh sách sách trong ví dụ này) cũng sẽ được theo dõi ở trạng thái Modified, dẫn đến các câu lệnh UPDATE được tạo cho chúng. Nếu thực thể có liên quan không được gán giá trị khóa, nó sẽ được đánh dấu là Added và một câu lệnh INSERT sẽ được tạo thay thế.

Xóa thực thể bằng DbSet

Phương thức mà bạn áp dụng để xóa thực thể thông qua DbSet tùy thuộc vào việc context hiện đang theo dõi thực thể bị xóa hay không.

Trong ví dụ sau, thực thể cần xóa được lấy bởi context, vì vậy context bắt đầu theo dõi nó ngay lập tức. Kết quả của phương thức DbSet<T>.Remove là trạng thái EntityState của thực thể được thiết lập thành Deleted.

context.Authors.Remove(context.Authors.Find(1));
context.SaveChanges();

Khi phương thức SaveChanges được gọi, một câu lệnh DELETE được tạo và thực thi bởi cơ sở dữ liệu.

exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [Authors]
WHERE [AuthorId] = @p0;
SELECT @@ROWCOUNT;
',N'@p0 int',@p0=1

Cách tiếp cận này thực sự dẫn đến hai câu lệnh SQL được thực thi: một câu lệnh để truy xuất thực thể từ cơ sở dữ liệu và câu lệnh thứ hai để xóa nó. Bạn có thể sử dụng một đối tượng sơ khai để đại diện cho đối tượng sẽ bị xóa và do đó ngăn đối tượng được truy xuất từ ​​cơ sở dữ liệu:

var context = new SampleContext();
var author = new Author { AuthorId = 1 };
context.Authors.Remove(author);
context.SaveChanges();

Thuộc tính duy nhất mà đối tượng sơ khai yêu cầu là giá trị khóa chính.

Dữ liệu liên quan

Nếu thực thể mà bạn muốn xóa có dữ liệu liên quan, thì cách tiếp cận mà bạn thực hiện sẽ phụ thuộc vào cách cấu hình mối quan hệ.

Một mối quan hệ được định nghĩa đầy đủ sẽ có một ràng buộc tham chiếu xếp tầng được đặt thành Delete hoặc SetNull, cũng tương tự một mối quan hệ được cấu hình thông qua Fluent API. Trong những trường hợp này, bạn có thể xóa hàng chính và để cơ sở dữ liệu quản lý các hàng phụ thuộc.

Khi hành động ràng buộc tham chiếu được đặt thành NoAction, bạn cần quan tâm đến mọi dữ liệu liên quan một cách rõ ràng. Ví dụ tiếp theo minh họa một mối quan hệ được cấu hình trên một mô hình không bao gồm thuộc tính khóa ngoại:

public class Author
{
   public int AuthorId { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public ICollection<Book> Books { get; set; }
}
public class Book
{
   public int BookId { get; set; }
   public string Title { get; set; }
}

Theo mặc định, mối quan hệ này được cấu hình là tùy chọn và tùy chọn hành động ràng buộc tham chiếu được cấu hình NoAction.

Ngoài ra, Entity Framework Core giới thiệu một thuộc tính shadow để đại diện cho khóa ngoại. Nó được đặt tên là AuthorId và được áp dụng cho thực thể Book, và vì mối quan hệ là tùy chọn nên thuộc tính AuthorId là nullable.

Để xóa tác giả, bạn cần xóa mối quan hệ giữa từng cuốn sách và tác giả. Mã để đạt được điều này như sau:

var context = new SampleContext();
var author = context.Authors.Find(1);
var books = context.Books.Where(b => EF.Property<int>(b, "AuthorId") == 1);
foreach (var book in books)
{
    author.Books.Remove(book);
}
context.Authors.Remove(author);
context.SaveChanges();

Tác giả của nó được truy xuất từ ​​cơ sở dữ liệu và sau đó sách của nó được lấy và xóa từng cuốn một khỏi bộ sưu tập Book của tác giả.

Cuối cùng, tác giả được truyền vào phương thức Remove của context. Kết quả là sách được cập nhật để giá trị AuthorId của chúng được sửa đổi thành null:

exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Books] SET [AuthorId] = @p0
WHERE [BookId] = @p1;
SELECT @@ROWCOUNT;
UPDATE [Books] SET [AuthorId] = @p2
WHERE [BookId] = @p3;
SELECT @@ROWCOUNT;
UPDATE [Books] SET [AuthorId] = @p4
WHERE [BookId] = @p5;
SELECT @@ROWCOUNT;
',N'@p1 int,@p0 int,@p3 int,@p2 int,@p5 int,@p4 int',@p1=1,@p0=NULL,@p3=2,@p2=NULL,@p5=3,@p4=NULL

và tác giả đã bị xóa:

exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [Authors]
WHERE [AuthorId] = @p6;
SELECT @@ROWCOUNT;
',N'@p6 int',@p6=1

Cách tiếp cận này dẫn đến bốn lệnh gọi đến cơ sở dữ liệu: một lệnh để lấy tác giả; một để lấy sách của tác giả, một để cập nhật sách và cuối cùng để xóa tác giả.

Do đó, bạn nên sử dụng các ràng buộc toàn vẹn tham chiếu để đặt các khóa ngoại thành null hoặc xóa các thành phần phụ thuộc.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *