quinta-feira, 12 de agosto de 2010

Declarações IN e NOT IN no Linq to Entity

Esses dias eu quebrei a cabeça para fazer uma declaração IN e NOT IN em minha query.
Enfim, eu tinha pensado que já tinha arrumado uma solução, que seria utilizar o Contains:

var query =

from i in dm.inspecoes

where i.idtipo_operacao == 6

&& !(from i2 in dm.inspecoes

where i.idtipo_operacao == 5

select i2.chassi)

.Contains(i.chassi)

select i;

foreach (var c in query) Console.WriteLine( c );

Nossa, fiquei super alegre em saber que eu poderia utilizar o Contains, que geraria uma clausula CONTAINS no comando SQL final.
Dai fui implementar...e...nada! :S
Dava o seguinte erro:

LINQ to Entities does not recognize the method 'Boolean Contains[String](System.Linq.IQueryable`1[System.String], System.String)' method, and this method cannot be translated into a store expression.

Igual diz o Milton Leite: "Que beleza!"
Pesquisando na internet, vi que o Entity não tem suporte para o método Contains. Dai pensei: Que padronização maravilhosa! essa é a Microsoft! :(

A SUBQUERY
Uma forma que eu arrumei de solucionar esse problema, é utilizar a clausula Where da minha entidade.
Primeiro de tudo, eu separei a subquery em uma outra variavel.
Nessa subquery, nós possuimos todas as ocorrências que nós queremos inserir dentro da clausula IN:

var subQuery = (from i2 in dm.inspecoes
where idtipo_operacao == 5
select new { chassi = "'" + i2.chassi + "'" });

Como o campo do meu critério é do tipo VARCHAR e não INT, eu preciso inserir uma apostrofe antes e depois do valor de cada campo, dai o resultado seria: { '123455abcde', '987654fedcba'}
Até ai beleza, montamos nossa SubQuery.
Agora, precisamos passar os valores para um array, para que posteriormente tenhamos os valores dentro da clausula IN. Nossa array fica dessa forma:

string[] resultados;
resultados = subQuery.Select(r => r.chassi).ToArray();

Para finalizar, basta inserirmos a clausula IN ou NOT IN em nossa query principal:

A QUERY PRINCIPAL E A SOLUÇÃO
var queryFinal = dm.inspecoes.Where("it.idtipo_operacao = 6 AND it.chassi IN {" + string.Join(",", resultados) + "}");

Notem que eu usei como alias para a minha tabela a sigla "it". Esse alias é criado pelo Emtity por padrão para a entidade selecionada.

Depois, com esse resultado você pode voltar um list ou um array.
return queryFinal.ToList();
return queryFinal.ToArray();

OBSERVAÇÕES
Como o Entity Framework não tem suporte para o Contains, esse foi um método que consegui obter o resultado que queria. Porém, se formos pensar bem, teremos uma queda de desempenho considerável em nossa query.
O que a query acima faz, é o seguinte:
SELECT * FROM inspecoes i WHERE i.chassi NOT IN ('331311233', '2323434244', '2342324234', '2343232423');

Lembra que passamos o resultado da subquery para um array? Então, esse array nada mais é que o seguinte resultado: { '331311233', '2323434244', '2342324234', '2343232423' }

Se o Entity desse suporte para o Contains, o resultado final seria:
SELECT * FROM inspecoes i WHERE i.idtipo_operacao = 6 AND i.chassi NOT IN (SELECT chassi FROM i i2 WHERE i2.idtipo_operacao = 5)

A consulta SQL seria mais rápida, porque o SGBD não precisará verificar valor por valor do conteudo que esta na clausula IN. Sem contar que podemos ter o campo "chassi" indexado, o que ajudaria no desempenho de nossas consultas.

To passando um perrengue danado com esse Entity, mas chego lá!!

quinta-feira, 29 de julho de 2010

Menu dinâmico buscando itens da base de dados (ASP.Net)

Fala pessoaL!
Nossa, faz muito tempo que não posto nada e para falar a verdade, faz tempo que eu nem acesso meu blog. Estou sem tempo e com muitas preocupações. Viajarei para a Australia no fim desse ano e isso esta me deixando ansioso e muito nervoso. Correria total!!
Bom, como o blog não é sobre mim mas sim sobre análise e desenvolvimento de sistemas, vamos ao que interessa.

Devido a um novo projeto da empresa, comecei a programar em asp.net C# há poucos dias. Precisei fazer um controle de login dos usuários e claro, um menu contendo todos os menus que o usuário tem acesso.
Só que esses menus estão cadastrados na base de dados. E agora?
Não achei nada na internet, nenhum blog de pessoas que já fizeram isso em asp.net e disponibilizaram.

TECNOLOGIAS E FERRAMENTAS UTILIZADAS

Estou utilizando o modelo entidade relacional Entity Framework da Microsoft, acessando uma base de dados MySQL (the best database ever! ;) )

Não irei tratar aqui sobre como fazer os relacionamentos das tabelas ou criação e associação entre as classes do nosso modelo objeto relacional, mas sim como criar o menu para o site e seus respectivos filhos.

TABELAS USADAS NO EXEMPLO


Eu utilizei uma tabela com relacionamento Self One to Many. Para quem não conhece esse tipo de relacionamento, vale a pena dar uma olhada nesse outro post em meu blog:

http://devutils.blogspot.com/2008/12/sql-relacionamentos-self-to-self.html

CREATE DATABASE `exemplomenu`

USE `exemplomenu`

DROP TABLE IF EXISTS `menus_web`;

CREATE TABLE `menus_web` (
`idmenus` int(7) unsigned NOT NULL AUTO_INCREMENT,
`nome` varchar(45) NOT NULL COMMENT 'nome do item de menu',
`ordem` int(7) unsigned DEFAULT NULL COMMENT 'para ordenação dos itens de menu',
`pai` int(7) unsigned DEFAULT NULL,
`hint` varchar(255) DEFAULT NULL,
`bitmap` int(7) DEFAULT NULL COMMENT 'icone do item de menu',
`url` varchar(50) DEFAULT NULL,
PRIMARY KEY (`idmenus`),
KEY `fk_menus_menus1` (`pai`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=latin1;

/*Data for the table `menus_web` */

insert into `menus_web`(`idmenus`,`nome`,`ordem`,`pai`,`hint`,`bitmap`,`url`) values (1,'Veículos',50,NULL,NULL,NULL,''),(2,'Relatórios',NULL,1,NULL,NULL,''),(3,'Por responsabilidade',NULL,2,NULL,NULL,'/veiculos/relatorios/responsabilidade.aspx'),(4,'Avarias por montadora',NULL,7,NULL,NULL,''),(5,'Avarias por modelo',NULL,7,NULL,NULL,''),(6,'Avarias por peça',NULL,7,NULL,NULL,''),(7,'Gráficos',NULL,1,NULL,NULL,''),(8,'Por responsabilidade e período',NULL,2,NULL,NULL,''),(9,'Algodão',NULL,NULL,NULL,NULL,''),(10,'Relatórios',NULL,9,NULL,NULL,''),(11,'Sair',NULL,NULL,NULL,NULL,'sair.aspx');


CRIAÇÃO DO MENU

Utilizarei o componente Menu, que se encontra na aba Navigation da ToolBox. Vamos deixar o ID dele como Menu1.

Repare que o menu possui uma propriedade chamada MaximumDynamicDisplayLevels. Essa propriedade diz quantos nodes filhos o menu poderá ter.

//Objeto do tipo MenuItem (node de um menu)
MenuItem vMenu;
//Objeto array, do tipo MenuItem
MenuItem[] vMenu2 = new MenuItem[5];
int vCont = 0;

Primeriamente vamos gerar um procedimento que nos traz todos os menus pais, ou seja, que contenha o valor pai igual a vazio.

private void GerarMenu()
{
using (kerpModel.kerpModelEntities dm = new kerpModel.kerpModelEntities())
{
//SELECIONA OS DADOS DO MENU POR MEIO DO LINQ
var consMenu = (from m in dm.menus_web
where m.pai == null
select m);


//CORRE OS REGISTROS DO MENU
foreach(kerpModel.menus_web row in consMenu)
{
//Cria um novo node para o menu
vMenu = new MenuItem(row.nome.ToString());

vMenu.NavigateUrl = row.url.ToString();
Menu1.Items.Add(vMenu);
//Chama o procedimento que verifica se o menu possui filhos, passando o id do menu como parametro
this.GerarMenuFilho(int.Parse(row.idmenus.ToString()));
}
}
}


//Função que adiciona os filhos do item de menu
private void GerarMenuFilho(int idpai)
{
using (kerpModel.kerpModelEntities dm = new kerpModel.kerpModelEntities())
{
//SELECIONA OS DADOS DO MENU, QUE TEVE O ID PASSADO POR PARAMETRO
var consMenu = (from m in dm.menus_web
where m.pai == idpai
select m);

//CORRE OS REGISTROS DO MENU
foreach (kerpModel.menus_web row in consMenu)
{
//Chama a função que verifica se o menu atual também possui filhos
if (totalFilhos(int.Parse(row.idmenus.ToString())) > 0)
{
//Adiciona o menu ao array de menus, para que ele possa ser recuperado posteriormente
vMenu2[vCont] = vMenu;
//Variavel que conta quantos nodes principais o menu possui
vCont++;
//Adiciona o filho atual ao menu e ao mesmo tempo diz que agora o novo menu é ele mesmo
vMenu.ChildItems.Add(vMenu = new MenuItem(row.nome.ToString(), null, null, row.url.ToString(), null));
//Chama novamente a função, e adiciona seus filhos
this.GerarMenuFilho(int.Parse(row.idmenus.ToString()));
}
else
{
//Caso o item não possua filos, apenas o adiciona como um novo node
vMenu.ChildItems.Add(new MenuItem(row.nome.ToString(), null, null, "~"+row.url.ToString(), null));
}
}

//Aqui esta o segredinho. Foi para isso que eu criei o contador. Cada vez que o node e seus filhos sao criados, ainda é necessário criar os outros nodes faltantes, mas esses nodes precisam ser inseridos em seus pais. Cada vez que o cursor passa por aqui, dizemos que o menu atual é o menu pai anterior. Dificil de entender, né? para entender, debuge a aplicação.
vCont--;
if(vCont >= 0)
vMenu = vMenu2[vCont];

}
}

//Função que verifica se o menu possui filhos
private int totalFilhos(int idmenu)
{
int count = 0;
using (kerpModel.kerpModelEntities dm = new kerpModel.kerpModelEntities())
{
count = (from m in dm.menus_web
where m.pai == idmenu
select m).Count();
}
return count;

}


//Para finalizar, no nosso Page_Load nós chamamos o procedimento
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
this.GerarMenu();
}
}

Pessoal, peço desculpas por estar ausente do blog. Esta muito dificil de acessá-lo. Mas assim que for possível, responderei a seus comentários. Obrigado.