Khi nào tôi nên gọi SaveChanges () khi tạo 1000 đối tượng Entity Framework? (như trong quá trình nhập khẩu)


80

Tôi đang chạy quá trình nhập sẽ có 1000 bản ghi trên mỗi lần chạy. Chỉ đang tìm kiếm một số xác nhận về các giả định của tôi:

Điều nào trong số này hợp lý nhất:

  1. Chạy SaveChanges()mọi AddToClassName()cuộc gọi.
  2. Chạy SaveChanges()mỗi số nAddToClassName() cuộc gọi.
  3. Chạy SaveChanges()sau tất cả các AddToClassName()cuộc gọi.

Lựa chọn đầu tiên có lẽ là chậm phải không? Vì nó sẽ cần phân tích các đối tượng EF trong bộ nhớ, tạo SQL, v.v.

Tôi giả định rằng tùy chọn thứ hai là tốt nhất của cả hai thế giới, vì chúng ta có thể kết thúc thử bắt SaveChanges()cuộc gọi đó và chỉ mất n số bản ghi cùng một lúc, nếu một trong số chúng không thành công. Có thể lưu trữ từng lô trong một Danh sách <>. Nếu SaveChanges()cuộc gọi thành công, hãy loại bỏ danh sách. Nếu nó không thành công, hãy ghi lại các mục.

Tùy chọn cuối cùng có thể cũng sẽ rất chậm, vì mọi đối tượng EF sẽ phải nằm trong bộ nhớ cho đến khi SaveChanges()được gọi. Và nếu lưu không thành công sẽ không có gì được cam kết, phải không?

Câu trả lời:


62

Tôi sẽ kiểm tra nó trước để chắc chắn. Hiệu suất không phải là quá tệ.

Nếu bạn cần nhập tất cả các hàng trong một giao dịch, hãy gọi nó sau tất cả lớp AddToClassName. Nếu các hàng có thể được nhập độc lập, hãy lưu các thay đổi sau mỗi hàng. Tính nhất quán của cơ sở dữ liệu là quan trọng.

Lựa chọn thứ hai tôi không thích. Sẽ rất khó hiểu đối với tôi (từ góc độ người dùng cuối cùng) nếu tôi thực hiện nhập vào hệ thống và nó sẽ từ chối 10 hàng trong số 1000, chỉ vì 1 hàng xấu. Bạn có thể thử nhập 10 và nếu không thành công, hãy thử từng cái một rồi đăng nhập.

Kiểm tra nếu mất nhiều thời gian. Đừng viết 'đáng tin cậy'. Bạn chưa biết điều đó. Chỉ khi nó thực sự là một vấn đề, hãy nghĩ đến giải pháp khác (marc_s).

BIÊN TẬP

Tôi đã thực hiện một số bài kiểm tra (thời gian tính bằng mili giây):

10000 hàng:

SaveChanges () sau 1 hàng: 18510,534
SaveChanges () sau 100 hàng: 4350,3075
SaveChanges () sau 10000 hàng: 5233,0635

50000 hàng:

SaveChanges () sau 1 hàng: 78496,929
SaveChanges () sau 500 hàng: 22302,2835
SaveChanges () sau 50000 hàng: 24022,8765

Vì vậy, thực sự cam kết sau n hàng sẽ nhanh hơn tất cả.

Khuyến nghị của tôi là:

  • SaveChanges () sau n hàng.
  • Nếu một lần cam kết không thành công, hãy thử lần lượt để tìm hàng bị lỗi.

Các lớp kiểm tra:

BÀN:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Lớp học:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}

Lý do tôi viết "có lẽ" là tôi đã phỏng đoán có học. Để nói rõ hơn rằng "Tôi không chắc", tôi đã đặt nó thành một câu hỏi. Ngoài ra, tôi nghĩ rằng việc suy nghĩ về những vấn đề tiềm ẩn TRƯỚC KHI gặp phải chúng là hoàn toàn hợp lý. Đó là lý do tôi đặt câu hỏi này. Tôi đã hy vọng ai đó sẽ biết phương pháp nào hiệu quả nhất, và tôi có thể làm theo cách đó, ngay lập tức.
John Bubriski

Anh bạn tuyệt vời. Chính xác những gì tôi đang tìm kiếm. Cảm ơn bạn đã dành thời gian để kiểm tra điều này! Tôi đoán rằng tôi có thể lưu trữ từng lô trong bộ nhớ, hãy thử cam kết và sau đó nếu nó không thành công, hãy chuyển qua từng lô riêng lẻ như bạn đã nói. Sau đó, khi lô đó được thực hiện, hãy giải phóng các tham chiếu đến 100 mục đó để chúng có thể bị xóa khỏi bộ nhớ. Cảm ơn một lần nữa!
John Bubriski

3
Bộ nhớ sẽ không được giải phóng, bởi vì tất cả các đối tượng sẽ được lưu giữ bởi ObjectContext, nhưng có 50000 hoặc 100000 trong ngữ cảnh không tốn nhiều dung lượng ngày nay.
LukLed

6
Tôi thực sự thấy rằng hiệu suất giảm giữa mỗi lần gọi đến SaveChanges (). Giải pháp cho điều này là thực sự loại bỏ ngữ cảnh sau mỗi lần gọi SaveChanges () và khởi tạo lại một ngữ cảnh mới để bổ sung lô dữ liệu tiếp theo.
Shawn de Wet

1
@LukLed không đúng lắm ... bạn đang gọi SaveChanges bên trong vòng lặp For của mình ... vì vậy mã có thể tiếp tục thêm nhiều mục hơn được lưu bên trong vòng lặp for trên cùng một phiên bản ctx và gọi lại SaveChanges sau trên cùng phiên bản đó .
Shawn de Wet

18

Tôi vừa tối ưu hóa một vấn đề tương tự trong mã của riêng tôi và muốn chỉ ra một cách tối ưu hóa phù hợp với tôi.

Tôi nhận thấy rằng phần lớn thời gian xử lý SaveChanges, cho dù xử lý 100 hay 1000 bản ghi cùng một lúc, đều bị ràng buộc bởi CPU. Vì vậy, bằng cách xử lý các bối cảnh với mẫu nhà sản xuất / người tiêu dùng (được triển khai với BlockingCollection), tôi đã có thể sử dụng tốt hơn nhiều lõi CPU và nhận được từ tổng số 4000 thay đổi / giây (theo báo cáo của giá trị trả về của SaveChanges) hơn 14.000 thay đổi / giây. Hiệu suất sử dụng CPU chuyển từ khoảng 13% (tôi có 8 lõi) lên khoảng 60%. Ngay cả khi sử dụng nhiều luồng tiêu dùng, tôi hầu như không đánh thuế hệ thống IO đĩa (rất nhanh) và việc sử dụng CPU của SQL Server không cao hơn 15%.

Bằng cách giảm tải lưu vào nhiều luồng, bạn có khả năng điều chỉnh cả số lượng bản ghi trước khi cam kết và số luồng thực hiện các hoạt động cam kết.

Tôi thấy rằng việc tạo 1 luồng nhà sản xuất và (số Lõi CPU) -1 luồng người tiêu dùng cho phép tôi điều chỉnh số lượng bản ghi được cam kết trên mỗi lô sao cho số lượng các mục trong Bộ sưu tập chặn dao động trong khoảng từ 0 đến 1 (sau khi luồng người tiêu dùng lấy một mục). Bằng cách đó, chỉ có đủ công việc để các luồng tiêu thụ hoạt động tối ưu.

Tất nhiên, kịch bản này yêu cầu tạo một bối cảnh mới cho mỗi lô, điều này tôi thấy là nhanh hơn ngay cả trong một kịch bản đơn luồng cho trường hợp sử dụng của tôi.


Xin chào, @ eric-j, bạn có thể vui lòng giải thích một chút dòng này "bằng cách xử lý các ngữ cảnh với mẫu nhà sản xuất / người tiêu dùng (được triển khai với BlockingCollection)" để tôi có thể thử với mã của mình không?
Foyzul Karim

14

Nếu bạn cần nhập hàng nghìn bản ghi, tôi sẽ sử dụng thứ gì đó như SqlBulkCopy, chứ không phải Entity Framework cho việc đó.


15
Tôi ghét nó khi mọi người không trả lời câu hỏi của tôi :) Vâng, hãy nói rằng tôi "cần" sử dụng EF. Sau đó là gì?
John Bubriski

3
Chà, nếu bạn thực sự PHẢI sử dụng EF, thì tôi sẽ cố gắng cam kết sau một loạt 500 hoặc 1000 bản ghi. Nếu không, bạn sẽ sử dụng quá nhiều tài nguyên và lỗi sẽ có khả năng khôi phục tất cả 99999 hàng bạn đã cập nhật khi hàng thứ 100000 bị lỗi.
marc_s 21/12/09

Với cùng một vấn đề, tôi đã kết thúc bằng cách sử dụng SqlBulkCopy, đây là cách hoạt động hiệu quả hơn EF trong trường hợp đó. Mặc dù tôi không thích sử dụng một số cách để truy cập cơ sở dữ liệu.
Julien N

2
Tôi cũng đang xem xét giải pháp này vì tôi gặp vấn đề tương tự ... Sao chép hàng loạt sẽ là một giải pháp tuyệt vời, nhưng dịch vụ lưu trữ của tôi không cho phép sử dụng nó (và tôi đoán những người khác cũng vậy), vì vậy đây không phải là một giải pháp khả thi tùy chọn cho một số người.
Dennis Ward

3
@marc_s: Làm thế nào để bạn xử lý nhu cầu thực thi các quy tắc nghiệp vụ vốn có trong các đối tượng nghiệp vụ khi sử dụng SqlBulkCopy? Tôi không thấy làm thế nào để không sử dụng EF mà không thực hiện thừa các quy tắc.
Eric J.

2

Sử dụng một thủ tục được lưu trữ.

  1. Tạo kiểu dữ liệu do người dùng xác định trong máy chủ Sql.
  2. Tạo và điền một mảng kiểu này vào mã của bạn (rất nhanh).
  3. Chuyển mảng đến thủ tục đã lưu trữ của bạn bằng một lần gọi (rất nhanh).

Tôi tin rằng đây sẽ là cách dễ nhất và nhanh nhất để làm điều này.


7
Thông thường trên SO, tuyên bố về "điều này là nhanh nhất" cần được chứng minh bằng mã và kết quả thử nghiệm.
Michael Blackburn

2

Xin lỗi, tôi biết chủ đề này đã cũ, nhưng tôi nghĩ chủ đề này có thể giúp những người khác giải quyết vấn đề này.

Tôi đã gặp vấn đề tương tự, nhưng có khả năng xác thực các thay đổi trước khi bạn thực hiện chúng. Mã của tôi trông như thế này và nó đang hoạt động tốt. Với việc chUser.LastUpdatedtôi kiểm tra xem đó có phải là mục mới hay chỉ là thay đổi. Vì không thể tải lại một Mục nhập chưa có trong cơ sở dữ liệu.

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();

Vâng, nó về cùng một vấn đề, phải không? Với điều này, bạn có thể thêm tất cả 1000 bản ghi và trước khi chạy, saveChanges()bạn có thể xóa những bản ghi sẽ gây ra Lỗi.
Jan Leuenberger

1
Nhưng điểm nhấn của câu hỏi là có bao nhiêu lần chèn / cập nhật để cam kết hiệu quả trong một lần SaveChangesgọi. Bạn không giải quyết vấn đề đó. Lưu ý rằng có nhiều lý do tiềm ẩn khiến SaveChanges không thành công hơn là lỗi xác thực. Nhân tiện, bạn cũng có thể đánh dấu các thực thể Unchangedthay vì tải lại / xóa chúng.
Gert Arnold

1
Bạn nói đúng, nó không trực tiếp giải quyết câu hỏi, nhưng tôi nghĩ hầu hết mọi người tình cờ gặp chủ đề này đều gặp vấn đề với việc xác thực, mặc dù có những lý do khác SaveChangeskhông thành công. Và điều này giải quyết được vấn đề. Nếu bài viết này thực sự làm phiền bạn trong chủ đề này, tôi có thể xóa bài này, vấn đề của tôi đã được giải quyết, tôi chỉ cố gắng giúp đỡ người khác.
Jan Leuenberger

Tôi có một câu hỏi về cái này. Khi bạn gọi GetValidationErrors()nó có "giả" một cuộc gọi đến cơ sở dữ liệu và truy xuất lỗi hay không? Cảm ơn bạn đã trả lời :)
Jeancarlo Fontalvo
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.