web-dev-qa-db-ja.com

永続テーブルを使用して接続コンテキスト情報を注入する

ストアドプロシージャに大きく依存するレガシーモジュールがいくつかあるアプリケーションに取り組んでいます(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

データ層の観点から、このアプローチには注意事項(堅牢性、パフォーマンスなど)がありますか?

ありがとう。

4
Alexei

パフォーマンスに関しては、接続が開かれるたびに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データベースで使用できます。利用できる場合、それははるかにクリーンな方法です。

4
Dan Guzman

このアプローチでは、3つの小さな問題と1つの大きな問題を確認できます。

軽微な問題:

  1. アドホッククエリのDELETEステートメントは、次の述語を使用します。

    AttributeName = @UsernameAttribute
    

    代わりに、SPID = @@SPIDでのみフィルタリングする必要があります。そのSPIDの最後のインスタンスからの古くなった値が、現在の値と混ざり合うことを望まないためです。

  2. ConnectionContextInfoテーブルでは、両方のAttribute%列がVARCHARとして定義されていますが、アドホッククエリでは、パラメーターがNVARCHARとして定義されており、文字列の前にN。テーブルを更新して、NVARCHARとしても定義されるようにする必要があります。

  3. ピーク時の使用時に挿入された、より高い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つの方法で実行できます。

    • T-SQLストアドプロシージャを使用して、OUTPUTパラメータを介して戻り値を返す、または
    • SQLCLRスカラーUDFを作成します。インプロセスコンテキスト接続を使用します(つまり、"Context Connection = true"とアセンブリをWITH PERMISSION_SET = SAFEとしてマークできます。SQLCLRUDFは、動的SQLを実行し、ローカル一時テーブルにアクセスできます。
1
Solomon Rutzky

ページの競合を最小限に抑えるのに役立つ方法で、コンテキストテーブルの行を事前に割り当てることをお勧めします。

これは、ランダムに生成された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;

enter image description here

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;
1
Max Vernon