Tôi có nên kiểm tra xem có cái gì đó tồn tại trong db không và có bị lỗi nhanh không hoặc đợi ngoại lệ db


32

Có hai lớp:

public class Parent 
{
    public int Id { get; set; }
    public int ChildId { get; set; }
}

public class Child { ... }

Khi gán ChildIdcho Parenttôi nên kiểm tra trước nếu nó tồn tại trong DB hoặc đợi DB ném ngoại lệ?

Ví dụ: (sử dụng Entity Framework Core):

LƯU Ý các loại kiểm tra này TẤT CẢ QUẢNG CÁO ngay cả trên các tài liệu chính thức của Microsoft: https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-USE- mvc / xử lý-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application # Sửa-bộ-bộ-điều khiển nhưng có xử lý ngoại lệ bổ sung choSaveChanges

đồng thời, lưu ý rằng mục đích chính của kiểm tra này là trả lại thông báo thân thiện và trạng thái HTTP đã biết cho người dùng API và không bỏ qua hoàn toàn các ngoại lệ cơ sở dữ liệu. Và ngoại lệ duy nhất được ném là bên trong SaveChangeshoặc SaveChangesAsyncgọi ... vì vậy sẽ không có ngoại lệ khi bạn gọi FindAsynchoặc Any. Vì vậy, nếu con tồn tại nhưng đã bị xóa trước SaveChangesAsyncđó thì ngoại lệ đồng thời sẽ bị ném.

Tôi đã làm điều này do một thực tế là foreign key violationngoại lệ sẽ khó định dạng hơn để hiển thị "Không thể tìm thấy trẻ em với id {Parent.ChildId}."

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    // is this code redundant?
   // NOTE: its probably better to use Any isntead of FindAsync because FindAsync selects *, and Any selects 1
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null)
       return NotFound($"Child with id {parent.ChildId} could not be found.");

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        

    return parent;
}

đấu với:

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    _db.Parents.Add(parent);
    await _db.SaveChangesAsync();  // handle exception somewhere globally when child with the specified id doesn't exist...  

    return parent;
}

Ví dụ thứ hai trong Postgres sẽ đưa 23503 foreign_key_violationra lỗi: https://www.postgresql.org/docs/9.4/static/errcodes-appcill.html

Nhược điểm của việc xử lý các trường hợp ngoại lệ theo cách này trong ORM như EF là nó sẽ chỉ hoạt động với một phụ trợ cơ sở dữ liệu cụ thể. Nếu bạn từng muốn chuyển sang máy chủ SQL hoặc thứ gì khác thì nó sẽ không hoạt động nữa vì mã lỗi sẽ thay đổi.

Không định dạng ngoại lệ đúng cho người dùng cuối có thể phơi bày một số điều bạn không muốn bất kỳ ai ngoài nhà phát triển thấy.

Liên quan:

https://stackoverflow.com/questions/6171588/preventing-race-condition-of-if-exists-update-else-insert-in-entity-framework

https://stackoverflow.com/questions/4189954/im Hiện

https://stackoverflow.com/questions/308905/should-there-be-a-transaction-for-read-queries


2
Chia sẻ nghiên cứu của bạn giúp mọi người . Hãy cho chúng tôi những gì bạn đã cố gắng và tại sao nó không đáp ứng nhu cầu của bạn. Điều này chứng tỏ rằng bạn đã dành thời gian để cố gắng tự giúp mình, nó giúp chúng tôi tránh nhắc lại các câu trả lời rõ ràng và hầu hết nó giúp bạn có được câu trả lời cụ thể và phù hợp hơn. Xem thêm Cách hỏi
gnat

5
Như những người khác đã đề cập, tồn tại khả năng một bản ghi có thể được chèn hoặc xóa đồng thời với việc bạn kiểm tra NotFound. Vì lý do đó, kiểm tra đầu tiên có vẻ như là một giải pháp không thể chấp nhận. Nếu bạn lo lắng về việc viết xử lý ngoại lệ dành riêng cho Postgres không khả dụng cho các phụ trợ cơ sở dữ liệu khác, hãy thử cấu trúc trình xử lý ngoại lệ theo cách mà chức năng cốt lõi có thể được mở rộng bởi các lớp dành riêng cho cơ sở dữ liệu (SQL, Postgres, v.v.)
billrichards

3
Nhìn qua các bình luận, tôi cần nói điều này: ngừng suy nghĩ ở mức độ . "Thất bại nhanh" không phải là một quy tắc tách biệt, nằm ngoài quy tắc có thể hoặc nên được tuân theo một cách mù quáng. Đó là một quy tắc của ngón tay cái. Luôn phân tích những gì bạn thực sự đang cố gắng để đạt được và sau đó xem xét bất kỳ kỹ thuật nào xem liệu nó có giúp bạn đạt được mục tiêu đó hay không. "Thất bại nhanh" giúp bạn ngăn ngừa các tác dụng phụ ngoài ý muốn. Và bên cạnh đó, "fail fast" thực sự có nghĩa là "thất bại ngay khi bạn có thể phát hiện ra có vấn đề". Cả hai kỹ thuật đều thất bại ngay khi phát hiện ra vấn đề, vì vậy bạn phải xem xét các cân nhắc khác.
jpmc26

1
@Konrad ngoại lệ phải làm gì với nó? Ngừng suy nghĩ về các điều kiện chủng tộc như một thứ sống trong mã của bạn: đó là một tài sản của vũ trụ. Bất cứ điều gì, bất cứ điều gì chạm vào tài nguyên mà nó không hoàn toàn kiểm soát (ví dụ: truy cập bộ nhớ trực tiếp, bộ nhớ dùng chung, cơ sở dữ liệu, API REST, hệ thống tệp, v.v.) nhiều lần và hy vọng nó không thay đổi có điều kiện chạy đua tiềm năng. Heck, chúng tôi đối phó với điều này trong C mà thậm chí không ngoại lệ. Đừng bao giờ phân nhánh ở trạng thái của tài nguyên mà bạn không kiểm soát nếu ít nhất một trong các nhánh gây rối với trạng thái của tài nguyên đó.
Jared Smith

1
@DanielPryden Trong câu hỏi của tôi, tôi đã không nói rằng tôi không muốn xử lý các ngoại lệ cơ sở dữ liệu (tôi biết rằng các ngoại lệ là không thể tránh khỏi). Tôi nghĩ rằng nhiều người đã hiểu nhầm, tôi muốn có thông báo lỗi thân thiện cho API Web của mình (cho người dùng cuối đọc) thích Child with id {parent.ChildId} could not be found.. Và định dạng "Vi phạm khóa ngoài" Tôi nghĩ là tồi tệ hơn trong trường hợp này.
Konrad

Câu trả lời:


3

Thay vào đó là một câu hỏi khó hiểu, nhưng CÓ, bạn nên kiểm tra trước và không chỉ xử lý ngoại lệ DB.

Trước hết, trong ví dụ của bạn, bạn đang ở lớp dữ liệu, sử dụng EF trực tiếp trên cơ sở dữ liệu để chạy SQL. Mã của bạn tương đương với việc chạy

select * from children where id = x
//if no results, perform logic
insert into parents (blah)

Thay thế bạn đang đề xuất là:

insert into parents (blah)
//if exception, perform logic

Sử dụng ngoại lệ để thực thi logic có điều kiện là chậm và phổ biến.

Bạn có một điều kiện cuộc đua và nên sử dụng một giao dịch. Nhưng điều này có thể được thực hiện đầy đủ trong mã.

using (var transaction = new TransactionScope())
{
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null) 
    {
       return NotFound($"Child with id {parent.ChildId} could not be found.");
    }

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        
    transaction.Complete();

    return parent;
}

Điều quan trọng là hãy tự hỏi:

"Bạn có mong đợi tình huống này xảy ra?"

Nếu không, sau đó chắc chắn, chèn đi và ném một ngoại lệ. Nhưng chỉ cần xử lý ngoại lệ như bất kỳ lỗi nào khác có thể xảy ra.

Nếu bạn thực sự mong đợi nó xảy ra, thì nó KHÔNG phải là ngoại lệ và bạn nên kiểm tra xem liệu đứa trẻ có tồn tại trước hay không, trả lời bằng thông điệp thân thiện phù hợp nếu không.

Chỉnh sửa - Có vẻ như có rất nhiều tranh cãi về điều này. Trước khi bạn downvote xem xét:

A. Điều gì xảy ra nếu có hai ràng buộc FK. Bạn có ủng hộ việc phân tích thông điệp ngoại lệ để tìm ra đối tượng bị thiếu không?

B. Nếu bạn có lỗi, chỉ có một câu lệnh SQL được chạy. Đó chỉ là các lần truy cập phát sinh thêm chi phí của truy vấn thứ hai.

C. Thông thường Id sẽ là khóa thay thế, Thật khó để tưởng tượng một tình huống mà bạn biết bạn không chắc chắn nó có trên DB. Kiểm tra sẽ là số lẻ. Nhưng nếu đó là một khóa tự nhiên mà người dùng đã nhập vào thì sao? Điều đó có thể có cơ hội cao không có mặt


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
maple_shaft

1
Điều này là hoàn toàn sai lầm và sai lệch! Chính những câu trả lời như thế này tạo ra những chuyên gia tồi mà tôi luôn phải chiến đấu chống lại. CHỌN không bao giờ khóa bảng, do đó, giữa CHỌN và CHERTN, CẬP NHẬT hoặc DELTE, bản ghi có thể thay đổi. Vì vậy, nó là thiết bị phần mềm tệ hại tệ hại và một tai nạn đang chờ xảy ra trong sản xuất.
Daniel Lobo

1
Giao diện @DanielLobo khắc phục điều đó
Ewan

1
hãy kiểm tra nếu bạn không tin tôi
Ewan

1
@yusha Tôi có mã ngay tại đây
Ewan

111

Kiểm tra tính duy nhất và sau đó thiết lập là một phản mẫu; ID luôn được chèn đồng thời giữa thời gian kiểm tra và thời gian viết. Cơ sở dữ liệu được trang bị để giải quyết vấn đề này thông qua các cơ chế như các ràng buộc và giao dịch; hầu hết các ngôn ngữ lập trình không. Do đó, nếu bạn coi trọng tính nhất quán của dữ liệu, hãy để lại cho chuyên gia (cơ sở dữ liệu), tức là thực hiện thao tác chèn và bắt ngoại lệ nếu xảy ra.


34
kiểm tra và thất bại không nhanh hơn chỉ là "cố gắng" và hy vọng điều tốt nhất. Trước đây ngụ ý 2 thao tác được thực hiện và thực hiện bởi hệ thống của bạn và 2 bởi DB, trong khi mới nhất chỉ ngụ ý một trong số chúng. Kiểm tra được ủy quyền cho máy chủ DB. Nó cũng ngụ ý một bước nhảy ít hơn vào mạng và một nhiệm vụ ít hơn được DB tham gia. Chúng tôi có thể nghĩ rằng một truy vấn nữa đến DB là giá cả phải chăng, nhưng chúng tôi thường quên suy nghĩ lớn. Hãy suy nghĩ đồng thời cao kích hoạt truy vấn hơn một trăm lần. Nó có thể nhân đôi toàn bộ lưu lượng đến DB. Nếu vấn đề đó là tùy thuộc vào bạn để quyết định.
Laiv

6
@Konrad Quan điểm của tôi là lựa chọn đúng mặc định là một truy vấn sẽ tự thất bại và đó là cách tiếp cận trước khi truy vấn riêng biệt có trách nhiệm chứng minh chính nó. Đối với "trở thành một vấn đề": vì vậy bạn đang sử dụng các giao dịch hoặc đảm bảo rằng bạn an toàn trước các lỗi ToCToU , phải không? Tôi không rõ ràng với mã được đăng là bạn, nhưng nếu bạn không phải, thì nó đã trở thành một vấn đề theo cách mà một quả bom tích tắc trở thành một vấn đề từ lâu trước khi nó thực sự phát nổ.
mtraceur

4
@Konrad EF Core sẽ không hoàn toàn đặt cả séc của bạn và chèn vào một giao dịch, bạn sẽ phải yêu cầu rõ ràng điều đó. Không có giao dịch, việc kiểm tra trước là vô nghĩa vì trạng thái cơ sở dữ liệu có thể thay đổi giữa kiểm tra và chèn bằng mọi cách. Ngay cả với một giao dịch, bạn có thể không ngăn cơ sở dữ liệu thay đổi dưới chân mình. Chúng tôi đã gặp phải một vấn đề vài năm trước khi sử dụng EF với Oracle, mặc dù db hỗ trợ nó, Entity không kích hoạt khóa các bản ghi đã đọc trong một giao dịch và chỉ có phần chèn được coi là giao dịch.
Mr.Mindor

3
"Kiểm tra tính duy nhất và sau đó cài đặt là một phản mẫu" Tôi sẽ không nói điều này. Nó phụ thuộc mạnh mẽ vào việc bạn có thể cho rằng không có sửa đổi nào đang xảy ra hay không và liệu kiểm tra có tạo ra kết quả hữu ích hơn không (thậm chí chỉ là một thông báo lỗi thực sự có ý nghĩa gì đó với người đọc) khi nó không tồn tại. Với cơ sở dữ liệu xử lý các yêu cầu web đồng thời, không, bạn không thể đảm bảo các sửa đổi khác không xảy ra, nhưng có những trường hợp khi đó là một giả định hợp lý.
jpmc26

5
Kiểm tra tính duy nhất trước tiên không loại bỏ sự cần thiết phải xử lý các thất bại có thể. Mặt khác, nếu một hành động sẽ yêu cầu thực hiện một số thao tác, kiểm tra xem tất cả có khả năng thành công hay không trước khi bắt đầu bất kỳ hành động nào thường tốt hơn thực hiện các hành động có thể cần phải được khôi phục. Thực hiện kiểm tra ban đầu có thể không tránh được tất cả các tình huống trong đó việc quay lại là cần thiết, nhưng nó có thể giúp giảm tần suất của các trường hợp đó.
supercat

38

Tôi nghĩ những gì bạn gọi là thất bại nhanh chóng và những gì tôi gọi nó không giống nhau.

Nói cơ sở dữ liệu để thực hiện thay đổi và xử lý lỗi, đó là nhanh chóng. Cách của bạn là phức tạp, chậm và không đặc biệt đáng tin cậy.

Kỹ thuật đó của bạn không thất bại nhanh, đó là trò chơi preflighting. Đôi khi có những lý do tốt, nhưng không phải khi bạn sử dụng cơ sở dữ liệu.


1
Có những trường hợp khi bạn cần truy vấn thứ 2 khi một lớp phụ thuộc vào lớp khác, vì vậy bạn không có lựa chọn nào trong trường hợp như vậy.
Konrad

4
Nhưng không phải ở đây. Và các truy vấn cơ sở dữ liệu có thể khá thông minh, vì vậy tôi thường nghi ngờ về việc không có sự lựa chọn nào.
gnasher729

1
Tôi nghĩ nó cũng phụ thuộc vào ứng dụng, nếu bạn tạo nó chỉ cho một vài người dùng thì nó sẽ không tạo ra sự khác biệt và mã dễ đọc hơn với 2 truy vấn.
Konrad

21
Bạn đang giả định rằng DB của bạn đang lưu trữ dữ liệu không nhất quán. Nói cách khác, có vẻ như bạn không tin tưởng vào DB của mình và tính nhất quán của dữ liệu. Nếu đó là trường hợp, bạn có một vấn đề thực sự lớn và giải pháp của bạn là một cách giải quyết. Một giải pháp giảm nhẹ định mệnh sẽ được ghi đè sớm hơn sau đó. Có thể có trường hợp bạn buộc phải sử dụng DB ngoài tầm kiểm soát và quản lý của mình. Từ các ứng dụng khác. Trong những trường hợp đó, tôi sẽ xem xét xác nhận như vậy. Trong mọi trường hợp, @gnasher đều đúng, bạn không bị lỗi nhanh hoặc đó không phải là điều chúng tôi hiểu là thất bại nhanh.
Laiv

15

Điều này bắt đầu như một bình luận nhưng phát triển quá lớn.

Không, như các câu trả lời khác đã nêu, không nên sử dụng mẫu này. *

Khi làm việc với các hệ thống sử dụng các thành phần không đồng bộ, sẽ luôn có một điều kiện chạy đua trong đó cơ sở dữ liệu (hoặc hệ thống tệp hoặc hệ thống không đồng bộ khác) có thể thay đổi giữa kiểm tra và thay đổi. Việc kiểm tra loại này đơn giản không phải là cách đáng tin cậy để ngăn chặn loại lỗi bạn không muốn xử lý.
Tồi tệ hơn là không đủ, trong nháy mắt, nó mang lại ấn tượng rằng nó sẽ ngăn chặn lỗi bản ghi trùng lặp mang lại cảm giác an toàn sai lầm.

Bạn vẫn cần xử lý lỗi.

Trong các bình luận bạn đã hỏi nếu bạn cần dữ liệu từ nhiều nguồn.
Vẫn không có.

Các vấn đề cơ bản không biến mất đi nếu những gì bạn muốn kiểm tra trở nên phức tạp hơn.

Bạn vẫn cần xử lý lỗi nào.

Ngay cả khi kiểm tra này là một cách đáng tin cậy để ngăn chặn lỗi cụ thể mà bạn đang cố gắng chống lại, các lỗi khác vẫn có thể xảy ra. Điều gì xảy ra nếu bạn mất kết nối với cơ sở dữ liệu, hoặc nó hết dung lượng, hoặc?

Bạn rất có thể vẫn cần xử lý lỗi liên quan đến cơ sở dữ liệu khác . Việc xử lý lỗi đặc biệt này có lẽ nên là một phần nhỏ của nó.

Nếu bạn cần dữ liệu để xác định những gì cần thay đổi, rõ ràng bạn sẽ cần thu thập nó từ một nơi nào đó. (tùy thuộc vào công cụ nào bạn đang sử dụng, có thể có những cách tốt hơn so với các truy vấn riêng biệt để thu thập nó) Nếu, khi kiểm tra dữ liệu bạn đã thu thập, bạn xác định rằng bạn không cần phải thay đổi, thật tuyệt, đừng thực hiện thay đổi. Xác định này là hoàn toàn tách biệt với mối quan tâm xử lý lỗi.

Bạn vẫn cần xử lý lỗi nào.

Tôi biết tôi đang lặp đi lặp lại nhưng tôi cảm thấy điều quan trọng là phải làm rõ điều này. Tôi đã dọn dẹp mớ hỗn độn này trước đây.

Cuối cùng nó sẽ thất bại. Khi thất bại, sẽ rất khó khăn và tốn thời gian để đi đến tận cùng. Giải quyết các vấn đề phát sinh từ điều kiện chủng tộc là khó khăn. Chúng không xảy ra một cách nhất quán, do đó sẽ khó hoặc thậm chí không thể sinh sản trong sự cô lập. Bạn đã không đưa vào xử lý lỗi thích hợp để bắt đầu nên bạn sẽ không có nhiều vấn đề: Có thể là báo cáo của người dùng cuối về một số văn bản khó hiểu (mà bạn đang cố gắng ngăn chặn không nhìn thấy ở nơi đầu tiên.) Có thể một dấu vết ngăn xếp trỏ lại chức năng đó mà khi bạn nhìn vào nó phủ nhận một cách trắng trợn lỗi thậm chí có thể xảy ra.

* Có thể có lý do kinh doanh hợp lệ để thực hiện các kiểm tra tồn tại này, chẳng hạn như để ngăn ứng dụng sao chép công việc đắt tiền, nhưng nó không phải là sự thay thế phù hợp để xử lý lỗi thích hợp.


2

Tôi nghĩ một điều thứ hai cần lưu ý ở đây - một trong những lý do bạn muốn điều này là để bạn có thể định dạng một thông báo lỗi cho người dùng xem.

Tôi chân thành khuyên bạn rằng:

a) hiển thị cho người dùng cuối cùng một thông báo lỗi chung cho mọi lỗi xảy ra.

b) ghi nhật ký ngoại lệ thực tế ở đâu đó mà chỉ nhà phát triển mới có thể truy cập (nếu trên máy chủ) hoặc nơi nào đó có thể được gửi cho bạn bằng các công cụ báo cáo lỗi (nếu khách hàng triển khai)

c) không thử và định dạng các chi tiết ngoại lệ lỗi mà bạn đăng nhập trừ khi bạn có thể thêm thông tin hữu ích. Bạn không muốn vô tình 'định dạng' một phần thông tin hữu ích mà bạn có thể sử dụng để theo dõi vấn đề.


Tóm lại - ngoại lệ có đầy đủ thông tin kỹ thuật rất hữu ích. Không ai trong số này nên dành cho người dùng cuối và bạn sẽ mất thông tin này trong tình trạng nguy hiểm.


2
"hiển thị cho người dùng cuối cùng một thông báo lỗi chung cho mọi lỗi xảy ra." đó là lý do chính, định dạng ngoại lệ cho người dùng cuối trông giống như một điều khủng khiếp phải làm ..
Konrad

1
Trong bất kỳ hệ thống cơ sở dữ liệu hợp lý nào, bạn sẽ có thể lập trình tìm hiểu lý do tại sao một cái gì đó đã thất bại. Không cần thiết phải phân tích một thông điệp ngoại lệ. Và nói chung hơn: ai nói rằng một thông báo lỗi cần phải được hiển thị cho người dùng? Bạn có thể thất bại lần chèn đầu tiên và thử lại trong một vòng lặp cho đến khi bạn thành công (hoặc tối đa một số giới hạn của lần thử lại hoặc thời gian). Và trên thực tế, back-and-retry là thứ bạn sẽ muốn thực hiện sau cùng.
Daniel Pryden
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.