テーブルが次のようなデータベースがあります:
tblCustomer(UserID [Primary Key],Facebook,Twitter,PhoneNum);
tblSales(InvoiceID [Primary Key],CustomerID [Foreign Key],ProductID [Foreign Key]);
私はいくつかの紙ベースのレコードをインポートしており、それらは時系列(時間)順で、次の列があります。
(Customer's)FaceBook,Twitter,PhoneNum,ProductID;
何らかの理由で既存のIDシステムがないため、UserIDはインポート時に自動生成されます。私のシナリオでは、Facebook、Twitter、電話番号のいずれでも顧客を一意に識別できるため、一意性の制約を適用するために、それぞれに一意のインデックスがあります。
データのインポートを容易にするビューを作成しました:
viewDataEntry(FaceBook,Twitter,PhoneNum,ProductID);
一般的なケースは、顧客のFacebook(または他の連絡方法)が複数の販売レコードに表示されることです。このような場合を処理するためにトリガーが作成されます。
CREATE TRIGGER
ON dbo.viewDataEntry
INSTEAD OF INSERT
AS
BEGIN TRY
INSERT INTO dbo.tblCustomer(Facebook,Twitter,PhoneNum)
SELECT Facebook,Twitter,PhoneNum FROM inserted;
END TRY
BEGIN CATCH
IF ERROR_NUMBER() != 2601 --To ignore uniqueness violation exception
THROW;
END CATCH
DECLARE @UserID INT;
SET @UserID = (SELECT UserID FROM dbo.tblCustomer AS O,inserted AS I WHERE (O.PhoneNum = I.PhoneNum OR O.Facebook = I.Facebook OR O.Twitter = I.Twitter));
INSERT INTO dbo.tblSales(CustomerID,ProductID)
SELECT @UserID,ProductID FROM inserted;
GO
意図される結果は次のとおりです。
重複した顧客レコードがインポートされた場合は、重複してドロップし、salesテーブルにのみ挿入します。
新しい顧客レコードがインポートされた場合、顧客テーブルと販売テーブルの両方のレコードを作成します。
ただし、重複する値が入力されると、エラー3910または3616が発生します。つまり、トランザクションはコミットできません。これは、顧客テーブルへの挿入をロールバックする必要があるためだと思います。トランザクションの一部をロールバックすることはできませんが、残りの部分(残念ながら意図した結果)は保持できません。
MERGEステートメントを見つけましたが、制限が多すぎます(MATCHEDの後にUPDATEとDELETEが続く必要があるなど)。
解決策を提供してください。
一般に、ビジネスルールのトリガーではなく、ストアドプロシージャを使用するのが最善です。宣言的制約を使用できない場合に、データ整合性ルールを適用するにはトリガーがより適切です。
以下は、トリガーの代わりにストアード・プロシージャーを使用する例です。考慮事項については、インラインコメントを参照してください。
CREATE TABLE dbo.tblCustomer(
UserID int NOT NULL IDENTITY
CONSTRAINT PK_tblCustomer PRIMARY KEY CLUSTERED
, Facebook varchar(100) NULL
, Twitter varchar(100) NULL
, PhoneNum varchar(20) NULL
, CONSTRAINT CK_tblCustomer_not_all_null CHECK (COALESCE(Facebook,Twitter,PhoneNum) IS NOT NULL)
);
CREATE UNIQUE NONCLUSTERED INDEX idx_tblCustomer_Facebook ON dbo.tblCustomer(Facebook) WHERE Facebook IS NOT NULL;
CREATE UNIQUE NONCLUSTERED INDEX idx_tblCustomer_Twitter ON dbo.tblCustomer(Twitter) WHERE Twitter IS NOT NULL;
CREATE UNIQUE NONCLUSTERED INDEX idx_tblCustomer_PhoneNum ON dbo.tblCustomer(PhoneNum) WHERE PhoneNum IS NOT NULL;
CREATE TABLE dbo.tblSales(
InvoiceID int NOT NULL IDENTITY
CONSTRAINT PK_tblSales PRIMARY KEY
, CustomerID int NOT NULL
CONSTRAINT FK_tblSales_tblCustomer
FOREIGN KEY (CustomerID) REFERENCES dbo.tblCustomer(UserID)
, ProductID int NOT NULL
-- CONSTRAINT FK_tblSales_tblProduct
-- FOREIGN KEY REFERENCES dbo.tblProduct(ProductID)
);
GO
CREATE OR ALTER PROCEDURE dbo.usp_InsertCustomerSale
@ProductID int
, @Facebook varchar(100)
, @Twitter varchar(100)
, @PhoneNum varchar(100)
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRAN;
--This statement will error with a 'subquery returned more than one value' if more than one customer already has these alternate keys.
DECLARE @UserID int = (
SELECT UserID
FROM dbo.tblCustomer WITH(UPDLOCK, HOLDLOCK) --locking hint to serialize concurrent insert attempts
WHERE
Facebook = @Facebook
OR Twitter = @Twitter
OR PhoneNum = @PhoneNum
);
IF @UserID IS NULL
BEGIN
--insert new customer row and get assigned identity value
INSERT INTO dbo.tblCustomer(Facebook, Twitter, PhoneNum)
VALUES(@Facebook, @Twitter, @PhoneNum);
SET @UserID = SCOPE_IDENTITY();
END;
--insert sales row
INSERT INTO dbo.tblSales(CustomerID,ProductID)
VALUES (@UserID, @ProductID);
COMMIT;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
GO
--insert customer and sale for customer 1
EXEC dbo.usp_InsertCustomerSale
@ProductID = 1
, @Facebook = 'fb1'
, @Twitter = 'tw1'
, @PhoneNum = '(111)111-1111';
--insert customer and sale for customer 2
EXEC dbo.usp_InsertCustomerSale
@ProductID = 2
, @Facebook = 'fb2'
, @Twitter = 'tw2'
, @PhoneNum = '(222)222-2222';
--insert sale only for customer 2 (with no natural key on sales table, nothing will prevent dup sales)
EXEC dbo.usp_InsertCustomerSale
@ProductID = 2
, @Facebook = 'fb2'
, @Twitter = 'tw2'
, @PhoneNum = '(222)222-2222';
--this will err due to ambiguous customer (customer 1 has phone and customer 2 has facebook and Twitter
EXEC dbo.usp_InsertCustomerSale
@ProductID = 3
, @Facebook = 'fb2'
, @Twitter = 'tw2'
, @PhoneNum = '(111)111-1111';