Mostrando postagens com marcador Soluções. Mostrar todas as postagens
Mostrando postagens com marcador Soluções. Mostrar todas as postagens

quinta-feira, 4 de novembro de 2010

Instalando o SQL Server 2008 e problema com Setup.rll

Estava instalando o SQL Server 2008 em uma VM que já possui o SQL Server 2008 R2 instalado, quando me deparei com o seguinte erro "SQL Server 2008R2\Resouces\1033\Setup.rll is either not designed to run on Windows or it contains an error tryinstalling the program again using the original installation media". Clicando em OK aparecia outra mensagem: "The loading and initialization of setup.rll failed with error code: 0800700C1".
Nada bom, pesquisei rapidinho na Internet mas nada me ajudou, então resolvi dar uma olhada no que temos e pensar um pouco.


Fui até o diretório informado "C:\Program Files\Microsoft SQL Server\100\Setup Bootstrap\Release\Resources\1033" e tentei abrir o rll, mas como não é um XML não nos ajuda. Como já instalei o R2 nessa máquina, suspeitei que o arquivo poderia ser de outra versão do SQL Server (já que o R2 tem como versão 100.50), então se procurarmos na mídia de instalação encontraremos um "setup.rll".

Data do arquivo no diretório: 19/07/2009
Data do arquivo na mídia de instalação: 10/07/2008

Bom, muito bom.
Troquei o nome do arquivo mais recente para setup_new.rll e copiei o arquivo da mídia de instalação para o diretório. Executei o setup e tudo funcionou corretamente.

Dica boba, mas espero que ajude alguma alma.

[]s
Luciano Caixeta Moreira - {Luti}
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm
www.srnimbus.com.br

sexta-feira, 1 de outubro de 2010

SQL Server 2008 R2 install - Could Not Find Database Engine Startup Handle

Hoje eu resolvi montar uma nova VM e comecei pela instalação do SQL Server 2008 R2 em uma máquina virtual. Estou rodando o VirtualPC 2007 SP1 no Win 7 x64 e a máquina virtual está rodando o Windows Server 2008.

Seguindo normalmente a instalação que já fiz 48934932 vezes, tudo ia bem até que “Could Not Find Database Engine Startup Handle”. As ferramentas eram instaladas, mas a engine relacional nada. Fiz alguns testei, mudei algumas coisas e nada! Sempre que tentava instalar o SQL Server ele parava no mesmo ponto.

Pesquisando um pouco encontrei este artigo que me foi útil: http://www.msbicentral.com/Resources/Articles/tabid/88/articleType/ArticleView/articleId/80/FLASH-SQL-Server-2008-R2-Error-Could-Not-Find-Database-Engine-Startup-Handle.aspx.

Para não queimar uma mídia peguei o ISO, fiz um extract com o WinRar e copiei os arquivos para dentro da VM. Reiniciei a instalação e tudo funcionou perfeitamente.

Espero que possa ajudar.

[]s

Luciano Caixeta Moreira - {Luti}
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm
www.srnimbus.com.br

segunda-feira, 20 de setembro de 2010

Sim, é um deadlock

Se você prefere ler o artigo em PDF, baixe aqui.


Revisando os posts da última semana eu encontrei um post interessante do nosso amigo Alexandre Lopes, que mostra um exemplo bem legal de deadlock. O post na íntegra está aqui: http://sqlserverday.com.br/alopes/?p=924 e aconselho lê-lo antes de continuar com
este post. Leu tudo? Então vamos a uma análise mais detalhada.


Monitorando com o profiler os eventos lock:acquired e lock:released, execute no banco de dados AdventureWorks a consulta “SELECT * FROM Sales.CurrencyRate” e veremos (Figura 01) que o lock manager optou por pegar um bloqueio de página, então temos um IS no objeto e, durante a leitura dos registros, o SQL Server vai pegando e liberando os bloqueios compartilhados na medida em que os registros das páginas são lidos.
clip_image002
(Figura 01)

Agora vamos ao deadlock, com base no script abaixo:

(Script 01)

USE AdventureWorks
go

select @@TRANCOUNT

-- Conexão 01
BEGIN TRAN

UPDATE Sales.CurrencyRate SET AverageRate = 2 WHERE CurrencyRateID =1
SELECT * FROM Sales.CurrencyRate

-- Script 02
BEGIN TRAN

UPDATE Sales.CurrencyRate SET AverageRate = 2 WHERE CurrencyRateID = 1

Quando iniciamos a transação da conexão 01 e executamos o primeiro update, veremos o SQL Server pegando um lock exclusivo no registro e IX na página e tabela (Figura 02).

clip_image004
(Figura 02)

Se a partir de outra conexão executarmos o mesmo comando de update, o que é de se esperar? Que o SQL Server requisite os mesmos recursos e a transação fique bloqueada pelo registro exclusivo colocado sobre a chave. Vejamos a saída do SP_LOCK para análise na figura 03.

clip_image006
(Figura 03)

Vemos claramente que a conexão de SPID 55 ficou bloqueada (WAIT) esperando que a conexão 52 libere o bloqueio exclusivo sobre o registro que está sendo atualizado. Outro ponto que eu quero chamar a atenção é para o bloqueio IX da página 1040, que foi concedido para a transação de SPID 55.

Se buscarmos no BOL o tópico “Lock Compatibility” veremos que IX são compatíveis com IX e até existe uma nota sobre isso: “An intent exclusive (IX) lock is compatible with an IX lock mode because IX means the intention is to update only some of the rows rather than all of them. Other transactions that attempt to read or update some of the rows are also permitted as long as they are not the same rows being updated by other transactions”.


Porém se analisarmos a tabela de compatibilidade, veremos que o bloqueio compartilhado (S) não é compatível com o IX, pois eu não poderia ler uma página em que algum registro estivesse sendo alterado, seria uma leitura suja não compatível com o nível de isolamento read committed. Acho que vocês já devem ter percebido onde eu quero chegar...


Se nesse momento nós voltarmos para a primeira transação que está com o bloqueio exclusivo no registro e executarmos o SELECT para buscar todos os registros, o que veremos? Sim senhor, um deadlock. E os recursos envolvidos são detalhados na figura 04.

clip_image008
(Figura 04)

Analisando a saída acima, vemos as sessões 52 e 55 se bloqueando, então estamos em uma situação de deadlock, onde o SQL Server deve escolher uma vítima. Os recursos envolvidos são o registro bloqueado pelo lock exclusivo e a página 1040, onde a transação do SPID 52 fica bloqueada com o status CNVT (Convert). Pela documentação do SQL Server: “CNVRT: The lock is being converted from another mode, but the conversion is blocked by another process holding a lock with a conflicting mode”.

Então o deadlock aconteceu porque estamos trabalhando em níveis de granularidade diferentes, um na página e outro no registro! Se o que eu acabei de afirmar for verdade, esse tipo de deadlock pode ser resolvido com uma hint no SELECT, onde indicamos que o SQL Server deve pegar bloqueios de registros, e não de página: “SELECT * FROM Sales.CurrencyRate (ROWLOCK)”. Faça o teste e verá que realmente dessa forma o deadlock não irá acontecer.

Conclusão

O comportamento do SQL Server está correto e é realmente um deadlock, então vou discordar do Alexandre em relação à qualidade do produto e que não existe um deadlock, mas sim tentar pensar como um program manager do produto. Qual é o maior ganho para o SQL Server? Permitir bloqueios de registro e utilizar o IX para indicar que existe algum recurso com menor granularidade sendo bloqueado (acelerando a análise de compatibilidade dos bloqueios) ou evitar esse comportamento impedindo bloqueios de registro ou análises custosas de compatibilidade de bloqueios em diferentes níveis (imagine bilhões de registros, um bloqueio exclusivo em um registro e outra transação pedindo um bloqueio compartilhado na tabela). Eu também ficaria com o row lock.

No fim eu gostei bastante de fazer essa pequena análise, pois é um excelente exemplo (e não muito comum) de um deadlock que o Alexandre trouxe para nós, possibilitando mostrar para vocês um pouco mais do SQL Server e seu funcionamento.


Abraços e até um próximo artigo.

[]s
Luciano Caixeta Moreira - {Luti}
luciano.moreira@srnimbus.com.br www.twitter.com/luticm www.srnimbus.com.br

terça-feira, 22 de junho de 2010

Exceções em testes de unidade para banco de dados

Como todo bom geek que se preza, estou tentando usar os diversos recursos que temos disponíveis nas nossas ferramentas e que muitas vezes ficam em segundo plano, pois acabamos viciados no que nos é comum.

Com um extremo cuidado estou tentando criar testes para meus projetos e recursos auxiliares, para garantir um kit profissional, com garantia da qualidade e confiabilidade, então nada mais natural que recorrer aos testes de unidade.
 Ao invés de trabalhar com o nosso tradicional UnitTest.cs e codificar todos os setups e testes em C#, resolvi usar o velho e bom DataDude (AKA. Visual Studio Database Professional), que na versão do 2010 me parece somente estar incluso nas versões Premium e Ultimate (http://www.microsoft.com/visualstudio/en-us/products).

Usando meu projeto de teste eu adicionei o item "Database Unit Test" e codifiquei para a procedure chamada proc_CriaSnapshot, uma série de testes (bem como o setup destes) utilizando o Transact-SQL. Durante a codificação eu achei relativamente simples a criação dos testes, mas não gostei da maneira que foi fornecida para tratar os erros gerados pelo teste.

Segundo a documentação (com escassas referências na web - nenhuma que me ajudou) a maneira que temos para verificar se uma exceção foi gerada, é a mesma que utilizamos para os testes de unidade tradicionais, isto é, utilizando atributos sobre a declaração do método. Então temos que colocar a chamada do procedimento sem nenhuma validação (figura 01) e depois, diretamente no código fonte decorar o método com o atributo ExpectedSqlException, conforme script 01.


(Figura 01)

(Script 01)
[TestMethod(), ExpectedSqlException(MatchFirstError=true, MessageNumber=50000, Severity=16, State=1)]
public void TesteException() { ... }

Executei o teste e funcionou tudo beleza, mas não me agradou por dois motivos.
  1. Por ser um procedimento que vou utilizar em vários ambientes eu optei por não atribuir um número específico aos erros gerados dentro do procedimento, isso significa que todos os meus erros terão o 50000 como número.
    1. Então o meu teste unitário pode passar como certo (capturando a exceção), mesmo se a exceção gerada for relativa a um outro problema diferente do que eu espero. Esse não determinismo no teste não é nada interessante, concordam.
    2. Se eu forçar um número (suponha 50001), caso chegue em um ambiente que já exista esse número registrado, terei que alterar o procedimento e os meus testes, o que também não me agrada.
  2. Até esse momento todos os testes estavam com asserções definidas através da interface, e não no código fonte por debaixo dos panos. Alguém desavisado poderia olhar para o teste sem nenhuma condição de teste e apagá-lo.
    1. Ok, eu posso colocar um comentário direto no T-SQL dizendo: "-- Esse teste possui no code-behind um ExpectedSqlException...". Mas não gostei nem um pouco disso.
Alternativa ao ExpectedSqlException

Como alternativa para o problema eu pensei inicialmente em estender o Visual Studio criando meu próprio TestCondition, que é detalhado na documentação do produto: Define Custom Conditions for Database Unit Tests (http://msdn.microsoft.com/en-us/library/dd193282.aspx).

Além de ser uma abordagem mais trabalhosa (digna da metáfora canhão vs. mosca) analisando os passos que eles mostram no walkthrough, é utilizado como ponto de verificação o recebimento de uma conexão e resultado através do método Assert. Mas se uma exceção for gerada pelo código, possivelmente o Assert nunca será invocado, o que me pareceu claramente ser a razão de não existir já disponível no Visual Studio 2010 uma test condition para exceções. Acho que vale a pena depois aprofundar um pouco mais no assunto, mas não terei tempo para isso agora.

Por fim resolvi utilizar o seguinte código (script 02) na chamada ao procedimento:

(Script 02)
BEGIN TRY
        EXEC CleansingKit.dbo.proc_CriaSnapshot 'TesteSnapshot', 'C:\DiretorioInexistente\'
END TRY
BEGIN CATCH
        SELECT 'Exceção correta foi gerada.'
        WHERE ERROR_MESSAGE() LIKE 'Directory lookup for the file % failed with the operating system error 2(The system cannot find the file specified.).'
END CATCH

Com essa abordagem eu consigo verificar exatamente a mensagem do erro (poderia também testar o ERROR_NUMBER) e mostrar a string "Exceção correta foi gerada.". Então com uma simples condição para verificação de um valor escalar (figura 02), que deve ser a string retornada pelo SELECT, conseguimos verificar se a exceção correta foi recebida.


(Figura 02)
Nessa abordagem fica claro o que estamos tentando fazer com o teste, podemos verificar exatamente qual é a mensagem de erro (inclusive usando wildcards como o "%" para informações que variam) e caso nenhuma exceção seja gerada o catch não é invocado e o teste não passa na asserção feita pelo Visual Studio.

O que você acha dessa abordagem? Como faz seus testes unitários para procedures?
[]s
Luciano Caixeta Moreira - {Luti}
Sr. Nimbus Serviços em Tecnologia Ltda
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm

quarta-feira, 7 de abril de 2010

Fuzzy lookup e tipo de dados da coluna Confidence

Post rápido com uma pequena solução.

Estou usando o fuzzy lookup em um projeto do SSIS, para tentar tirar algumas inconsistências das tabelas, tentando encontrar registros semelhantes como "Luciano Caixeta Moreira" e "Luciano CaiZeta Moreira", por exemplo.

Tudo indo certo, montei a fonte, transformação fuzzy lookup configurada e criei uma nova tabela PessoaFuzzy para armazenar o resultado do que for encontrado. Fui executar o pacote e ... Pau!

"[OLE DB Destination [104]] Error: SSIS Error Code DTS_E_OLEDBERROR. An OLE DB error has occurred. Error code: 0x80004005.
An OLE DB record is available. Source: "Microsoft SQL Server Native Client 10.0" Hresult: 0x80004005 Description: "OLE DB provider 'STREAM' for linked server '(null)' returned invalid data for column '[!BulkInsert]._Confidence'."


O erro já direciona o foco para onde está o problema, campo Confidence, mas qual dado é inválido? Coloquei alguns data viewers no pipeline do SSIS para tentar no zoiômetro pegar alguma coisa fora do padrão (estava testando em uma pequena massa de registros) e nada aparente.

Vamos nos voltar para o "pai-dos-desenvolvedores-com-pressa-que-não-querem-gastar-muito-tempo-com-besteira".Uma rápida pesquisa e voilá, solução encontrada.

Quando o SSIS gera a tabela de destino o campo Confidence, o tipo de dados fica como REAL, mas é necessário um FLOAT para armazenar o resultado gerado pelo SSIS! Alterado o campo, tudo funcionou corretamente.

Agora, sinceramente, isso me cheira a um BUG feio. Não era para o BIDS já gerar um campo float de cara? Na verdade eu até estou querendo evitar essa palavra, pois nas duas últimas semanas eu estou passando por uma série de casos que me cheiram a bug e estou ficando um pouco alucinado com isso.

Fica aí uma referência em português sobre o assunto.

[]s
Luciano Caixeta Moreira - {Luti}
Chief Innovation Officer
Sr. Nimbus Serviços em Tecnologia Ltda
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm

terça-feira, 23 de março de 2010

[Artigo] Herança no EF4 e Identity no SSDL

Este artigo descreve brevemente o uso de herança (type per hierarchy) no Entity Framework 4.0, mas destaca um problema que enfrentei com o modelo de entidades quando estava fazendo o mapeamento a partir do banco de dados.
No fim o problema talvez poderá ser resolvido com o RTM do Visual Studio 2010 ou continuar em aberto (existe uma entrada no connect), então pode ser que você precise de uma mãozinha sobre o assunto.

No artigo eu também comento sobre as exceções “A member named cannot be defined in class . It is defined in ancestor class ” e “A value shared across entities or associations is generated in more than one location. Check that mapping does not split an EntityKey to multiple store-generated columns”.

Parti do pressuposto que você já conhece um pouco do Entity Framework, caso contrário, eu já gravei alguns webcasts sobre o assunto e um artigo que escrevi sobre as novidades do EF 4.0 deve sair qualquer dia desses em uma publicação nacional.

Quer ver o artigo completo? Baixe-o aqui.



[]s
Luciano Caixeta Moreira - {Luti}
Chief Innovation Officer
Sr. Nimbus Serviços em Tecnologia Ltda
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm

sexta-feira, 19 de fevereiro de 2010

Colunas UNIQUE e NULLs

Se quiser baixar o PDF e o script que utilizei, clique aqui.

Durante o último treinamento do SQL Server 2008 Internals, tive mais uma vez o prazer de usufruir de uma das grandes vantagens de ser instrutor, que é aprender com os alunos, então compartilho com vocês.
Estávamos discutindo sobre a utilização do NULL e eu joguei na sala a pergunta: Como fazemos para manter a unicidade de uma coluna e ainda permitirmos diversos valores nulos?

(Pausa para respirar e pensar um pouquinho)

No SQL Server a unicidade dos valores em uma coluna é garantida através de índices marcados como UNIQUE (cluster ou não) e uma vez inserido um NULL, nenhum outro NULL pode ser adicionado a tabela, pois é um valor duplicado.
É interessante ver esse comportamento de igualdade de nulos em uma constraint, pois se testarmos a igualdade de um nulo através de uma consulta, veremos que NULL é diferente de NULL (ele é desconhecido).

SELECT 'Comparando'
WHERE 1 = 1

SELECT 'Comparando'
WHERE NULL = NULL
go

Então como você resolve esse problema?

- Uma abordagem seria trabalhar com triggers na tabela, garantindo a unicidade dos valores não nulos.
- Particularmente não gosto dessa abordagem, por prolongar a transação e, se necessário, efetuar um rollback da mesma.

- Poderíamos garantir a unicidade através da aplicação ou SPs, mas aí temos que garantir que ninguém vai conseguir inserir um registro "por fora".
- Essa é uma abordagem interessante por evita o rollback, mas a falta de controle e de informações para o query optimizer (como no primeiro caso) não é legal.

- Outra abordagem que mostro no treinamento, seria criarmos uma coluna computada que em combinação com a coluna original (que precisa garantir unicidade para não-nulos) deve ser única. Essa coluna computada condicionalmente recebe um valor único (o campo da PK, por exemplo) caso o campo original seja nulo ou recebe NULL caso ele não seja nulo. Dessa forma poderíamos criar uma constraint UNIQUE nas colunas original e calculada, garantindo assim a unicidade não-nula.
- Gosto dessa abordagem porque trabalhamos com constraints e não preciso confiar em terceiros para que a regra seja respeitada.


Entendeu a explicação da terceira solução? Bem, eu já li vinte vezes o que escrevi e não entendi nada, então segue um exemplo para exemplificar melhor o que eu disse. :-)

USE tempdb
go

-- Criando a tabela de teste
IF (OBJECT_ID('Funcionario') IS NOT NULL)
DROP TABLE Funcionario
go

CREATE TABLE Funcionario
(
Codigo INT IDENTITY NOT NULL,
Nome VARCHAR(200) NOT NULL,
CNPJ CHAR(14) NULL)
go

ALTER TABLE Funcionario
ADD CONSTRAINT UNQ_Funcionario_CNPJ
UNIQUE (CNPJ)
go

ALTER TABLE Funcionario
ADD CONSTRAINT PK_Funcionario
PRIMARY KEY (Codigo)
go

-- Inserts OK
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar', '000.000.000-00')
go

-- Ambos os INSERTs abaixo irão trazer problema por conta da constraint UNIQUE
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno 2', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar 2', '000.000.000-00')
go

-- Reconstruindo a tabela com a coluna computada
IF (OBJECT_ID('Funcionario') IS NOT NULL)
DROP TABLE Funcionario
go

CREATE TABLE Funcionario
(
Codigo INT IDENTITY NOT NULL,
Nome VARCHAR(200) NOT NULL,
CNPJ CHAR(14) NULL,
CNPJNulo AS (CASE WHEN CNPJ IS NULL THEN Codigo ELSE -1 END)
)
go

ALTER TABLE Funcionario
ADD CONSTRAINT PK_Funcionario
PRIMARY KEY (Codigo)
go

ALTER TABLE Funcionario
ADD CONSTRAINT UNQ_Funcionario_CNPJ
UNIQUE (CNPJ, CNPJNulo)
go

-- Inserts OK
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar', '000.000.000-00')
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Caio do Botafogo', '000.000.000-01')
go

-- Vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno 2', NULL)
go

-- Não vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar 2', '000.000.000-00')
go


Melhorou?
Dessa forma conseguimos garantir a unicidade antes que o valor seja inserido na tabela, sem a necessidade de criação de triggers.


SQL Server 2008

Agora que vem a sacada, enquanto estava falando sobre isso o amigo Burgos me perguntou: eu não conseguiria resolver esse problema utilizando índices com filtro?

(Momento de silêncio na sala)

Caramba! Se a unicidade é garantida através de índices e eu posso criar um índice com o predicado "IS NOT NULL", então provavelmente filtered index deve resolver o problema! Testamos e bang! Na mosca.

Eu tinha ficado tão focado nos ganhos de desempenho e tamanho dos índices com filtro que nunca tinha parado para pensar nessa utilização! Nada melhor do que dar aula e aprender também = Doscendo discimus.

Vamos ao exemplo…

-- Somente para SQL Server 2008
-- Resolução com filtered indexes
IF (OBJECT_ID('Funcionario') IS NOT NULL)
DROP TABLE Funcionario
go

CREATE TABLE Funcionario
(
Codigo INT IDENTITY NOT NULL,
Nome VARCHAR(200) NOT NULL,
CNPJ CHAR(14) NULL)
go

ALTER TABLE Funcionario
ADD CONSTRAINT PK_Funcionario
PRIMARY KEY (Codigo)
go

CREATE UNIQUE NONCLUSTERED INDEX idx_CNPF
ON Funcionario (CNPJ)
WHERE CNPJ IS NOT NULL
go

-- Inserts OK
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno', NULL)
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar', '000.000.000-00')
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Caio do Botafogo', '000.000.000-01')
go

-- Vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Ronaldo Fenômeno 2', NULL)
go

-- Não vai funcionar
INSERT INTO Funcionario (Nome, CNPJ) VALUES ('Nilmar 2', '000.000.000-00')
go


Viu que realmente funciona?!
Espero que a solução pré-SQL Server 2008 e a nova abordagem possam ajudar você no dia-a-dia.

Só fiquei agoniado com uma coisa nessa abordagem, relacionado com o Query Optimizer, mas vou fazer alguns testes e depois coloco aqui minhas considerações.

[]s
Luciano Caixeta Moreira - {Luti}
Chief Innovation Officer
Sr. Nimbus Serviços em Tecnologia Ltda - www.srnimbus.com.br
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm

terça-feira, 24 de novembro de 2009

Gerando script das views para suas tabelas

Bom dia pessoal.

Hoje eu estava trabalhando em um cliente e precisei fazer uma coisa bem manual: Criar uma série de views com um nome diferente da tabela que estamos consultando, mas contendo todos os campos da tabela original.
Qual o motivo disso? Nós estamos criando um ambiente temporário onde estou jogando um monte de informações e vamos expor uma "interface" usando visões, que o usuário de negócio vai poder consultar à vontade e eventualmente criar consultas e relatórios. Então usaremos essa abstração, que nesse momento refletirá boa parte das tabelas, para evitar um pouco de retrabalho e atrito entre os lados, caso a estrutura mude, e dividir bem a questão de segurança.

Agora que vocês estão contextualizados vamos ver o que bolei... Eu poderia simplesmente sair escrevendo umas 50 visões com todos os campos, mas isso iria levar um tempão, então montei um script rápido que me ajudaria a gerar o código que preciso.

Seu mecanismo básico é o seguinte: tenho uma tabela temporária com N registros contendo o nome do esquema, da tabela existente e o nome que quero dar para a view. Bom base nessa tabela eu utilizo o CROSS APPLY para gerar uma string usando informações da sys.objects, sys.columns e sys.schemas, usando o truque com XML que já coloquei aqui no blog (http://luticm.blogspot.com/2009/06/gerar-registros-em-forma-de-colunas.html).

Segue o código T-SQL utilizando o AdventureWorks2008 para vocês brincarem e, quem sabe, utilizarem em algum momento, customizando o que será gerado.

USE AdventureWorks2008
go

WITH TabelaView AS
(SELECT Esquema, Tabela, Visao
FROM ( VALUES
('Sales', 'SalesOrderHeader', 'Venda'),
('Sales', 'SalesOrderDetail', 'DetalheVenda'),
('Production', 'Product', 'Produto'))
AS T(Esquema, Tabela, Visao))

SELECT
CodigoViews.Instrucao
FROM TabelaView
CROSS APPLY
(SELECT
'
IF OBJECT_ID(''vw_'+ TabelaView.Visao +''') IS NOT NULL
DROP VIEW dbo.[vw_'+ TabelaView.Visao +']
go

CREATE VIEW dbo.vw_' + TabelaView.Visao + '
WITH SCHEMABINDING
AS
SELECT ' +
STUFF(
(SELECT N', ' + QUOTENAME(SC.name) AS [text()]
FROM SYS.columns AS SC
INNER JOIN sys.objects AS SO
ON SO.object_id = SC.object_id
INNER JOIN sys.schemas AS SS
ON SO.schema_id = SS.schema_id
WHERE SO.type = 'U'
AND SO.name = TabelaView.Tabela
AND SS.name = TabelaView.Esquema
FOR XML PATH('')), 1, 2, N'') + '
FROM ' + TabelaView.Esquema + '.' + TabelaView.Tabela + '
go'
AS Instrucao) AS CodigoViews
go

E o código gerado é esse aqui:

IF OBJECT_ID('vw_Venda') IS NOT NULL
DROP VIEW dbo.[vw_Venda]
go

CREATE VIEW dbo.vw_Venda
WITH SCHEMABINDING
AS
SELECT [SalesOrderID], [RevisionNumber], [OrderDate], [DueDate], [ShipDate], [Status], [OnlineOrderFlag], [SalesOrderNumber], [PurchaseOrderNumber], [AccountNumber], [CustomerID], [SalesPersonID], [TerritoryID], [BillToAddressID], [ShipToAddressID], [ShipMethodID], [CreditCardID], [CreditCardApprovalCode], [CurrencyRateID], [SubTotal], [TaxAmt], [Freight], [TotalDue], [Comment], [rowguid], [ModifiedDate]
FROM Sales.SalesOrderHeader
go

IF OBJECT_ID('vw_DetalheVenda') IS NOT NULL
DROP VIEW dbo.[vw_DetalheVenda]
go

CREATE VIEW dbo.vw_DetalheVenda
WITH SCHEMABINDING
AS
SELECT [SalesOrderID], [SalesOrderDetailID], [CarrierTrackingNumber], [OrderQty], [ProductID], [SpecialOfferID], [UnitPrice], [UnitPriceDiscount], [LineTotal], [rowguid], [ModifiedDate]
FROM Sales.SalesOrderDetail
go

IF OBJECT_ID('vw_Produto') IS NOT NULL
DROP VIEW dbo.[vw_Produto]
go

CREATE VIEW dbo.vw_Produto
WITH SCHEMABINDING
AS
SELECT [ProductID], [Name], [ProductNumber], [MakeFlag], [FinishedGoodsFlag], [Color], [SafetyStockLevel], [ReorderPoint], [StandardCost], [ListPrice], [Size], [SizeUnitMeasureCode], [WeightUnitMeasureCode], [Weight], [DaysToManufacture], [ProductLine], [Class], [Style], [ProductSubcategoryID], [ProductModelID], [SellStartDate], [SellEndDate], [DiscontinuedDate], [rowguid], [ModifiedDate]
FROM Production.Product
go


Notem que o T-SQL é bem simples e fácil de ser alterado, então se eu quisesse omitir colunas do tipo uniqueidentifier ou remover campos com nome CodigoXXXXXXX, basta adicionar algumas cláusulas where no código.

Post rápido, mas espero que seja útil para alguém. Ou então pelo menos a idéia do T-SQL...
Você pode baixar o script aqui.

[]s
Luciano Caixeta Moreira - {Luti}
Chief Innovation Officer
Sr. Nimbus Serviços em Tecnologia Ltda
luciano.moreira@srnimbus.com.br
www.twitter.com/luticm

segunda-feira, 26 de outubro de 2009

Testes de unidade com deployment de arquivos


Olá pessoal, vamos de Visual Studio hoje.


Imagine um cenário bem comum: Estamos desenvolvendo um projeto que manipula arquivos e, para ajudar o desenvolvimento, você cria uma classe utilitária que lê do arquivo de configuração qual é o repositório local (diretório) onde os arquivos são armazenados. Essa classe auxiliar será utilizada por toda aplicação, então não existirá somente para testes. Exemplo de código abaixo:


public class Configuração
{
private static string repositórioArquivos;

static Configuração() {
repositórioArquivos = ConfigurationManager.AppSettings["RepositórioArquivos"];
}

public static string RepositórioArquivos
{
get { return repositórioArquivos; }
}
}

public void RenomearArquivo(string nomeArquivo, string novoNomeArquivo)
{
string repositório = Configuração.RepositórioArquivos;
File.Move(Path.Combine(repositório, nomeArquivo), Path.Combine(repositório,
novoNomeArquivo));
}

Antes ou depois de codificar a classe, você desenvolve alguns testes para testar seu componente, então para tudo funcionar corretamente você adiciona ao seu projeto de testes um arquivo App.Config e colocar uma entrada no AppSettings, que será lida pelo auxiliar de configuração:

[TestMethod]
public void TesteCenárioUsoBásico() {

// Pré-condições
Assert.IsTrue(File.Exists(Path.Combine(
TestesComArquivos.Configuração.RepositórioArquivos, "arquivoqualquer.txt")));

TestesComArquivos.ClasseNegocio negócio = new TestesComArquivos.ClasseNegocio();
negócio.RenomearArquivo("arquivoqualquer.txt", "novoarquivotexto.txt");

// Pós-condições
Assert.IsTrue(File.Exists(Path.Combine(
TestesComArquivos.Configuração.RepositórioArquivos, "novoarquivotexto.txt")));
}


Execute o projeto (em anexo a este artigo) e tudo funcionará perfeitamente. Quer dizer, mais ou menos, pois aqui temos alguns problemas:

1 - Depois que você executar esse teste, o arquivo estará renomado e a próxima execução vai falhar. Você corrige isso escrevendo direito as pré-condições e setup do seu teste. Por simplicidade eu deixarei como está, ok?

Tudo isso funciona corretamente se você está desenvolvendo sozinho, mas e se houver uma equipe junto com você?

2 - Em primeiro lugar, os arquivos não serão levados juntos com o projeto, então se alguém adiciona outro arquivo para teste, o que você faz? Isso garoto, adicione os arquivos ao projeto de teste, para todos os desenvolvedores terem acesso ao distinto quando pegarem uma nova versão do projeto.

3 - (Um problema de verdade) Cada desenvolvedor pode definir um diretório local diferente para seu workspace de trabalho, então se um desenvolvedor colocar os arquivos da solução em outro diretório e mudar o App.Config, quando você pegar a última versão do projeto no TFS, bye bye testes!

Uma maneira de "resolver" isso é ficar mudando o App.Config para cada desenvolvedor ou então padronizar o diretório local para o seu projeto, mas é um gato MUITO feio. Concorda? E se houverem builds diários, você vai fazer o quê? Como resolvemos isso?


  1. Adicione ao seu projeto o diretório "Arquivos" e coloque lá dentro o famoso "arquivoqualquer.txt". Lembre de marcar a opção do arquivo "Copy to output directory" com "Copy if newer".

  2. Como a cada nova execução dos testes são gerados novos diretórios com um timestamp diferente (ex.: "C:\Projects\VisualStudio\TestesComArquivos\TestResults\luciano.moreira_DSKSRN01 2009-10-26 14_26_18"), precisamos referenciar os arquivos dentro do diretório "Out", onde estão as DLLs do projeto.

    1. Como nossa propriedade em Configuração.RepositórioArquivos é somente leitura e não queremos interferir com a interface da classe, adicionarei um método internal chamado DefineRepositórioArquivos.

      internal static void DefineRepositórioArquivos(string repositório) {
      repositórioArquivos = repositório;
      }

    2. Para que esse método seja visível no nosso projeto de testes, utilizamos um pequeno recurso do .NET, definindo que os métodos internal do assembly de negócio são visíveis somente para o projeto de testes. Recurso que somente utilizo nesse tipo de cenário.

      [assembly: InternalsVisibleTo("TestesComArquivos_TesteSuite")]

    3. A partir desse momento eu faço uma pequena alteração no setup do meu teste, para que ele defina qual o repositório de arquivos de acordo com o diretório de deployment do teste. Para isso eu utilizo a classe auxiliar TestContext, que possui a propriedade DeploymentDirectory. Se você não quiser usar essa classe auxiliar, pode partir para a ignorância com o AppDomain.CurrentDomain.BaseDirectory.

      TestesComArquivos.Configuração.DefineRepositórioArquivos(TestContext.DeploymentDirectory);

    Se você executar o seu teste nesse momento irá receber um erro! Analisando com cuidado verificará que o problema está no primeiro Assert, que pergunta pela existência do arquivo, então olhando o diretório criado pelos testes notará que o arquivo não foi colocado no Out. Huummm, mas no passo 1 você já pediu para o arquivo ser copiado para o diretório de saída, não é suficiente?


    O pior é que não! Quando você compila o projeto de teste o arquivo é colocado no "\Bin\Debug" corretamente, mas não é levado para o "\TestResult\....\Out" que é criado para o teste. Para isso é necessário editar as configurações dos testes em "Local.testsettings" (dentro de Solution Items) e no Deployment adicionar o diretório "Arquivos", conforme figura abaixo.




  3. Pronto!

    Basta executar o seu teste que tudo vai funcionar e quantas vezes forem necessárias, pois cada novo teste copia o arquivo original para um novo diretório, evitando o primeiro problema que eu citei. Agora todos os seus desenvolvedores podem trabalhar tranquilamente, adicionar novos arquivos ao diretório já criado e codificar novos testes, basta lembrar de definir o diretório correto no início dos testes.

    Bom, espero que tenham gostado. O projeto que criei está disponível juntamente com o PDF do artigo. Baixe aqui.

    []s
    Luciano Caixeta Moreira - {Luti}
    Chief Innovation Officer Sr. Nimbus Serviços em Tecnologia Ltda luciano.moreira@srnimbus.com.br
    www.twitter.com/luticm