2009-04-21 7 views
5

Tôi đang làm việc trong một dự án nơi các mục cơ sở dữ liệu không bị xóa, nhưng chỉ được đánh dấu là đã xóa. Một cái gì đó như thế này:Việc cần làm khi tôi muốn sử dụng các ràng buộc cơ sở dữ liệu nhưng chỉ đánh dấu là đã xóa thay vì xóa?

id name  deleted 
--- ------- -------- 
1 Thingy1 0 
2 Thingy2 0 
3 Thingy3 0 

Tôi muốn có thể xác định thứ gì đó giống như một ràng buộc UNIQUE trên cột name. Có vẻ dễ dàng, phải không?

Hãy tưởng tượng một kịch bản trong đó "Thingy3" bị xóa và một kịch bản mới được tạo (có thể là năm sau). Chúng tôi nhận được:

id name  deleted 
--- ------- -------- 
1 Thingy1 0 
2 Thingy2 0 
3 Thingy3 1 
... 
100 Thingy3 0 

Từ quan điểm của người dùng, anh đã xóa một mục và tạo một mục mới. Cũng giống như xóa một tệp và tạo tệp mới. Vì vậy, nó rõ ràng với anh ta rằng các mục mới là không liên quan và unattached với bất kỳ dữ liệu kết nối với mục cũ.

Đã được xử lý, vì DB chỉ quan tâm đến số id và vì mục mới có số id trong số 100 thay vì 3, chúng hoàn toàn khác nhau.

Khó khăn của tôi phát sinh khi tôi muốn ngăn người dùng tạo một mục "Thingy3" khác. Nếu tôi có một ràng buộc UNIQUE chỉ xem xét các mục không được đánh dấu deleted, thì tôi đã giải quyết được một vấn đề.

(Tất nhiên, sau đó tôi sẽ phải đối phó với những gì xảy ra khi một người nào đó làm một undo của delete ...)

Vì vậy, làm thế nào tôi có thể xác định rằng loại một hạn chế?

+2

BTW này được gọi là xóa mềm http://databaserefactoring.com/IntroduceSoftDelete.html – cherouvim

+1

Rất vui khi biết điều này có tên. Cảm ơn! – scraimer

Trả lời

11

Bạn có thể thêm giá trị id vào cuối tên khi bản ghi bị xóa, vì vậy khi ai đó xóa id 3 tên trở thành Thingy3_3 và sau đó khi họ xóa id 100 tên trở thành Thingy3_100. Điều này sẽ cho phép bạn tạo một chỉ mục tổng hợp duy nhất trên tên và các trường đã xóa nhưng sau đó bạn phải lọc cột tên bất cứ khi nào bạn hiển thị nó và loại bỏ id từ cuối tên.

Có lẽ giải pháp tốt hơn là thay thế cột đã xóa của bạn bằng cột deleted_at loại DATETIME. Sau đó bạn có thể duy trì một chỉ mục duy nhất về tên và bị xóa tại, với một bản ghi không bị xóa có giá trị null trong trường deleted_at. Điều này sẽ ngăn việc tạo nhiều tên ở trạng thái hoạt động nhưng sẽ cho phép bạn xóa cùng một tên nhiều lần.

Bạn rõ ràng cần thực hiện kiểm tra khi hủy xóa bản ghi để đảm bảo rằng không có hàng có cùng tên và trường bị xóa rỗng trước khi cho phép xóa.

Bạn thực sự có thể triển khai tất cả logic này trong cơ sở dữ liệu bằng cách sử dụng trình kích hoạt INSTEAD-OF để xóa. Trình kích hoạt này sẽ không xóa bản ghi nhưng thay vào đó sẽ cập nhật cột deleted_at khi bạn xóa bản ghi.

Đoạn mã ví dụ sau minh họa này

CREATE TABLE swtest ( 
    id   INT IDENTITY, 
    name  NVARCHAR(20), 
    deleted_at DATETIME 
) 
GO 
CREATE TRIGGER tr_swtest_delete ON swtest 
INSTEAD OF DELETE 
AS 
BEGIN 
    UPDATE swtest SET deleted_at = getDate() 
    WHERE id IN (SELECT deleted.id FROM deleted) 
    AND deleted_at IS NULL  -- Required to prevent duplicates when deleting already deleted records 
END 
GO 

CREATE UNIQUE INDEX ix_swtest1 ON swtest(name, deleted_at) 

INSERT INTO swtest (name) VALUES ('Thingy1') 
INSERT INTO swtest (name) VALUES ('Thingy2') 
DELETE FROM swtest WHERE id = SCOPE_IDENTITY() 
INSERT INTO swtest (name) VALUES ('Thingy2') 
DELETE FROM swtest WHERE id = SCOPE_IDENTITY() 
INSERT INTO swtest (name) VALUES ('Thingy2') 

SELECT * FROM swtest 
DROP TABLE swtest 

Các chọn từ truy vấn này trả về sau

 
id  name  deleted_at 
1  Thingy1 NULL 
2  Thingy2 2009-04-21 08:55:38.180 
3  Thingy2 2009-04-21 08:55:38.307 
4  Thingy2 NULL 

Vì vậy, bên trong mã của bạn, bạn có thể xóa các bản ghi bằng cách sử dụng bình thường xóa và để kích hoạt chăm sóc của các chi tiết. Vấn đề duy nhất có thể (mà tôi có thể thấy) là xóa các bản ghi đã bị xóa có thể dẫn đến các hàng trùng lặp, do đó điều kiện trong trình kích hoạt không cập nhật trường deleted_at trên hàng đã bị xóa.

+0

Bình chọn này, bởi vì nó chính xác là những gì tôi đang xem xét cho ứng dụng của riêng tôi. Đang cố gắng nghĩ về bất kỳ nhược điểm nào, nhưng dường như nó vẫn đủ vững chắc. – belugabob

+0

Tôi thích ý tưởng "deleted_at"! – scraimer

0

Không chắc về SQLServer2005, nhưng bạn có thể xác định hợp chất constrainst/chỉ số

CREATE UNIQUE INDEX [MyINDEX] ON [TABLENAME] ([NAME] , [DELETED]) 

Như đã chỉ ra bởi SteveWeet, điều này sẽ chỉ cho phép bạn xóa/tạo hai lần.

+0

Điều này sẽ chỉ cho phép xóa một 'Thingy3' mà bạn không thể tạo, xóa, tạo lại và sau đó xóa lại. –

+0

Điểm tốt, điều này sẽ không cho phép cập nhật N, sửa chữa nó –

1

Ví dụ: bạn có thể thêm ký tự không hợp lệ (*) vào tên đã xóa. Nhưng bạn vẫn gặp sự cố khi phục hồi mục đã xóa. Vì vậy, có lẽ ý tưởng tốt hơn là cấm tên kép ngay cả khi chúng bị xóa.

Bạn có thể xóa các bản ghi đã xóa sau một khoảng thời gian (hoặc di chuyển chúng sang một bảng riêng).

+0

Sửa đổi dữ liệu do người dùng nhập luôn luôn có vấn đề, vì bạn không nhất thiết dự đoán liệu dữ liệu có kết thúc bằng ký tự không hợp lệ hay không. Phải thừa nhận rằng, ký tự sẽ chỉ được thêm vào các bản ghi 'đã xóa', để giới thiệu một yếu tố nhất định về khả năng dự đoán. Dù bằng cách nào, tôi thích giải pháp của Steve Weet hơn, vì nó hỗ trợ phục hồi. – belugabob

0

Tạo ràng buộc duy nhất trên (tên, đã xóa). Điều này có nghĩa là bạn chỉ có thể xóa một tên trên mỗi tên.

Các công việc xung quanh rõ ràng cho rằng công trình thuộc ANSI-92 nhưng không phải trên MS SQLServer: https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=299229

+0

cột "đã xóa" phải là thứ có tiềm năng va chạm ít hơn (không có boolean!), Vì có thể có một số mục có cùng tên trong lịch sử. – deamon

1

Đối với một bảng đơn giản gọi là [Test] với các cột ID (int), Lọc (nvarchar), bị xóa (bit)

ALTER TABLE [dbo].[Test] ADD 
    CONSTRAINT [DF_Test_Deleted] DEFAULT (0) FOR [Deleted], 
    CONSTRAINT [IX_Test] UNIQUE NONCLUSTERED 
    (
     [filter], 
     [Deleted] 
    ) ON [PRIMARY] 
+0

Cách tiếp cận này trở nên có vấn đề khi một bản ghi bị 'xóa' một bản ghi có cùng giá trị 'Bộ lọc' được thêm vào và nỗ lực 'xóa' bản ghi này được thực hiện - khi ràng buộc đó bị vi phạm. – belugabob

1

Thêm băm ngẫu nhiên sau tên duy nhất. Cái gì đó có thể dễ dàng đảo ngược. Có thể tách biệt với dấu gạch dưới hoặc một số ký tự khác.

Chỉnh sửa sau nhận xét: Bạn chỉ cần thêm dấu gạch dưới và dấu thời gian hiện tại.

+0

Brilliant! Heck, tôi thậm chí có thể đổi tên thành "Thingy3 (xóa ngày 1/1/2000 # 1)" để có thêm thông tin cho người dùng! – scraimer

2

Vấn đề với ràng buộc duy nhất hợp chất là không thể có nhiều bản ghi có cùng tên bị xóa. Điều này có nghĩa là hệ thống sẽ bị ngắt sau khi bạn xóa một bản ghi thứ ba. Tôi sẽ không khuyên bạn nên thêm các công cụ vào tên vì lý do, một tình huống trùng lặp có thể phát sinh. Ngoài ra, bằng cách làm như vậy, về cơ bản bạn đang làm hỏng dữ liệu trong cơ sở dữ liệu cũng như thêm logic kinh doanh khó hiểu vào chính dữ liệu.

Giải pháp duy nhất, cơ sở dữ liệu rộng, là thêm trình kích hoạt để kiểm tra xem dữ liệu được chèn/cập nhật có hợp lệ hay không. Cũng có thể đặt các kiểm tra bên ngoài cơ sở dữ liệu, vào mã.

3

Có thể đáng xem xét sử dụng bảng "thùng rác". Thay vì giữ các bản ghi cũ trong cùng một bảng với một lá cờ, di chuyển chúng vào bảng riêng của nó với các ràng buộc riêng của nó. Ví dụ, trong bảng đang hoạt động bạn có một ràng buộc UNIQUE về tên, nhưng trong bảng thùng rác bạn không.

+0

Điều này có thể dẫn đến xung đột khi ORM được sử dụng (không biết nếu điều này có liên quan ở đây). – deamon

1

Thay vì cột sử dụng cột đã xóa end_date. Khi người dùng xóa một bản ghi, hãy thêm ngày hiện tại vào cột end_date. Bất kỳ bản ghi nào trong đó cột end_date là NULL là các bản ghi hiện tại của bạn. Xác định ràng buộc duy nhất trên hai tên cột và end_date. Do ràng buộc này, bạn không bao giờ có một kịch bản khi trùng lặp tên bản ghi hợp lệ. Bất kỳ người dùng nào muốn hủy xóa một bản ghi, bạn cần phải đặt cột end_date thành null và nếu điều này vi phạm ràng buộc duy nhất thì bạn sẽ hiển thị người dùng thư cho người dùng có cùng tên đó.

+0

Đó là những gì Steve Weet đã nói tại http://stackoverflow.com/questions/771197/what-to-do-when-i-want-to-use-database-constraints-but-only-mark-as-deleted- inste/771337 # 771337 – scraimer

1

Loại ràng buộc bạn yêu cầu là ràng buộc mức CHECK mức bảng nghĩa là ràng buộc CHECK bao gồm truy vấn con kiểm tra NOT EXISTS (hoặc tương đương) cho bảng, ví dụ:

CREATE TABLE Test 
(
    ID INTEGER NOT NULL UNIQUE, 
    name VARCHAR(30) NOT NULL, 
    deleted INTEGER NOT NULL, 
    CHECK (deleted IN (0, 1)) 
); 

ALTER TABLE Test ADD 
    CONSTRAINT test1__unique_non_deleted 
     CHECK 
     (
     NOT EXISTS 
     (
      SELECT T1.name 
       FROM Test AS T1 
      WHERE T1.deleted = 0 
      GROUP 
       BY T1.Name 
      HAVING COUNT(*) > 1 
     ) 
    ); 

INSERT INTO Test (ID, name, deleted) VALUES (1, 'Thingy1', 0) 
; 
INSERT INTO Test (ID, name, deleted) VALUES (2, 'Thingy2', 0) 
; 
INSERT INTO Test (ID, name, deleted) VALUES (3, 'Thingy3', 1) 
; 
INSERT INTO Test (ID, name, deleted) VALUES (4, 'Thingy3', 1) 
; 
INSERT INTO Test (ID, name, deleted) VALUES (5, 'Thingy3', 0) 
; 
INSERT INTO Test (ID, name, deleted) VALUES (6, 'Thingy3', 0) 
; 

cuối cùng INSERT (ID = 6) sẽ gây ra những khó khăn để cắn và INSERT sẽ thất bại. Q.

... ooh, gần như quên đề cập: SQL Server chưa hỗ trợ các ràng buộc CHECK có chứa truy vấn con (tôi đã thử nghiệm ở trên trên ACE/JET, a.k.a.). Trong khi bạn có thể sử dụng FUNCTION Tôi đã đọc điều này là không an toàn do các ràng buộc kiểm tra SQL Server trên cơ sở từng hàng (xem David Portas' Blog). Cho đến khi tính năng SQL-92 đầy đủ này được hỗ trợ trong SQL Server, cách giải quyết ưa thích của tôi là sử dụng cùng một logic trong một trigger.