19 de março de 2014

Configurando sites do IIS com Delphi e WMI - Part I

Dois dos produtos no portfólio da ABC71 são soluções para internet e que, portanto, necessitam de configurações num servidor de aplicações web antes de ficarem disponíveis para os usuários. Configurar o IIS (Internet Information Services) manualmente em si nem é tão complexo mas, dada a quantidade de detalhes envolvidos, acaba se tornando um processo bastante sujeito a falhas.

Minimizamos as possibidades de erro deste processo criando um instalador em Delphi para realizar a parte mais braçal. Ele se baseia nos recursos da tecnologia WMI (Windows Management Instrumentation) para o IIS, o que permite criar sites e pools de aplicação, gerenciar as propriedades de ambos, adicionar tipos de arquivos reconhecidos pela aplicação (MIMEs), aplicar permissões de acesso, iniciar, interromper e reciclar sites, etc.

As classes WMI para acessar esses recursos não são instalados por padrão com o IIS mas podemos garantir que eles estejam presentes com o DISM, programa do Windows que serve, entre outras coisas, para gerenciar quais recursos do sistema operacional estarão habilitados. A linha de comando a seguir habilita as ferramentas para gerenciar o ISS via WMI; ela deve ser executada no servidor do IIS com o usuário Administrador para ter efeito.
dism.exe /Online /Enable-Feature /FeatureName:IIS-WebServerManagementTools /FeatureName:IIS-ManagementScriptingTools

O comando acima pode ser extendido para incluir a instalação do IIS e ativar os recursos que sejam necessários para o funcionamento do seu site, tais como extensões e filtros ISAPI, aplicações ASP ou CGI, etc.

Falei em outra ocasião aqui no blog sobre o WMI, tecnologia da Microsoft para administração centralizada de diversos aspectos de um computador. No post Obtendo nível de sinal do Wifi usando WMI com Delphi eu importei a interface COM do WMI, obtendo acesso às funções desse mecanismo. Neste post, usarei um recurso do Delphi para tornar o acesso mais prático: late binding; ou seja, acessarei diretamente os nomes das funções e propriedades de uma classe WMI, deixando para a linguagem resolvê-los em tempo de execução.

Antes de partir para qualquer tipo de ação, precisamos obter em nosso programa uma instância do WMI. O WMI é organizado hierarquicamente, tendo uma raiz (o namespace) para cada grupo de aspectos gerenciáveis do computador, representados por classes. Dado um nome de computador e um namespace, podemos obter a respectiva instância do WMI através da função abaixo:
function GetWMIObject(wmiHost, wmiRoot: string): IDispatch;
var chEaten: Integer;
BindCtx: IBindCtx;
Moniker: IMoniker;
objectName : String;
begin
{ Monta o nome do objeto WMI desejado }
objectName := Format('winmgmts:\\%s\%s',[wmiHost,wmiRoot]);
OleCheck(CreateBindCtx(0, bindCtx));

{ Obtém um "moniker" para o objeto indicado pelo nome montado a partir do host e do root }
OleCheck(MkParseDisplayName(BindCtx, StringToOleStr(objectName), chEaten, Moniker));

{ Recupera o IDispatch para facilitar o uso do objeto no programa }
OleCheck(Moniker.BindToObject(BindCtx, nil, IDispatch, Result));
end;

O resultado deste código é a instância do WMI criada como uma interface genérica IDispatch. Para ter acesso aos recursos do IIS no computador local, chamamos esta função passando o valor '.' como Host e 'root\WebAdministration' como raiz.
var FWmiObj : IDispatch;
begin
FWmiObj := GetWMIObject('.', 'root\WebAdministration');
{ ... }

De posse da instância do WMI, agora podemos iniciar a administração de sites propriamente dita. No caso do ERP Pronto da ABC71, nós criamos um Application Pool específico para isolar a execução de nossa aplicação, protegendo-a de problemas que eventualmente surjam em outros sites no mesmo servidor. Usando o WMI, podemos perguntar ao IIS se o pool já está criado e, então, criá-lo se for necessário:
var FObj, FEnum, FItem, FAppPool: OLEVariant;
lEnum: IEnumVariant;
FNome: String;
qtde: LongWord;
begin
FObj := FWmiObj;
FNome := 'ERPProntoServApp';
FENum := FObj.ExecQuery('SELECT * FROM ApplicationPool WHERE Name="' + FNome + '"', 'WQL', 0)._NewEnum;

{ Recupera instância do IEnumerateVariant para poder navegar pelos registros encontrados. }
lEnum := IUnknown(FEnum) As IEnumVariant;

{ Navega para o 1o registro do conjunto. Se achou, FItem contem o item encontrado. }
if (lEnum.Next(1, FItem, qtde) <> 0) then begin
{ Não achou, cria o pool aqui }
FAppPool := FObj.Get('ApplicationPool');
FAppPool.Create(FNome, false);

{ Localiza o pool recém criado para poder configurá-lo }
FENum := FObj.ExecQuery('SELECT * FROM ApplicationPool WHERE Name="' + FNome + '"', 'WQL', 0)._NewEnum;

lEnum := IUnknown(FEnum) As IEnumVariant;
if (lEnum.Next(1, FItem, qtde) <> 0) then
Raise Exception.Create ('Não foi possível criar pool ' + FNome);
end;

{ Outras configurações para o Pool }
FItem.AutoStart := true;
FItem.Enable32BitAppOnWin64 := true;
FItem.ManagedPipelineMode := 0; {integrado }
FItem.ManagedRuntimeVersion := ''; { sem .NET / código gerenciado }
FItem.RapidFailProtection := false;

{ Efetiva as alterações, gravando-as no IIS }
FItem.Put_();
{ ... }

Para forçar o Delphi a usar late binding, declaro uma variável OLEVariant e lanço nela o objeto WMI instanciado. Com isso, posso inquerir a classe ApplicationPool sem me preocupar em declarar a função ExecQuery do WMI. O resultado da chamada a essa função é um variant contendo um IEnumVariant, interface que permite percorrer a lista de registros encontrados. Como este resultado não implementa a interface IDispatch, o Delphi não consegue descobrir seus métodos e propriedades automaticamente, razão pela qual a conversão explícita é obrigatória neste caso. Já para os itens extraídos do enumerado, podemos contar com o late binding normalmente, conforme demonstra o código.

Se o ApplicationPool com o nome proposto não for encontrado pela consulta, o programa cria o pool. Isto é feito obtendo uma instância estática da classe de pools e invocando o método Create dela. Após isto, o pool já está criado e pode ser posicionado normalmente através de query.

Nossa aplicação é construída em C++ 32 bits, isto é, sem código gerenciado da plataforma .NET. Assim, o final do código é reservado para garantir que o pool execute corretamente neste ambiente, ligando as configurações apropriadas e gravando-as no IIS.

Num próximo post, mostro como criar a aplicação no IIS e associá-la ao pool criado aqui.

17 de janeiro de 2014

Facilitando a criação de queries complexas no SQL Server

Criar certas queries para um banco de dados pode ser um trabalho bastante intrincado. É o caso de consultas cujos filtros (cláusula WHERE) são baseados em cálculos independentes, bem como atualizações que são vinculadas ao resultado de alguma das funções de agregação (como SUM, MIN ou MAX). O cenário fica ainda pior se a manutenção em questão envolver mais de uma tabela, necessitando acessar outros dados através de joins.

Em geral, estas situações podem ser resolvidas mais facilmente usando-se cursores ou outra técnica de programação similar. Para quem usa o SQL Server, no entanto, a solução pode ser bem simples. Desde a versão 2005, esse gerenciador de Banco de Dados dispõe de um recurso chamado CTE - Common Table Expression ou "Expressão de Tabela Comum".

Basicamente, a CTE constrói uma tabela temporária com os registros resultantes de um comando SELECT especificado. A tabela temporária é, então, disponibilizada para o próximo comando SQL executado na mesma conexão com o banco de dados. Assim, esse segundo comando pode executar qualquer query (SELECTs, UPDATEs, DELETEs, INSERTs) referenciando a CTE como se este fosse uma tabela ordinária do banco de dados. Após esse comando ser executado, o SQL Server remove a tabela temporária criada pela CTE.

A sintaxe para declararmos uma CTE consiste na palavra chave WITH seguida pelo nome da tabela temporária, uma lista opcional para indicar os nomes que os campos assumirão, a palavra chave AS e, entre parênteses, a query propriamente dita. O quadro a seguir retrata a sintaxe descrita acima:
WITH nome-da-expressão [ ( nome-coluna-1 [ ,...nome-coluna-n ] ) ] AS
(
texto-do-SELECT
)
Para exemplificar na prática o uso desse recurso, considere o seguinte cenário: você possui uma tabela para registrar todas as alterações feitas num determinado documento em seu banco de dados. Cada alteração é cadastrada com um novo valor sequencial num campo que é parte da chave dos registros.

Em um determinado momento, você decide fazer uma faxina nos registros, limpando o banco de dados para manter apenas a última alteração registrada de cada documento. Como cada documento possui uma quantidade diferente de alterações registradas, não há um número de sequência que você possa usar como corte. A solução, então, é calcular a maior sequência de cada documento e remover os registros cujas sequências sejam menores que o valor calculado em cada documento diferente.

Veja abaixo uma definição simplificada da tabela e um conjunto hipotético dos registros contidos nela:
NUMERO_DOC TIPO_DOC SEQUENCIA USUARIO XML_ALTERACOES
1 NOTA FISCAL 0 GUSTAVO 0x00012010322032...
1 NOTA FISCAL 1 FULANO 0x00012A10962032...
1 NOTA FISCAL 2 GUSTAVO 0x012018650322027...
2 NOTA FISCAL 0 FULANO 0x012018650322027...
3 NOTA FISCAL 0 FULANO 0x012018650322027...
3 NOTA FISCAL 1 FULANO 0x012018650322027...
1 DUPLICATA CR 0 MANOELA 0x012018650322027...
1 DUPLICATA CR 1 MANOELA 0x012014603546520...
1 PEDIDO 0 JOSE 0x012014603520989...
Primeiro, vamos montar a CTE. Devemos obter um registro para cada documento diferente, calculando junto o maior número de sequência de alteração existente para cada um deles. Conseguimos isso facilmente com uma agregação simples:
WITH sel_docs AS (
SELECT numero_doc, tipo_doc, MAX(sequencia) AS sequencia
FROM docs
GROUP BY numero_doc, tipo_doc
)
Ao declarar esta CTE, omiti o nome das colunas na tabela resultante. Desse modo, os nomes adotados pelo SQL Server correspondem aos nomes na query interna. A coluna contendo o MAX não tem um nome por padrão, razão pela qual usei a cláusla AS para forçar num nome na própria query. No geral, é uma boa prática nomear explicitamente as colunas para evitar conflitos e tornar mais clara a operação como um todo.

A tabela abaixo mostra o conteúdo da nossa CTE, à qual dei o nome de sel_docs:
NUMERO_DOC TIPO_DOC SEQUENCIA
1 NOTA FISCAL 2
2 NOTA FISCAL 0
3 NOTA FISCAL 1
1 DUPLICATA CR 1
1 PEDIDO 0
Agora, a montagem do comando seguinte da conexão SQL considera o quadro acima como uma tabela do nosso banco de dados. Podemos, então, escrever o comando DELETE proposto no exemplo como um JOIN entre a CTE e a tabela original (DOCS):
DELETE docs FROM sel_docs
WHERE docs.numero_doc = sel_docs.numero_doc
AND docs.tipo_doc = sel_docs.tipo_doc
AND docs.sequencia < sel_docs.sequencia
O comando SELECT que compõe uma CTE pode ter cláusula WHERE e dispor de JOINS, se necessário. Poderíamos, portanto, ter restringido o tipo de documento na CTE do nosso exemplo ou até mesmo usar o resultado de algum cálculo envolvendo trechos do texto no campo XML_ALTERACOES para construir uma restrição mais elaborada.

Em tempo: o CTE está previsto na padronização do SQL ANSI-99; por isso, outros gerenciadores de banco de dados também implementaram esse recurso. O Oracle, por exemplo, o introduziu em sua versão 9i release 2, com sintaxe praticamente idêntica à apresentada neste post. Mais detalhes podem ser encontrados no artigo WITH Clause : Subquery Factoring.