SQL – como agrupar registros por uma coluna e pegar o último pela data

Não sou bom com SQL – nunca fui, nunca quis me aprofundar e tenho um certo bloqueio com relação a isso.
Mas, algumas vezes tenho que mexer com isso, e sempre me deparo com alguns erros bobos ou algumas coisas mais complexas.

No caso, incrivelmente as questões bobas não me atrapalharam, mas quebrei a cabeça quando tinha uma tabela com uma estrutura mais ou menos semelhante a essa (vou suprimir as colunas que não interessam para o problema):

  • id: id primário, auto-incremente
  • f_id: é uma relação de cada registro com registros de outra tabela. Não é único
  • adddate: data do registro
  • nome: nome do registro

Minha necessidade era agrupar os registros por esse f_id e retornar a data mais recente de cada grupo.
À princípio estava tentando fazer um SELECT e agrupar por GROUP BY, dando um ORDER BY depois, mas percebi que uma vez usado o GROUP BY nada garante qual é o registro que ele vai usar quando der um ORDER BY.
Vi então que a coisa seria mais séria, e que teria que usar SUBSELECTS… estava quebrando a cabeça e o Alexandre (@kurko) me ajudou após um pedido de SOS pelo twitter, e a seguinte query abaixo me salvou!

SELECT u.f_id, u.nome, ( SELECT adddate FROM usuario AS t WHERE t.f_id=u.f_id ORDER BY t.adddate DESC LIMIT 1) as data FROM usuario AS u GROUP BY u.f_id ORDER BY u.f_id

Espero que seja de utilidade!

Usando a Prototype – passo 1 – escondendo selects debaixo de uma popup

Vamos lá, falar de usos práticos da biblioteca Prototype. Okey, aqui no And After há alguns posts a respeito já, mas estou pensando em criar uma série deles.
Nesse primeiro, amos ver como criar uma função que esconde determinados elementos do html se outro estiver por cima.

Não entendeu? Vamos lá, vou explicar de onde veio a idéia – acho que fica mais fácil.
Uma coisa que aparece em qualquer projeto de web ultimamente é a necessidade de se fazer caixas (normalmente divs) que abrem por cima de outras. Normalmente para mostrar conteúdos oriundos de requisições ajax (ok, sei que o termo não está correto, mas não consegui pensar em outro).
Aí, o que normalmente se faz? Tem-se uma div com display:none e no retorno da requisição (callback), dá-se um display:block nela mostrando o conteúdo da forma adequada.
O grande problema é quando essa div abre por cima de um select ou um flash, por exemplo. O IE6 mostra o select ou o swf por cima da div (não ocorre no Firefox nem IE7), acabando com o layout e dificultando tudo. Para isso fiz essa função aqui, genérica, que recebe dois parâmetros: o id do seu elemento html que estará por cima, e o id de um elemento html que contém elementos do tipo select.

Usando o objeto Position (veja a documentação aqui) da Prototype, é possível acessar o método cumulativeOffset(element) que retorna um array com as posições das coordenadas x e y do elemento, independente do local no código em que esteja (posição relativa dos pais, absoluta, floats, etc…)
Com isso, o que é feito: pega-se a posição da div de popup e suas dimensões, e se varre a lista de selects presentes no outro elemento. A cada select que é encontrado, pega-se também as coordenadas desse select e suas dimensões, e através de uma lógica simples faz a comparação nos eixos x e y se a popup está por cima do select. Se sim, deixa o select como invisível.
Segue o código:

function escondeNoOver(id_popup, id_area) {
        //define qual a popup
        var popup = $(id_popup);
        //pega todas os selects de um determinado elemento
        var selects = $(id_area).getElementsByTagName("select");
        //pega coordenadas X/Y da popup (topo do lado esquerdo) e dimensões da mesma
        var posX = Position.cumulativeOffset(popup)[0];
        var posY = Position.cumulativeOffset(popup)[1];
        var width = popup.clientWidth;
        var height = popup.clientHeight;
       
        //percorre a lista de selects
        for(i=0; i<selects.length; i++) {
           
            //pega coordenadas X/Y do select (topo do lado esquerdo) e dimensões do mesmo
            var selX = Position.cumulativeOffset(selects[i])[0];
            var selY = Position.cumulativeOffset(selects[i])[1];
            var widthS = selects[i].clientWidth;
            var heightS = selects[i].clientHeight;
           
            var onX = false;
            //verifica se no eixo X (horizontal), a popup está entre o início e o final do select
            if(posX<selX+widthS) {
                if(posX+width>selX)
                    onX = true;
            } else {
                if(selX+widthS>posX+width)
                    onX = true;
            }
           
           
            //verifica se no eixo Y (vertical), a popup está entre o início e o final do select
            var onY = false;
            if(posY<selY+heightS) {
           
                if(posY+height>selY)
                    onY = true;
            } else {
                if(selY+heightS>posY+height)
                    onY = true;
            }
           
            //tem que estar dentro de X e Y para esconder a popup
            if(onX && onY){
                selects[i].style.visibility = "hidden";
            }
        }
    }

Abaixo segue o código funcional de um html com essa função, onde ao clicar no link do final da página sera chamada a função e ela verificará se há campos do tipo select por baixo daquela div de popup que possui um fundo acinzentado.
Vale notar que essa função pode ser alterada para verificar outros tipos de objetos e não precisa ser usada apenas para essa questão de retorno de requisições ajax – muitas vezes precisamos esconder algumas coisas, independente do motivo, não? 🙂
 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Untitled Document</title>
<style type="text/css">
* {margin:0; padding:0; border:0;}
body {font-family:Verdana, Arial, Helvetica, sans-serif;}
input, select, textarea {border:1px solid #000000;}
form {margin:50px;}
p {margin:10px;}
div#popup{padding:20px; border:3px solid #000000; position:absolute; left:80px; top:350px; background-color:#F0F0F0; width:200px;}
</style>
<script type="text/javascript" src="http://www.prototypejs.org/assets/2008/1/25/prototype-1.6.0.2.js"></script>
<script type="text/javascript">
    function escondeNoOver(id_popup, id_area) {
        //define qual a popup
        var popup = $(id_popup);
        //pega todas os selects de um determinado elemento
        var selects = $(id_area).getElementsByTagName("select");
        //pega coordenadas X/Y da popup (topo do lado esquerdo) e dimensões da mesma
        var posX = Position.cumulativeOffset(popup)[0];
        var posY = Position.cumulativeOffset(popup)[1];
        var width = popup.clientWidth;
        var height = popup.clientHeight;
        //percorre a lista de selects
        for(i=0; i<selects.length; i++) {
            //pega coordenadas X/Y do select (topo do lado esquerdo) e dimensões do mesmo
            var selX = Position.cumulativeOffset(selects[i])[0];
            var selY = Position.cumulativeOffset(selects[i])[1];
            var widthS = selects[i].clientWidth;
            var heightS = selects[i].clientHeight;
            var onX = false;
            //verifica se no eixo X (horizontal), a popup está entre o início e o final do select
            if(posX<selX+widthS) {
                if(posX+width>selX)
                    onX = true;
            } else {
                if(selX+widthS>posX+width)
                    onX = true;
            }
            //verifica se no eixo Y (vertical), a popup está entre o início e o final do select
            var onY = false;
            if(posY<selY+heightS) {
           
                if(posY+height>selY)
                    onY = true;
            } else {
                if(selY+heightS>posY+height)
                    onY = true;
            }
            //tem que estar dentro de X e Y para esconder a popup
            if(onX && onY){
                selects[i].style.visibility = "hidden";
            }
        }
    }
</script>
</head>
<body>
<div id="popup">sua div de popup</div>
<form action="entreemcontato.php" method="post" id="form">
<div>
    <p>Nome:</p>
    <p><input type="text" name="nome" value="Seu Nome"/></p>
    <p>Comentários:</p>
    <p><textarea name="comentários" rows="5" cols="20">
    Seus comentários
    </textarea></p>
    <p>Você é:</p>
    <p><input type="radio" name="vocee" value="homem" checked="checked"/>
    Homem
    </p>
    <p><input type="radio" name="vocee" value="mulher"/>
    Mulher
    </p>
    <p><input type="checkbox" name="cefet" checked="checked"/>Estudo no Cefet</p>
    <p>O que você achou desse minicurso?</p>
    <select>
    <option value="bom">Bom</option>
    <option value="regular" selected="selected">Regular</option>
    <option value="ruim">Ruim</option>
    </select>
    <p>Você repetiria esse curso?</p>
    <select>
    <option value="bom">Sim</option>
    <option value="regular" selected="selected">Sempre</option>
    </select>
    <p><input type="submit" value="Enviar"/></p>
    <p><input type="reset" value="Limpar"/></p>
</div>
</form>
<a href="#" onclick="escondeNoOver(´popup´, ´form´);" title="checar popup">checar popup</a>
</body>
</html>
 

Em breve, outros exemplos usando a Prototype 🙂

Selects aninhados – fazendo uma busca

Provavelmente já existem muitos tutoriais na net de como fazer uma busca por selects aninhados usando (a metodologia) ajax. Mas, como na web sempre tem gente nova chegando, e novas abordagens são sempre bem-vindas, vou mostrar como fazer uma dessas buscas de uma forma bem simples.

O que é necessário: um banco MYSQL rodando, PHP (versão 4 já da conta) e a biblioteca javascript prototype.

Tendo os ingredientes à mão, vamos criar um banco de dados com duas tabelas:

bandas: campos ID e NOME

discos: campos ID, ID_BANDA, NOME



A idéia é que a tabela bandas liste várias bandas (sendo ID chave primária), e a tabela discos vai guardar uma lista de discos referentes às bandas inseridas na outra tabela (usando ID_BANDA para referenciar a qual a banda o disco pertence).

Segue um dump do banco:

CREATE TABLE `bandas` (
  `id` int(11) NOT NULL auto_increment,
  `nome` varchar(128) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
--
-- Extraindo dados da tabela `bandas`
--
INSERT INTO `bandas` VALUES (1, ´Iron Maiden´);
INSERT INTO `bandas` VALUES (2, ´Metallica´);
INSERT INTO `bandas` VALUES (3, ´Sepultura´);
-- --------------------------------------------------------
--
-- Estrutura da tabela `discos`
--
CREATE TABLE `discos` (
  `id` int(11) NOT NULL auto_increment,
  `id_banda` int(11) NOT NULL,
  `nome` varchar(128) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=7 ;
--
-- Extraindo dados da tabela `discos`
--
INSERT INTO `discos` VALUES (1, 1, ´Fear of the Dark´);
INSERT INTO `discos` VALUES (2, 1, ´The Number of the Beast´);
INSERT INTO `discos` VALUES (3, 2, ´Load´);
INSERT INTO `discos` VALUES (4, 2, ´Master of Puppets´);
INSERT INTO `discos` VALUES (5, 3, ´Chaos AD´);
INSERT INTO `discos` VALUES (6, 3, ´Arise´);





Tendo feito isso, monta-se uma página com um formulário que possui dois selects. Através de uma consulta SQL feita pelo PHP ,todos os registros da tabela bandas são retornados e o primeiro select é populado, colocando o value de cada option igual ao id da banda (ele será usado para fazer a consulta SQL na tabela discos de uma banda específica).

No onchange desse select, se chama uma função javascript:

– ela vai fazer uma requisição utilizando o objeto Ajax da (biblioteca) prototype, a uma página php chamada discos.php

– discos.php consulta a tabela discos e procura os discos de uma banda específica, retornando uma string no formato registro1|registro2|registro3…

– no retorno da função, por javascript é feito um split na string retornada e, com isso, recupera-se num vetor a lista de discos. Com isso, só é necessário apagar os ítens anteriores da lista, percorrer o vetor gerado pelo split e criar para cada item dessa lista uma option e adicioná-la (appendChild()) ao select.



Segue o código da página principal:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Exemplo de select aninhado</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.2/prototype.js"></script>
<script type="text/javascript">
    function buscadiscos(field){
        //requisita o serviço PHP que vai listar os discos de uma banda, pelo id
        new Ajax.Request(´discos.php?id=´ + $F(field), {
            onSuccess: function(transport) {
                //alert(transport.responseText);
                $("discos").innerHTML = "";
                var discos = transport.responseText.split("|");
                for(var i=0; i<discos.length; i++) {
                    if(discos[i]=="")
                        continue;
                    var li = new Element("li").update(discos[i]);
                    $("discos").appendChild(li);
                }
            }
        });
       
    }
</script>
</head>
<body>
<h1>Exemplo - select aninhado</h1>
<form id="busca_discos" name="busca_discos" method="POST" action="#">
    <label>banda:</label>
    <select id="bandas" onchange="buscadiscos(this);">
        <option value="-1">selecione...</option>
    <?php
        //conecta ao banco
        $link = mysql_connect(´localhost´, ´root´, ´´);
        mysql_select_db(´exemplo_select´, $link);
        //monta string que seleciona todas as bandas
        $sql = "SELECT * FROM bandas";
        $result = mysql_query($sql);
        while ($row = mysql_fetch_array($result)) {
            echo("<option value=´" . $row["id"] . "´>" . $row["nome"] . "</option>");
        }
    ?>
    </select>
    <br /><br /><br />
    <label>discos:<label>
    <select id="discos"></select>
   
</form>
</body>
</html>





Segue o código da página discos.php:

<?php
    //conecta ao banco
    $link = mysql_connect(´localhost´, ´root´, ´´);
    mysql_select_db(´exemplo_select´, $link);
    //monta string que seleciona todos os discos de uma determinada banda
    $sql = "SELECT * FROM discos WHERE id_banda=" . $_GET["id"];
    $result = mysql_query($sql);
    while ($row = mysql_fetch_array($result)) {
        echo($row["nome"] . "|");
    }
?>

 

Vale ressaltar que a maneira de discos.php retirnar os dados foi a mais simples possível – e de menor qualidade. Para melhorar o código, seria interessante retornar um XML ou retornar os dados no formato JSON (tanto o PHP5 tem métodos nativos para codificar dados em formato JSON, quando a biblioteca JSON tem para decodificar os mesmos).