Thủ tục lưu trữ cơ sở dữ liệu với chế độ xem trước


15

Một mẫu khá phổ biến trong ứng dụng cơ sở dữ liệu mà tôi làm việc là cần tạo một thủ tục được lưu trữ cho một báo cáo hoặc tiện ích có "chế độ xem trước". Khi một thủ tục như vậy thực hiện cập nhật, tham số này chỉ ra rằng kết quả của hành động sẽ được trả về, nhưng thủ tục không thực sự thực hiện các cập nhật cho cơ sở dữ liệu.

Một cách để thực hiện điều này là chỉ cần viết một if câu lệnh cho tham số và có hai khối mã hoàn chỉnh; một trong số đó cập nhật và trả về dữ liệu và cái còn lại chỉ trả về dữ liệu. Nhưng điều này là không mong muốn vì sự trùng lặp mã và mức độ tin cậy tương đối thấp rằng dữ liệu xem trước thực sự là một sự phản ánh chính xác về những gì sẽ xảy ra với một bản cập nhật.

Ví dụ sau đây cố gắng tận dụng các điểm lưu trữ và biến số giao dịch (không bị ảnh hưởng bởi các giao dịch, ngược lại với các bảng tạm thời) để chỉ sử dụng một khối mã duy nhất cho chế độ xem trước làm chế độ cập nhật trực tiếp.

Lưu ý: Rollback giao dịch không phải là một tùy chọn vì lệnh gọi thủ tục này có thể được lồng trong một giao dịch. Điều này đã được thử nghiệm trên SQL Server 2012.

CREATE TABLE dbo.user_table (a int);
GO

CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE] (
  @preview char(1) = 'Y'
) AS

CREATE TABLE #dataset_to_return (a int);

BEGIN TRANSACTION; -- preview mode required infrastructure
  DECLARE @output_to_return TABLE (a int);
  SAVE TRANSACTION savepoint;

  -- do stuff here
  INSERT INTO dbo.user_table (a)
    OUTPUT inserted.a INTO @output_to_return (a)
    VALUES (42);

  -- catch preview mode
  IF @preview = 'Y'
    ROLLBACK TRANSACTION savepoint;

  -- save output to temp table if used for return data
  INSERT INTO #dataset_to_return (a)
  SELECT a FROM @output_to_return;
COMMIT TRANSACTION;

SELECT a AS proc_return_data FROM #dataset_to_return;
RETURN 0;
GO

-- Examples
EXEC dbo.PREVIEW_EXAMPLE @preview = 'Y';
SELECT a AS user_table_after_preview_mode FROM user_table;

EXEC dbo.PREVIEW_EXAMPLE @preview = 'N';
SELECT a AS user_table_after_live_mode FROM user_table;

-- Cleanup
DROP TABLE dbo.user_table;
DROP PROCEDURE dbo.PREVIEW_EXAMPLE;
GO

Tôi đang tìm kiếm phản hồi về mã và mẫu thiết kế này và / hoặc nếu các giải pháp khác cho cùng một vấn đề tồn tại ở các định dạng khác nhau.

Câu trả lời:


12

Có một số sai sót trong phương pháp này:

  1. Thuật ngữ "xem trước" có thể khá sai lệch trong hầu hết các trường hợp, tùy thuộc vào bản chất của dữ liệu được vận hành (và điều đó thay đổi từ hoạt động sang hoạt động). Điều gì để đảm bảo rằng dữ liệu hiện tại đang được vận hành sẽ ở trạng thái tương tự giữa thời điểm dữ liệu "xem trước" được thu thập và khi người dùng quay lại 15 phút sau - sau khi lấy một tách cà phê, bước ra ngoài để hút thuốc, đi bộ xung quanh khối, quay trở lại và kiểm tra một cái gì đó trên eBay - và nhận ra rằng họ đã không nhấp vào nút "OK" để thực sự thực hiện thao tác và cuối cùng có nhấp vào nút không?

    Bạn có giới hạn thời gian tiến hành thao tác sau khi xem trước được tạo không? Hoặc có thể là một cách để xác định rằng dữ liệu ở trạng thái tương tự tại thời điểm sửa đổi như lúc ban đầu SELECT?

  2. Đây là một điểm nhỏ vì mã ví dụ có thể được thực hiện vội vàng và không đại diện cho trường hợp sử dụng thực sự, nhưng tại sao lại có "Xem trước" cho một INSERThoạt động? Điều đó có thể có ý nghĩa khi chèn nhiều hàng thông qua một cái gì đó giống như INSERT...SELECTvà có thể có một số lượng hàng khác nhau được chèn, nhưng điều này không có ý nghĩa nhiều đối với một hoạt động đơn lẻ.

  3. điều này là không mong muốn vì ... độ tin cậy tương đối thấp rằng dữ liệu xem trước thực sự là một sự phản ánh chính xác về những gì sẽ xảy ra với một bản cập nhật.

    Chính xác thì "mức độ tự tin thấp" này đến từ đâu? Mặc dù có thể cập nhật một số lượng hàng khác nhau hơn là hiển thị SELECTkhi nhiều bảng được THAM GIA và có sự trùng lặp của các hàng trong tập kết quả, nhưng điều đó không phải là vấn đề ở đây. Bất kỳ hàng nào sẽ bị ảnh hưởng bởi một UPDATEđều có thể tự chọn. Nếu có sự không phù hợp thì bạn đang thực hiện truy vấn không chính xác.

    Và những tình huống có sự trùng lặp do bảng THAM GIA khớp với nhiều hàng trong bảng sẽ được cập nhật không phải là tình huống trong đó "Xem trước" sẽ được tạo. Và nếu có trường hợp xảy ra trường hợp này, thì cần phải giải thích cho người dùng rằng họ được cập nhật một tập hợp con của báo cáo được lặp lại trong báo cáo để nó không xuất hiện lỗi nếu chỉ có ai đó nhìn vào số lượng hàng bị ảnh hưởng

  4. Để hoàn thiện (mặc dù các câu trả lời khác đã đề cập đến vấn đề này), bạn không sử dụng TRY...CATCHcấu trúc nên có thể dễ dàng gặp sự cố khi lồng các cuộc gọi này (ngay cả khi không sử dụng Lưu điểm và ngay cả khi không sử dụng Giao dịch). Vui lòng xem câu trả lời của tôi cho Câu hỏi sau, tại đây trên DBA.SE, để biết mẫu xử lý các giao dịch qua các lệnh gọi Thủ tục được lưu trữ lồng nhau:

    Chúng tôi có bắt buộc phải xử lý Giao dịch bằng Mã C # cũng như trong thủ tục được lưu trữ không

  5. NGAY CẢ NẾU các vấn đề được lưu ý ở trên đã được giải quyết, vẫn còn một lỗ hổng nghiêm trọng: trong thời gian ngắn hoạt động được thực hiện (tức là trước đó ROLLBACK), mọi truy vấn đọc bẩn (truy vấn sử dụng WITH (NOLOCK)hoặc SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED) có thể lấy dữ liệu không có một lát sau Mặc dù bất kỳ ai sử dụng truy vấn đọc bẩn đều phải biết về điều này và đã chấp nhận khả năng đó, các hoạt động như thế này làm tăng đáng kể cơ hội đưa ra các dị thường dữ liệu rất khó gỡ lỗi (nghĩa là: bạn muốn dành bao nhiêu thời gian để cố gắng tìm một vấn đề không có nguyên nhân trực tiếp rõ ràng?).

  6. Một mô hình như thế này cũng làm giảm hiệu suất hệ thống bằng cách tăng khả năng chặn bằng cách lấy thêm khóa và tạo thêm hoạt động Nhật ký giao dịch. (Bây giờ tôi thấy rằng @MartinSmith cũng đã đề cập đến 2 vấn đề này trong một nhận xét về Câu hỏi.)

    Ngoài ra, nếu có các Triggers trên các bảng được sửa đổi, đó có thể là một chút xử lý bổ sung (CPU và đọc vật lý / logic) không cần thiết. Các tác nhân kích hoạt cũng sẽ làm tăng thêm khả năng dị thường dữ liệu do đọc bẩn.

  7. Liên quan đến điểm được lưu ý trực tiếp ở trên - khóa tăng - việc sử dụng Giao dịch làm tăng khả năng gặp phải bế tắc, đặc biệt là nếu có liên quan đến Triggers.

  8. Một vấn đề ít nghiêm trọng hơn chỉ liên quan đến kịch bản INSERThoạt động ít có khả năng xảy ra : dữ liệu "Xem trước" có thể không giống với dữ liệu được chèn liên quan đến các giá trị cột được xác định bởi các DEFAULTràng buộc ( Sequences/ NEWID()/ NEWSEQUENTIALID()) và IDENTITY.

  9. Không cần thêm chi phí viết nội dung của Bảng biến vào Bảng tạm thời. Điều ROLLBACKnày sẽ không ảnh hưởng đến dữ liệu trong Biến bảng (đó là lý do tại sao bạn nói rằng bạn đã sử dụng Biến bảng ở vị trí đầu tiên), do đó, sẽ đơn giản hơn khi chỉ đơn giản là SELECT FROM @output_to_return;ở cuối, và sau đó thậm chí không bận tâm đến việc tạo Tạm thời Bàn.

  10. Chỉ trong trường hợp không biết sắc thái của Điểm lưu trữ này (khó có thể nói từ mã ví dụ vì nó chỉ hiển thị một Quy trình được lưu trữ duy nhất): bạn cần sử dụng tên Lưu điểm duy nhất để ROLLBACK {save_point_name}thao tác hoạt động như bạn mong đợi. Nếu bạn sử dụng lại tên, ROLLBACK sẽ quay lại Điểm lưu gần đây nhất của tên đó, có thể không ở cùng mức lồng nhau nơi ROLLBACKđược gọi từ đó. Vui lòng xem khối mã ví dụ đầu tiên trong câu trả lời sau để thấy hành vi này hoạt động: Giao dịch trong một thủ tục được lưu trữ

Điều này dẫn đến là:

  • Thực hiện "Xem trước" không có ý nghĩa nhiều đối với các hoạt động đối mặt với người dùng. Tôi làm điều này thường xuyên cho các hoạt động bảo trì để tôi có thể thấy những gì sẽ bị xóa / Rác được thu thập nếu tôi tiến hành hoạt động. Tôi thêm một tham số tùy chọn được gọi @TestModevà thực hiện một IFcâu lệnh hoặc SELECTkhi @TestMode = 1nào nó thực hiện DELETE. Đôi khi tôi thêm @TestModetham số vào Thủ tục lưu trữ được gọi bởi ứng dụng để tôi (và những người khác) có thể thực hiện kiểm tra đơn giản mà không ảnh hưởng đến trạng thái của dữ liệu, nhưng tham số này không bao giờ được ứng dụng sử dụng.

  • Chỉ trong trường hợp điều này không rõ ràng từ phần "vấn đề" hàng đầu:

    Nếu bạn cần / muốn chế độ "Xem trước" / "Kiểm tra" để xem điều gì sẽ bị ảnh hưởng nếu câu lệnh DML được thực thi, thì KHÔNG sử dụng Giao dịch (tức là BEGIN TRAN...ROLLBACKmẫu) để thực hiện việc này. Đó là một mô hình mà, tốt nhất, chỉ thực sự hoạt động trên một hệ thống người dùng duy nhất, và thậm chí không phải là một ý tưởng tốt trong tình huống đó.

  • Lặp lại phần lớn truy vấn giữa hai nhánh của IFcâu lệnh sẽ gây ra một vấn đề tiềm ẩn là cần phải cập nhật cả hai câu hỏi mỗi khi có thay đổi. Tuy nhiên, sự khác biệt giữa hai truy vấn thường đủ dễ để bắt gặp trong đánh giá mã và dễ sửa. Mặt khác, các vấn đề như khác biệt về trạng thái và đọc bẩn khó tìm và sửa chữa hơn nhiều. Và vấn đề giảm hiệu năng hệ thống là không thể khắc phục. Chúng ta cần nhận ra và chấp nhận rằng SQL không phải là ngôn ngữ hướng đối tượng và việc đóng gói / giảm mã trùng lặp không phải là mục tiêu thiết kế của SQL giống như với nhiều ngôn ngữ khác.

    Nếu truy vấn đủ dài / phức tạp, bạn có thể gói nó trong Hàm nội tuyến có giá trị. Sau đó, bạn có thể thực hiện đơn giản SELECT * FROM dbo.MyTVF(params);cho chế độ "Xem trước" và THAM GIA với (các) giá trị khóa cho chế độ "thực hiện". Ví dụ:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • Nếu đây là một kịch bản báo cáo như bạn đã đề cập, thì chạy báo cáo ban đầu là "Xem trước". Nếu ai đó muốn thay đổi một cái gì đó họ thấy trên báo cáo (có lẽ là trạng thái), thì điều đó không yêu cầu xem trước bổ sung vì mong muốn là thay đổi dữ liệu hiện được hiển thị.

    Nếu hoạt động có thể thay đổi số tiền giá thầu theo một% hoặc quy tắc kinh doanh nhất định, thì điều đó có thể được xử lý trong lớp trình bày (JavaScript?).

  • Nếu bạn thực sự cần thực hiện "Xem trước" cho hoạt động hướng tới người dùng cuối , thì bạn cần phải nắm bắt trạng thái của dữ liệu trước tiên (có thể là hàm băm của tất cả các trường trong kết quả được đặt cho các UPDATEhoạt động hoặc các giá trị chính cho DELETEcác hoạt động), và sau đó, trước khi thực hiện thao tác, hãy so sánh thông tin trạng thái đã nắm bắt với thông tin hiện tại - trong Giao dịch thực hiện HOLDkhóa trên bàn để không có gì thay đổi sau khi thực hiện so sánh này - và nếu có BẤT K differ sự khác biệt nào, hãy ném một lỗi và làm một ROLLBACKthay vì tiến hành với UPDATEhoặc DELETE.

    Để phát hiện sự khác biệt cho các UPDATEhoạt động, một giải pháp thay thế cho việc tính toán hàm băm trên các trường có liên quan sẽ là thêm một cột loại ROWVERSION . Giá trị của ROWVERSIONkiểu dữ liệu sẽ tự động thay đổi mỗi khi có thay đổi đối với hàng đó. Nếu bạn có một cột như vậy, bạn sẽ SELECTcùng với dữ liệu "Xem trước" khác và sau đó chuyển nó sang bước "chắc chắn, tiếp tục và thực hiện cập nhật" cùng với (các) giá trị khóa và giá trị (s) thay đổi. Sau đó, bạn sẽ so sánh các ROWVERSIONgiá trị được truyền vào từ "Xem trước" với các giá trị hiện tại (trên mỗi khóa) và chỉ tiếp tục với UPDATEif ALLcủa các giá trị khớp. Lợi ích ở đây là bạn không cần phải tính toán một hàm băm có tiềm năng, ngay cả khi không có khả năng, cho các phủ định sai và mất một số thời gian mỗi lần bạn thực hiện SELECT. Mặt khác, ROWVERSIONgiá trị được tăng tự động chỉ khi thay đổi, vì vậy không có gì bạn cần phải lo lắng. Tuy nhiên, ROWVERSIONloại là 8 byte, có thể cộng lại khi xử lý nhiều bảng và / hoặc nhiều hàng.

    Có hai ưu và nhược điểm đối với mỗi hai phương pháp này để xử lý việc phát hiện trạng thái không nhất quán liên quan đến UPDATEhoạt động, vì vậy bạn sẽ cần xác định phương thức nào có nhiều "pro" hơn "con" cho hệ thống của bạn. Nhưng trong cả hai trường hợp, bạn có thể tránh được sự chậm trễ giữa việc tạo Bản xem trước và thực hiện thao tác gây ra hành vi ngoài mong đợi của người dùng cuối.

  • Nếu bạn đang thực hiện chế độ "Xem trước" của người dùng cuối, ngoài việc nắm bắt trạng thái của các bản ghi tại thời điểm đã chọn, chuyển qua và kiểm tra tại thời điểm sửa đổi, bao gồm một DATETIMEcho SelectTimevà điền thông qua GETDATE()hoặc một cái gì đó tương tự. Chuyển nó dọc theo lớp ứng dụng để có thể chuyển nó trở lại thủ tục được lưu trữ (chủ yếu là một tham số đầu vào duy nhất) để có thể kiểm tra nó trong Thủ tục lưu trữ. Sau đó, bạn có thể xác định rằng NẾU hoạt động không phải là chế độ "Xem trước", thì @SelectTimegiá trị cần không quá X phút trước giá trị hiện tại của GETDATE(). Có lẽ 2 phút? 5 phút? Nhiều khả năng không quá 10 phút. Ném một lỗi nếu DATEDIFFtrong MINUTES vượt quá ngưỡng đó.


4

Cách tiếp cận đơn giản nhất thường là tốt nhất và tôi thực sự không có vấn đề gì với việc sao chép mã trong SQL, đặc biệt là không phải trong cùng một mô-đun. Sau khi tất cả hai truy vấn đang làm những điều khác nhau. Vậy tại sao không chọn 'Tuyến 1' hoặc Giữ cho nó đơn giản và chỉ có hai phần trong kho lưu trữ, một phần để mô phỏng công việc bạn cần làm và một phần để thực hiện, ví dụ như thế này:

CREATE TABLE dbo.user_table ( rowId INT IDENTITY PRIMARY KEY, a INT NOT NULL, someGuid UNIQUEIDENTIFIER DEFAULT NEWID() );
GO
CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE2]

    @preview CHAR(1) = 'Y'

AS

    SET NOCOUNT ON

    --!!TODO add error handling

    IF @preview = 'Y'

        -- Simulate INSERT; could be more complex
        SELECT 
            ISNULL( ( SELECT MAX(rowId) FROM dbo.user_table ), 0 ) + 1 AS rowId,
            42 AS a,
            NEWID() AS someGuid

    ELSE

        -- Actually do the INSERT, return inserted values
        INSERT INTO dbo.user_table ( a )
        OUTPUT inserted.rowId, inserted.a, inserted.someGuid
        VALUES ( 42 )

    RETURN

GO

Điều này có lợi thế là tự ghi lại tài liệu (nghĩa IF ... ELSElà dễ theo dõi), độ phức tạp thấp (so với điểm lưu với phương pháp tiếp cận biến bảng IMO), do đó ít có lỗi hơn (điểm tuyệt vời từ @Cody).

Về quan điểm của bạn về sự tự tin thấp, tôi không chắc tôi hiểu. Hợp lý hai truy vấn với cùng một tiêu chí sẽ làm điều tương tự. Có khả năng không khớp giữa cardinality giữa UPDATEa và a SELECT, nhưng sẽ là một đặc điểm của các phép nối và tiêu chí của bạn. Bạn có thể giải thích thêm?

Bên cạnh đó, bạn nên đặt NULL/ NOT NULLproperty và các bảng và biến bảng của bạn, xem xét đặt khóa chính.

Cách tiếp cận ban đầu của bạn có vẻ hơi phức tạp có thể dễ bị bế tắc hơn, vì INSERT/ UPDATE/ DELETEhoạt động yêu cầu mức khóa cao hơn so với đơn giản SELECTs.

Tôi nghi ngờ procs thế giới thực của bạn phức tạp hơn, vì vậy nếu bạn cảm thấy cách tiếp cận trên sẽ không hiệu quả với họ, hãy đăng lại với một số ví dụ khác.


3

Mối quan tâm của tôi là như sau.

  • Việc xử lý giao dịch không tuân theo mô hình chuẩn được lồng trong khối Bắt đầu Thử / Bắt đầu Bắt đầu. Nếu đây là một mẫu thì trong một quy trình được lưu trữ với một vài bước nữa bạn có thể thoát khỏi giao dịch này trong chế độ xem trước với dữ liệu vẫn được sửa đổi.

  • Theo định dạng tăng công việc của nhà phát triển. Nếu họ thay đổi các cột bên trong thì họ cũng cần sửa đổi định nghĩa biến bảng, sau đó sửa đổi định nghĩa bảng tạm thời, sau đó sửa đổi các cột chèn ở cuối. Nó sẽ không được phổ biến.

  • Một số thủ tục được lưu trữ không trả về cùng một định dạng dữ liệu mỗi lần; nghĩ về sp_WhoIsActive như một ví dụ phổ biến.

Tôi đã không cung cấp một cách tốt hơn để làm điều đó nhưng tôi không nghĩ những gì bạn có là một mô hình tốt.

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.