Criar constantes no nível de banco de dados (enumerações) sem usar o CLR?

9

Eu tenho vários objetos SQL que precisam executar ações alternativas com base no estado desejado da solicitação. Existe uma maneira de criar constantes no nível de banco de dados (enumerações) que podem ser passadas para procedimentos armazenados, funções com valor de tabela e usadas em consultas (sem usar o CLR)?

CREATE PROCEDURE dbo.DoSomeWork(@param1 INTEGER, ..., @EnumValue myEnumType)  AS ...;

e depois use-o:

EXEC doSomeWork 85, ..., (myEnumType.EnumValue1 + myEnumType.EnumValue2);

Onde myEnumTypeconteria alguns valores de enumeração.

No procedimento, eu seria capaz de usá @EnumValue-lo e testá-lo em relação aos valores myEnumTypepara fazer o trabalho necessário. Eu faria os valores de myEnumTypeuma máscara de bits para o caso que estou considerando.

Para um exemplo simples, considere um processo caro que pega um conjunto de dados enorme e o reduz a um conjunto de dados menor, mas ainda muito grande. Nesse processo, você precisa fazer alguns ajustes no meio do processo que afetarão o resultado. Digamos que este seja um filtro para (ou contra) alguns tipos de registros com base em algum status de um cálculo intermediário dentro da redução. O @EnumValuetipo de myEnumTypepode ser usado para testar esse

SELECT   ...
FROM     ...
WHERE       (@EnumValue & myEnumType.EnumValue1 = myEnumType.EnumValue1 AND ...)
        OR  (@EnumValue & myEnumType.EnumValue2 = myEnumType.EnumValue2 AND ...)
        OR  ...

Esses tipos de constantes no nível do banco de dados são possíveis no SQL Server sem o uso do CLR?

Estou procurando uma enumeração no nível de banco de dados ou um conjunto de constantes que podem ser passadas como parâmetros para procedimentos armazenados, funções e assim por diante.

Edmund
fonte

Respostas:

9

Você pode criar um tipo de enumeração no SQL Server usando um esquema XML.

Por exemplo cores.

create xml schema collection ColorsEnum as '
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="Color">
        <xs:simpleType>
            <xs:restriction base="xs:string"> 
                <xs:enumeration value="Red"/>
                <xs:enumeration value="Green"/>
                <xs:enumeration value="Blue"/>
                <xs:enumeration value="Yellow"/>
            </xs:restriction> 
        </xs:simpleType>
    </xs:element>
</xs:schema>';

Isso permite que você use uma variável ou parâmetro do tipo xml(dbo.ColorsEnum).

declare @Colors xml(dbo.ColorsEnum);
set @Colors = '<Color>Red</Color><Color>Green</Color>'

Se você tentar adicionar algo que não é uma cor

set @Colors = '<Color>Red</Color><Color>Ferrari</Color>';

você recebe um erro.

Msg 6926, Level 16, State 1, Line 43
XML Validation: Invalid simple type value: 'Ferrari'. Location: /*:Color[2]

Construir o XML assim pode ser um pouco entediante, por exemplo, você pode criar uma visualização auxiliar que também mantenha os valores permitidos.

create view dbo.ColorsConst as
select cast('<Color>Red</Color>' as varchar(100)) as Red,
       cast('<Color>Green</Color>' as varchar(100)) as Green,
       cast('<Color>Blue</Color>' as varchar(100)) as Blue,
       cast('<Color>Yellow</Color>' as varchar(100)) as Yellow;

E use-o assim para criar a enumeração.

set @Colors = (select Red+Blue+Green from dbo.ColorsConst);

Se você deseja criar a visualização dinamicamente a partir do Esquema XML, é possível extrair as cores com esta consulta.

select C.Name
from (select xml_schema_namespace('dbo','ColorsEnum')) as T(X)
  cross apply T.X.nodes('//*:enumeration') as E(X)
  cross apply (select E.X.value('@value', 'varchar(100)')) as C(Name);

Obviamente, a enumeração também pode ser usada como parâmetros para funções e procedimentos.

create function dbo.ColorsToString(@Colors xml(ColorsEnum))
returns varchar(100)
as
begin
  declare @T table(Color varchar(100));

  insert into @T(Color)
  select C.X.value('.', 'varchar(100)')
  from @Colors.nodes('Color') as C(X);

  return stuff((select ','+T.Color
                from @T as T
                for xml path('')), 1, 1, '');
end
create procedure dbo.GetColors
  @Colors xml(ColorsEnum)
as
select C.X.value('.', 'varchar(100)') as Color
from @Colors.nodes('Color') as C(X);
declare @Colors xml(ColorsEnum) = '
<Color>Red</Color>
<Color>Blue</Color>
';

select dbo.ColorsToString(@Colors);

set @Colors = (select Red+Blue+Green from dbo.ColorsConst);
exec dbo.GetColors @Colors;
Mikael Eriksson
fonte
6

Como você aparentemente está usando o SQL Server 2016, eu gostaria de jogar outra opção ' possível ' - SESSION_CONTEXT.

O artigo de Leonard Lobel, Compartilhando Estado no SQL Server 2016,SESSION_CONTEXT possui algumas informações muito boas sobre essa nova funcionalidade no SQL Server 2016.

Resumindo alguns pontos-chave:

Se você sempre quis compartilhar o estado da sessão entre todos os procedimentos e lotes armazenados durante toda a vida útil de uma conexão com o banco de dados, vai adorar SESSION_CONTEXT. Quando você se conecta ao SQL Server 2016, obtém um dicionário com estado, ou o que geralmente é chamado de bolsa de estado, algum lugar onde você pode armazenar valores, como seqüências de caracteres e números, e depois recuperá-lo com uma chave que você atribui. No caso de SESSION_CONTEXT, a chave é qualquer sequência e o valor é uma variável sql_, o que significa que pode acomodar uma variedade de tipos.

Depois de armazenar algo SESSION_CONTEXT, ele permanece lá até a conexão ser fechada. Ele não é armazenado em nenhuma tabela do banco de dados, apenas permanece na memória enquanto a conexão permanecer ativa. E todo e qualquer código T-SQL executado dentro de procedimentos armazenados, gatilhos, funções ou qualquer outra coisa, pode compartilhar o que você usar SESSION_CONTEXT.

A coisa mais próxima que tivemos até agora foi essa CONTEXT_INFO, que permite armazenar e compartilhar um único valor binário de até 128 bytes de comprimento, que é muito menos flexível do que o dicionário fornecido SESSION_CONTEXT, que suporta vários valores de dados diferentes tipos.

SESSION_CONTEXTé fácil de usar, basta chamar sp_set_session_context para armazenar o valor por uma chave desejada. Ao fazer isso, você fornece a chave e o valor, é claro, mas também pode definir o parâmetro read_only como true. Isso bloqueia o valor no contexto da sessão, para que não possa ser alterado pelo resto da vida útil da conexão. Assim, por exemplo, é fácil para um aplicativo cliente chamar esse procedimento armazenado para definir alguns valores de contexto da sessão logo após estabelecer a conexão com o banco de dados. Se o aplicativo definir o parâmetro read_only ao fazer isso, os procedimentos armazenados e outro código T-SQL que será executado no servidor poderão ler apenas o valor, eles não poderão alterar o que foi definido pelo aplicativo em execução no cliente.

Como teste, criei um gatilho de login do servidor que define algumas CONTEXT_SESSIONinformações - uma delas SESSION_CONTEXTfoi definida como @read_only.

DROP TRIGGER IF EXISTS [InitializeSessionContext] ON ALL SERVER
GO
CREATE TRIGGER InitializeSessionContext ON ALL SERVER
FOR LOGON AS

BEGIN

    --Initialize context information that can be altered in the session
    EXEC sp_set_session_context @key = N'UsRegion'
        ,@value = N'Southeast'

    --Initialize context information that cannot be altered in the session
    EXEC sp_set_session_context @key = N'CannotChange'
        ,@value = N'CannotChangeThisValue'
        ,@read_only = 1

END;

Entrei como um usuário completamente novo e consegui extrair as SESSION_CONTEXTinformações:

DECLARE @UsRegion varchar(20)
SET @UsRegion = CONVERT(varchar(20), SESSION_CONTEXT(N'UsRegion'))
SELECT DoThat = @UsRegion

DECLARE @CannotChange varchar(20)
SET @CannotChange = CONVERT(varchar(20), SESSION_CONTEXT(N'CannotChange'))
SELECT DoThat = @CannotChange

Eu até tentei alterar as informações de contexto 'read_only':

EXEC sp_set_session_context @key = N'CannotChange'
    ,@value = N'CannotChangeThisValue'

e recebeu um erro:

Msg 15664, Nível 16, Estado 1, Procedimento sp_set_session_context, Linha 1 [Linha 8 de Início do Lote] Não é possível definir a chave 'CannotChange' no contexto da sessão. A chave foi definida como somente leitura para esta sessão.

Uma observação importante sobre os gatilhos de login ( deste post )!

Um gatilho de logon pode efetivamente impedir conexões bem-sucedidas ao Mecanismo de Banco de Dados para todos os usuários, incluindo membros da função de servidor fixa sysadmin. Quando um gatilho de logon está impedindo conexões, os membros da função de servidor fixa sysadmin podem se conectar usando a conexão de administrador dedicada ou iniciando o Mecanismo de Banco de Dados no modo de configuração mínima (-f)


Uma desvantagem potencial é que isso preenche toda a instância do contexto da sessão (não por banco de dados). Neste ponto, as únicas opções em que consigo pensar são:

  1. Nomear seus Session_Contextpares nome-valor prefixando-os com o nome do banco de dados de modo a não causar uma colisão com o mesmo nome do tipo em outro banco de dados. Isso não resolve o problema de pré-definir TODOS Session_Contextos valores de nome para todos os usuários.
  2. Quando o gatilho de login é acionado, você tem acesso a EventData(xml) que pode ser usado para extrair o usuário de login e, com base nisso, pode criar Session_Contextpares específicos de nome e valor.
Scott Hodgin
fonte
4

No SQL Server, não (embora eu me lembre de ter criado constantes nos pacotes Oracle em 1998 e tenha deixado de tê-las no SQL Server).

E acabei de testar e descobri que você não pode fazer isso com o SQLCLR, pelo menos não no sentido de que funcionaria em todos os casos. A espera é as restrições nos parâmetros do procedimento armazenado. Parece que você não pode ter um .ou ::no nome do parâmetro. Eu tentei:

EXEC MyStoredProc @ParamName = SchemaName.UdtName::[StaticField];

-- and:

DECLARE @Var = SchemaName.UdtName = 'something';
EXEC MyStoredProc @ParamName = @Var.[InstanceProperty];

Nos dois casos, nem sequer passou da fase de análise (verificada usando SET PARSEONLY ON;) devido a:

Msg 102, Nível 15, Estado 1, Linha xxxxx
Sintaxe incorreta próxima a '.'.

Por outro lado, ambos os métodos funcionaram para os parâmetros de Função Definida pelo Usuário:

SELECT MyUDF(SchemaName.UdtName::[StaticField]);

-- and:

DECLARE @Var = SchemaName.UdtName = N'something';
SELECT MyUDF(@Var.[InstanceProperty]);

Portanto, o melhor que você realmente pode fazer é usar o SQLCLR para ter algo que funcione diretamente com UDFs, TVFs, UDAs (suponho) e consultas e, em seguida, atribuir a variáveis ​​locais quando precisar usar com procedimentos armazenados:

DECLARE @VarInt = SchemaName.UdtName::[StaticField];
EXEC MyStoredProc @ParamName = @VarInt;

Essa é a abordagem que adotei quando há uma oportunidade de ter um valor de enum real (em oposição a um valor de pesquisa que deve estar em uma tabela de pesquisa específica para seu uso / significado).


Com relação a tentar fazer isso com uma Função Definida pelo Usuário (UDF) para cuspir o valor "constant" / "enum", não consegui fazer com que isso funcionasse em termos de transmiti-lo como um parâmetro de Procedimento Armazenado:

EXEC MyStoredProc @ParamName = FunctionName(N'something');

retorna o erro "Sintaxe incorreta", com o SSMS destacando tudo entre parênteses, mesmo que eu substitua a string por um número, ou o parêntese correto, se não houver um parâmetro para passar.

Solomon Rutzky
fonte