5

MS2000で作業しているので、ジョブ(JPSID)をそれらを所有する従業員(EmpID)にマップするJobOwnersというテーブルがあります。また、そのジョブの所有を開始した日付(DateStarted)、そのジョブの所有を停止した日付(DateEnded)、および所有権がアクティブかどうか(IsActive)も含まれます。このように見えます。

CREATE TABLE JobOwners
(
    LogID int NOT NULL IDENTITY(1,1) PRIMARY KEY,
    JPSID int NOT NULL FOREIGN KEY REFERENCES JobsPerShift(JPSID),
    EmpID int NOT NULL FOREIGN KEY REFERENCES Employees(EmpID),
    DateStarted datetime,
    DateEnded datetime,
    IsActive tinyint NOT NULL   
)

非アクティブな複製は問題ありませんが、アクティブなJPSIDの複製があってはなりません。いくつかの調査で、CHECK制約の関数を使用してこれを達成できることがわかりました。

CREATE FUNCTION CheckActiveCount(@JPSID INT) 
RETURNS INT AS 
BEGIN
    DECLARE @result INT
    SELECT @result = COUNT(*) FROM JobOwners WHERE JPSID = @JPSID AND IsActive = 1
    RETURN @result
END
GO

ALTER TABLE JobOwners 
 ADD CONSTRAINT CK_JobOwners_IsActive
 CHECK ((IsActive = 1 AND dbo.CheckActiveCount(JPSID) <= 1) OR (IsActive = 0))

これで十分に機能します。他にアクティブなJPSID2がないため、IsActive1でJPSID2を挿入できます。IsActiveが0の場合はチェックが適用されないため、IsActive0でJPSID2を挿入できます。ただし、制約と競合するため、JPSID2をIsActive1とともに再度挿入します。下記参照。

INSERT INTO JobOwners
 VALUES(2,2,NULL,NULL,1)

(1 row(s) affected)

INSERT INTO JobOwners
 VALUES(2,2,NULL,NULL,0)

(1 row(s) affected)

INSERT INTO JobOwners
 VALUES(2,3,NULL,NULL,1)

INSERT statement conflicted with COLUMN FOREIGN KEY constraint...

非アクティブなレコードの1つをアクティブに更新しようとすると、問題が発生します。どういうわけか、それは私を可能にします。

UPDATE JobOwners SET IsActive = 1
 WHERE LogID = 3

(1 row(s) affected)

同じステートメントを再度実行すると、制約と競合しますが、初回は競合しません。このアプリのフロントエンドは、非アクティブなレコードをアクティブに変更することはなく、新しいレコードを挿入するだけですが、それでもテーブルで許可したいものではありません。

アクティブなジョブ所有者を分離し、ジョブ所有者の履歴用に別のテーブルを用意するのが最善かどうか疑問に思っていますが、ここでのベストプラクティスについてはわかりません。どんな助けでも大歓迎です。

ありがとう、
ベン

4

2 に答える 2

7

特定の操作によって、バイパスされる UDF を呼び出すチェック制約が発生するという既知の問題があります。バグは Connect にリストされ (破棄され、すべてのリンクが孤立する前に)、認識されましたが、修正されないとしてクローズされました。これは、回避策に頼る必要があることを意味します。

私の最初の回避策は、おそらく更新トリガーの代わりになるでしょう。私を正直に保ち、これをさらにテストさせてくれた Martin に感謝します。同じステートメントで 2 つの行が 1 に更新されるのを防げないことがわかりました。ロジックを修正し、競合状態を防ぐためにトランザクションを追加しました。

CREATE TRIGGER dbo.CheckJobOwners ON dbo.JobOwners
INSTEAD OF UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  BEGIN TRANSACTION;

  UPDATE j SET IsActive = 1 -- /* , other columns */
    FROM dbo.JobOwners AS j INNER JOIN inserted AS i
    ON i.LogID = j.LogID
    WHERE i.IsActive = 1 AND NOT EXISTS 
    (    -- since only one can be active, we don't need an expensive count:
      SELECT 1 FROM dbo.JobOwners AS j2
        WHERE j2.JPSID = i.JPSID
        AND j2.IsActive = 1 AND j2.LogID <> i.LogID
    )
    AND NOT EXISTS 
    (    -- also need to protect against two rows updated by same statement: 
      SELECT 1 FROM inserted AS i2
        WHERE i2.JPSID = i.JPSID
        AND i2.IsActive = 1 AND i2.LogID <> i.LogID
    );

  -- *if* you want to report errors:
  IF (@@ROWCOUNT <> (SELECT COUNT(*) FROM inserted WHERE IsActive = 1))
    RAISERROR('At least one row was not updated.', 11, 1);

  -- assume setting active = 0 always ok & that IsActive is not nullable
  UPDATE j SET IsActive = 0 -- /* , other columns */
    FROM dbo.JobOwners AS j INNER JOIN inserted AS i
    ON j.LogID = i.LogID
    WHERE i.IsActive = 0;

  COMMIT TRANSACTION;
END
GO

(トリガーの代わりにトリガーを使用する唯一の理由は、事後にロールバックするのではなく、更新する必要がある行のみを更新することです (複数の場合に無効な更新のみをロールバックすることはできません)。 -行の更新)))。

ここでは、この問題について多くの良い議論があります。

https://web.archive.org/web/20171013131650/http://sqlblog.com/blogs/tibor_karaszi/archive/2009/12/17/be-careful-with-constraints-calling-udfs.aspx

于 2012-08-21T17:59:32.780 に答える