ストアドプロシージャに大きく依存するレガシーモジュールがいくつかあるアプリケーションに取り組んでいます(ORMがないため、すべてのフェッチとデータの永続化はストアドプロシージャを通じて行われます)。
レガシーモジュールのセキュリティは、SUSER_NAME()
を使用して現在のユーザーを取得し、セキュリティルールを適用します。
ORM(Entity Framework)を使用するように移行しており、SQLコネクタは汎用ユーザーを使用してデータベース(SQL Server)に接続するため、現在のユーザー名を多くのプロシージャに提供する必要があります。
.NETコードの変更を回避するために、新しい接続が作成されたときに、なんとかして現在のユーザーをコンテキストに「挿入」することを考えました。
CREATE TABLE dbo.ConnectionContextInfo
(
ConnectionContextInfoId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_ConnectionContextInfo PRIMARY KEY,
Created DATETIME2 NOT NULL CONSTRAINT DF_ConnectionContextInfo DEFAULT(GETDATE()),
SPID INT NOT NULL,
AttributeName VARCHAR(32) NOT NULL,
AttributeValue VARCHAR(250) NULL,
CONSTRAINT UQ_ConnectionContextInfo_Info UNIQUE(SPID, AttributeName)
)
GO
接続が開かれる(または接続プールが使用されるときに再利用される)場合、次のコマンドが使用されます。
exec sp_executesql N'
DELETE FROM dbo.ConnectionContextInfo WHERE SPID = @@SPID AND AttributeName = @UsernameAttribute;
INSERT INTO dbo.ConnectionContextInfo (SPID, AttributeName, AttributeValue) VALUES (@@SPID, @UsernameAttribute, @Username);
',N'@UsernameAttribute nvarchar(8),@Username nvarchar(16)',@UsernameAttribute=N'Username',@Username=N'domain\username'
go
(0 CPU、〜15読み取り、<6 ms)
スカラー関数を使用すると、現在のユーザーを簡単に取得できます。
alter FUNCTION dbo.getCurrentUser()
RETURNS VARCHAR(250)
AS
BEGIN
DECLARE @ret VARCHAR(250) = (SELECT AttributeValue FROM ConnectionContextInfo where SPID = @@SPID AND AttributeName = 'Username')
-- fallback to session current, if no data is found on current SPID (i.e. call outside of the actual application)
RETURN ISNULL(@ret, SUSER_NAME())
END
GO
データ層の観点から、このアプローチには注意事項(堅牢性、パフォーマンスなど)がありますか?
ありがとう。
パフォーマンスに関しては、接続が開かれるたびにDELETE
およびINSERT
のオーバーヘッドが発生します。または、この目的で組み込み接続CONTEXT_INFOを使用することもできます。次の例では、情報を固定長の48バイト構造で格納しています。
EXEC sp_executesql N'
DECLARE @ContextInfo binary(48);
SET @ContextInfo = CAST(CAST(@UsernameAttribute AS nchar(8)) + CAST(@Username AS nchar(16)) AS binary(48));
',N'@UsernameAttribute nvarchar(8),@Username nvarchar(16)',@UsernameAttribute=N'Username',@Username=N'domain\username'
GO
CREATE FUNCTION dbo.getCurrentUser()
RETURNS VARCHAR(250)
AS
BEGIN
DECLARE
@ContextInfo binary(48) = CONTEXT_INFO()
, @Username nvarchar(16);
SET @Username = RTRIM(CAST(SUBSTRING(@ContextInfo, 17, 32) AS nvarchar(16)));
RETURN ISNULL(@Username, SUSER_NAME());
END
GO
また、 sp_set_session_context および SESSION_CONTEXT() は、SQL Server 2016およびAzure SQLデータベースで使用できます。利用できる場合、それははるかにクリーンな方法です。
このアプローチでは、3つの小さな問題と1つの大きな問題を確認できます。
軽微な問題:
アドホッククエリのDELETE
ステートメントは、次の述語を使用します。
AttributeName = @UsernameAttribute
代わりに、SPID = @@SPID
でのみフィルタリングする必要があります。そのSPIDの最後のインスタンスからの古くなった値が、現在の値と混ざり合うことを望まないためです。
ConnectionContextInfo
テーブルでは、両方のAttribute%
列がVARCHAR
として定義されていますが、アドホッククエリでは、パラメーターがNVARCHAR
として定義されており、文字列の前にN
。テーブルを更新して、NVARCHAR
としても定義されるようにする必要があります。
ピーク時の使用時に挿入された、より高いSPID値からの古いデータは、次回SPIDが使用されるまで呼び出されないDELETE
がないため、かなり長い間残ります。 SQL Serverエージェントジョブを作成して1日に1回実行し、DELETE
行をX日前に作成することができます。
主要な問題(および解決策):
@Danの回答に基づいて作成した comment に基づいて、_CONTEXT_INFO
を使用することはできません。サイズが制限されているため、将来的に属性を追加する機能が制限されるため、特にNVARCHAR
。
幸い、永続的なテーブルは必要ありません。グローバル一時テーブルを使用すると、これを少し簡略化できます。これにより、前の行をDELETE
する必要がなくなり、ほとんどの場合、SQLエージェントジョブが古いレコードをクリーンアップし、OR行の束を事前に割り当てますが、それに一致する行を更新する必要があります空の文字列へのSPIDまたは各接続ごとのNULL
.
接続を確立したら、テーブルを作成するだけです。次に、任意のキーと値のペアを挿入します。テーブルが自動的に削除されるのは、接続が閉じたとき(プールされていない接続とプールされた接続)、またはその接続を再利用する次のセッションで最初のステートメントが実行され、内部sp_reset_connection
プロセスが実行されたとき(プールされた接続)です。
今、あなたは自分自身に尋ねているかもしれません:
ストアドプロシージャ(またはアドホッククエリ)の終了時に一時テーブルがクリーンアップされませんか?
ローカル一時テーブル(つまり#Name
)の場合、はい、作成されたプロセス/サブプロセスが終了するとクリーンアップされ、親コンテキストで使用できなくなります。ただし、グローバル一時テーブル(つまり、##Name
)は、それらが作成されたプロセスの終了後も存続し、親コンテキストで使用できます。
一時テーブルはグローバルであるため、同じ名前を共有することはできません。
正しい。複数のセッションが互いに衝突するため、CREATE TABLE
ステートメントで標準のテーブル名を使用しても機能しません。コードはアプリからのみ機能するため、アプリから渡されないため、セッションで利用可能な何かを使用してテーブル名を区別する方法が必要ですand目標はアプリコードを変更しないでください。したがって、@@SPID
値を既知の固定プレフィックスに追加するだけで、そのセッションの任意の時点でテーブル名を推測できます。
何かのようなもの:
CREATE PROCEDURE dbo.InitializeSessionContext
AS
SET NOCOUNT ON;
DECLARE @Query NVARCHAR(MAX),
@Template NVARCHAR(MAX) = N'
CREATE TABLE ##SessionContext{{SPID}}
(
AttributeName NVARCHAR(32) NOT NULL,
AttributeValue NVARCHAR(250) NULL,
Created DATETIME2 NOT NULL CONSTRAINT DF_SessionContext{{SPID}} DEFAULT(GETDATE())
);';
SET @Query = REPLACE(@Template, N'{{SPID}}', @@SPID);
EXEC(@Query);
GO
質問に示されているUDFとは異なり、このアプローチでは動的SQLと一時テーブルへのアクセスの両方が必要ですが、どちらも許可されていません。
正解です。どちらもT-SQL関数では実行できませんが、他の2つの方法で実行できます。
OUTPUT
パラメータを介して戻り値を返す、または"Context Connection = true"
とアセンブリをWITH PERMISSION_SET = SAFE
としてマークできます。SQLCLRUDFは、動的SQLを実行し、ローカル一時テーブルにアクセスできます。ページの競合を最小限に抑えるのに役立つ方法で、コンテキストテーブルの行を事前に割り当てることをお勧めします。
これは、ランダムに生成されたGUIDをテーブルクラスタリングキーとして使用することを実際にお勧めする非常に数少ない時間の1つです。このキーは、特定のSPIDのページ位置のランダマイザーとして機能し、ページの競合。
CREATE TABLE dbo.ConnectionContextInfo
(
SlotID UNIQUEIDENTIFIER
CONSTRAINT PK_ConnectionContextInfo
PRIMARY KEY CLUSTERED
DEFAULT (NEWID())
, SPID INT NOT NULL
, AttributeName VARCHAR(128) NOT NULL
, AttributeValue VARCHAR(255) NULL
, CONSTRAINT UQ_ConnectionContextInfo_Info
UNIQUE(SPID, AttributeName)
);
GO
CREATE INDEX IX_ConnectionContextInfo_Lookups
ON dbo.ConnectionContextInfo(SPID, AttributeName);
GO
これにより、必要な行がテーブルに事前に入力されます(spid /属性の組み合わせごとに1つ)。
;WITH Numbers AS
(
SELECT TOP(32767)
rn = ROW_NUMBER() OVER (ORDER BY o1.object_id)
FROM sys.objects o1
, sys.objects o2
, sys.objects o3
)
, Attributes AS
(
SELECT AttrName = N'UserName'
UNION ALL
SELECT AttrName = N'SomeOtherAttribute'
)
INSERT INTO dbo.ConnectionContextInfo (SPID, AttributeName)
SELECT rn
, AttrName
FROM Numbers
, Attributes;
本当にsp_executesql
を使用する必要がある場合は、次のようにします。
EXEC sys.sp_executesql N'UPDATE dbo.ConnectionContextInfo
SET AttributeValue = @AttributeValue
WHERE SPID = @@SPID
AND AttributeName = @AttributeName;'
, N'@AttributeName nvarchar(128), @AttributeValue nvarchar(128)'
, @AttributeName = N'Username'
, @AttributeValue = N'domain\username';
GO
私のspidの結果:
SELECT *
, plc.*
FROM dbo.ConnectionContextInfo
CROSS APPLY sys.fn_PhysLocCracker(%%PhysLoc%%) plc
WHERE SPID = @@SPID;
sp_executesql
を使用する代わりに、ストアドプロシージャを使用して更新を行うことをお勧めします。これにより、エラー処理を簡単に含め、クライアントに影響を与えることなくサーバー側でこのコードを自由に更新できます。例えば:
IF OBJECT_ID('dbo.UpdateConnectionContextInfo') IS NOT NULL
DROP PROCEDURE UpdateConnectionContextInfo;
GO
CREATE PROCEDURE dbo.UpdateConnectionContextInfo
(
@AttributeName NVARCHAR(128)
, @AttributeValue NVARCHAR(255)
)
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.ConnectionContextInfo
SET AttributeValue = @AttributeValue
WHERE SPID = @@SPID
AND AttributeName = @AttributeName
RETURN @@ROWCOUNT;
END
GO
これは、渡された@AttributeName
が無効な場合、@ RetValを0に設定します。
DECLARE @RetVal INT;
EXEC @RetVal = dbo.UpdateConnectionContextInfo
@AttributeName = 'UserName', @AttributeValue = 'SomeUser';
SELECT @RetVal;