Cache de variáveis no PHP (Parte 2)

Em: 26/11/2007 Tags: PHP Comentários (4) Referencie do seu blog (Trackback)

Na primeira parte desta série tratei da utilização da palavra-chave static como forma de manter uma variável PHP em cache. No exemplo dado, mantinha-se a tabela “role” completa em cache (de fato apenas os campos “id” e “name” eram mantidos, mas todas demais colunas da tabela poderiam ter sido mantidas em cache).

Minha tabela “roles” possuirá, no máximo, em torno de 20 registros, e por isso um cache completo da tabela não foi problemático. Mas pode ser interessante realizar o cache de tabelas maiores, com muitos milhares de registros, por exemplo. E aí entra a técnica que a partir de agora chamarei de “cache preguiçoso”.

A idéia do cache preguiçoso é a seguinte: você sabe que terá de carregar para a variável em cache (no nosso exemplo, $roles) um grande número de registros, mas não sabe de antemão quais são. Como carregar todos os registros de uma única vez pode levar muito tempo e/ou consumir muita memória, podemos acessar o banco apenas quando for necessário, sob demanda. Confira o exemplo a seguir.

function get_role_id($name) {

    global $db;

    static $roles;

    if (empty($roles)) {
        $roles = array(); // inicializa o cache
    }

    if (array_key_exists($name, $roles)) {
        print "NAO acessei o BD...\n";
        $role_id = $roles[$name]; // cache nao eh atualizado
    }

    else if ($role_id = $db->get_role_id_from_name($name)) {
        print "Acessei o BD...\n";
        $roles[$name] = $role_id; // coloca mais um valor no cache
    }

    else {
        $role_id = null;
    }

    return $role_id;
}

A implementação do cache preguiçoso é bastante simples. Na primeira vez que a função é chamada o cache ($roles) é inicializado. Nas chamadas subseqüentes verifica-se se $roles possui um valor associado à chave que chega como parâmetro. Nas primeiras chamadas da função é natural que as chaves ainda não existam, e o banco é acessado (através do objeto $bd) para recuperar o registro em questão.

Quando uma chave que já foi recuperada do banco é solicitada novamente, então não é mais preciso acessar o banco de dados (ela está no cache!). O programa abaixo utiliza essa função e ilustra seu funcionamento.

print "  id do papel 'admin' = "  . get_role_id('admin')."\n";
print "  id do papel 'editor' = " . get_role_id('editor')."\n";
print "  id do papel 'guest' = "  . get_role_id('guest')."\n";

print "  id do papel 'admin' = "  . get_role_id('admin')."\n";
print "  id do papel 'editor' = " . get_role_id('editor')."\n";
print "  id do papel 'guest' = "  . get_role_id('guest')."\n";

print "  id do papel 'writer' = " . get_role_id('writer')."\n";

A saída do programa:

Acessei o BD...
  id do papel 'admin' = 1
Acessei o BD...
  id do papel 'editor' = 2
Acessei o BD...
  id do papel 'guest' = 4
NAO acessei o BD...
  id do papel 'admin' = 1
NAO acessei o BD...
  id do papel 'editor' = 2
NAO acessei o BD...
  id do papel 'guest' = 4
Acessei o BD...
  id do papel 'writer' = 3

Cache de variáveis no PHP (Parte 1)

Em: 26/11/2007 Tags: PHP Comentários (9) Referencie do seu blog (Trackback)

Projetos PHP costumam sofrer de uma alta dose de variáveis globais. Em PHP, por padrão, as variáveis ficam acessíveis apenas no escopo onde são declaradas. Quando dentro de uma função, por exemplo, só é possível utilizar uma variável global se a primeira linha dessa função possui a palavra-chave global:

function funcao_com_2_var_globais() {
    global $var_1, $var_2;

    // implementacao
}

Neste exemplo, funcao_com_2_var_globais() tem acesso a $var_1 e a $var_2. Embora seja simples, portanto, importar variáveis globais para o contexto local, o código pode se tornar mais difícil de manter, caso essas variáveis não tenham acesso somente-leitura. Em um sistema onde diversas funções podem editar o estado de uma variável global é complicado saber qual o seu valor em um dado momento da execução do programa.

Em certos casos variáveis globais são utilizadas sem necessidade, onde uma variável declarada como static poderia tornar o mesmo código mais simples.

O código com a qual estou lidando essa semana envolve a utilização da função get_role_from_id_name. “Role”, no caso, é uma tabela do meu banco de dados, a qual armazena papéis que usuários podem desempenhar no sistema. O objetivo dessa função é receber um nome abreviado para um papel (”admin”, por exemplo), e retornar o valor da chave primária referente ao registro cujo campo “name” possui valor igual a essa string.

O autor do código original decidiu utilizar a variável global $roles para armazenar um mapeamento (array associativo) entre nomes de papéis e id’s de papéis, conforme o código abaixo (as declarações “print”, obviamente, não fazem parte do código original).

function get_role_id_from_name($name) {

    global $db, $roles;

    if (empty($roles)) {
        print ">> Acessei o BD...\n";
        $_roles = $db->get_roles();
        foreach ($_roles as $_role_id => $_role) {
            $roles[$_role['name']] = $_role_id;
        }
    }

    else {
        print ">> NAO acessei o BD...\n";
    }

    return $roles[$name];
}

Para testar o código acima podemos executar este script:

print "id do papel 'admin' = "  . get_role_id_from_name('admin')."\n";
print "id do papel 'editor' = " . get_role_id_from_name('editor')."\n";
print "id do papel 'guest' = "  . get_role_id_from_name('guest')."\n";

… que nos daria a seguinte saída:

Acessei o BD...
  id do papel 'admin' = 1
NAO acessei o BD...
  id do papel 'editor' = 2
NAO acessei o BD...
  id do papel 'guest' = 3

Como a função get_role_id_from_name() é bastante usada na aplicação, é interessante minizar o acesso ao banco para realizar essa tarefa. Mas em nome da otimização acabou sendo introduzida uma variável global desnecessária. Desnecessária pois ela será utilizada apenas nessa função, e em nenhuma outra.

Podemos resolver esse problema de forma parecida com ajuda da palavra-chave static, conforme a nova versão da função.

function get_role_id_from_name($name) {

    global $db;

    static $roles;

    if (empty($roles)) {
        print "Acessei o BD...\n";
        $roles = array();
        $_roles = $db->get_roles();
        foreach ($_roles as $_role_id => $_role) {
            $roles[$_role['name']] = $_role_id;
        }
    }

    else {
        print "NAO acessei o BD...\n";
    }

    return $roles[$name];
}

A declaração static $roles; indica ao PHP que queremos manter em cache a variável $roles. Quando a variável estiver vazia (na primeira vez em que a função é executada), acessaremos o banco de dados (através do objeto $db) para inicializá-la. Nas próximas chamadas a essa função, no entanto, $roles não estará empty(), pois o PHP guardou o valor atribuído a ela na primeira chamada.

Nosso script de testes anterior retornaria, portanto, a seguinte saída:

Acessei o BD...
  id do papel 'admin' = 1
NAO acessei o BD...
  id do papel 'editor' = 2
NAO acessei o BD...
  id do papel 'guest' = 3

Ou seja, temos exatamente a mesma funcionalidade, mas com uma variável global a menos no nosso código.

Se decidir adotar esta técnica em um projeto, não esqueça que na maior parte das situações você deseja sempre trabalhar com a última versão dos registros do banco de dados, e não com uma versão em cache. No caso apresentado, a execução do programa não insere nem remove dados da tabela “roles”, e por isso foi adequado mantê-la em cache.


Ruby versus PHP: minha comparação subjetiva e desleal (Parte 1)

Em: 23/11/2007 Tags: PHP, Ruby Comentários (4) Referencie do seu blog (Trackback)

Meu trabalho atual envolve muita exportação de dados entre diferentes bancos de dados relacionais. Hoje me deparei com a seguinte situação: precisava extrair dados de uma base MySQL, formatá-los apropriadamente em um arquivo de texto, e então inseri-los no Moodle através de sua interface administrativa (que aceita arquivos tabulares como forma de cadastrar usuários “em lote”).

Minha tabela de origem possuía um campo fullname, mas o Moodle só aceita firstname e lastname. Portanto, ou o meu SELECT quebraria esses campos ou então eu utilizaria alguma linguagem para pré-processar esses arquivos.

E foi aí que Ruby deu conta do recado muito bem. Essa dica vale para qualquer operação rápida de manipulação de arquivos que você não saiba fazer dentro do MySQL ou do Vim.

Minha consulta SQL gerava a seguinte saída (colunas nome de usuário, senha, nome completo e email):

fulano,123456,Fulano da Silva,fulano@teste.com
beltrano,123456,Beltrano da Silva,beltrano@teste.com

Mas o que eu queria era (note a vírgula entre o primeiro nome e o sobrenome):

fulano,123456,Fulano,da Silva,fulano@teste.com
beltrano,123456,Beltrano,da Silva,beltrano@teste.com

Ah, claro, se eu tivesse apenas duas linhas no arquivo eu não teria escrito o programa. Na verdade eram mais de 100. Então decidi escrever o seguinte programa Ruby para me ajudar.

#!/usr/bin/env ruby

File.open(ARGV[0]) do |f| 
  f.each_line do |line|
    username, password, full_name, email = line.split(',')
    first_name, last_name = full_name.split(' ', 2)
    STDOUT << [username, password, first_name, last_name, email].join(',')
  end 
end 

Para utilizar o script, primeiro o defini como executável:

chmod a+x formatador.rb

E em seguida o executei, passando como único parâmetro o caminho para o arquivo de origem. Como estamos utilizando STDOUT para escrever a saída, o resultado do script é impresso no seu terminal (ele é a saída padrão). Você pode direcionar essa saída para um arquivo utilizando o caracter > (no Linux/Unix da vida).

./formatador.rb entrada.txt > saida.txt

A vantagem óbvia de se utilizar STDOUT em vez de se criar outro arquivo dentro do código-fonte é que podemos testar rapidamente o comportamento do script ao deixar a saída ser impressa no terminal.

Embora eu tenha PHP como a minha linguagem de programação “materna”, nem pensei em escrever esse script com ela. Mas enquanto escrevia este artigo pensei “Vamos ver quanto tempo eu levo…”. E isso foi há uns 20 minutos. Isso é algo que me irrita no PHP: eu sempre tenho dificuldade pra escrever as mesmas coisas, sejam elas simples ou complexas. A ordem dos parâmetros de funções da biblioteca padrão geralmente é aleatória, você não sabe se a função leva underscore ou não, você esquece o nome dela (mas você já a utilizou tantas vezes!)… A questão é que enquanto eu levei menos de 5 minutos pra escrever a versão em Ruby, não concluí a versão em PHP por não ter paciência pra tentar descobrir o bug do programa. Para os interessados, a idéia básica era esta.

Note que ele é praticamente uma “tradução literal” do programa Ruby, feitas certas ressalvas:

  • Em Ruby você não precisa fechar o arquivo que você abriu, pois o método File#open faz isso pra você depois que a sua callback é executada. Em PHP você precisa fechar o arquivo explicitamente (em geral esqueço disso).
  • Enquanto em Ruby você itera sobre as linhas de um arquivo utilizando o intuitivo File#each_line (eu nunca me esqueci do nome desse método), em PHP você utiliza o medonho fgets() (já perdi a conta de quantas vezes precisei procurar por esse método na documentação oficial, pois a cada 2 meses sem utilizá-lo esqueço que ele sequer existe, hehe).

Confesso que o PHP poderia ser uma linguagem muito pior: a versão PHP do programa ficou tão compacta quanto a versão Ruby graças a vários “métodos-irmãos” entre as duas linguagens:

  • Em PHP a função list() é utilizado para permitir as atribuições simultâneas que Ruby suporta “nativamente”
  • Em PHP a função explode() quebra uma string em um array. Em Ruby utilizamos Array#split
  • Em PHP a função implode() constrói um array a partir de uma string. Em Ruby utilizamos Array#join

Resumo da ópera: sempre que possível utilize Ruby em vez de PHP. Sua saúde mental agradece.


Conversation Preview no Gmail: não utilizem

Não, não estou apresentando mais um script GreaseMonkey. Estou apenas dando um alerta: não utilize o script “Conversation Preview”, que você pode ativar dentro da aba “Messages” das configurações da Better Gmail.

Esse script acrescenta ao Gmail o atalho v, que pode ser utilizado quando uma mensagem em uma listagem está marcada. Pressionando essa tecla um efeito JavaScript tenta exibir o que deveria ser um preview do conteúdo da mensagem selecionada. Mas isso não é o que de fato ocorre. Na área central dessa janelinha que se abre você verá apenas a mensagem “Loading…” indefinidamente, pois você acaba de ser desconectado do Gmail!.

Por que esse script te desconecta? Não faço a menor idéia, por isso digo: não utilizem.

O teste foi realizado com a interface antiga do Gmail.


Better Gmail e Folders4Gmail

Better Gmail é uma extensão para o Firefox que facilita a ativação de diversos scripts GreaseMonkey dentro da interface do Gmail. Com a nova interface do leitor de emails muitos desses scripts deixaram de funcionar, por isso recomendo a instalação da versão antiga da extensão. Instale o Better Gmail 2 se você deseja utilizar a versão compatível com a nova interface, e a Better Gmail 1.1 se você deseja continuar com a antiga. Eu recomendo, no momento, a Better Gmail 1.1.

Após a instalação o menu “Tools” ganha um novo item, “Better Gmail”. Acesse essa opção para ativar as novas funcionalidades.

Além da integração com Google Reader e Calendar, um dos scripts que ativei dentro da Better Gmail 1.1 foi o Folders4Gmail. Com ele você pode organizar seus labels em uma árvore de labels e sub-labels. Por exemplo, se você possui labels como “Lista Ruby-BR”, “Lista RSpec” e “Lista Ruby”, você pode organizá-los, visualmente, sob um único label ao criar um label adicional “Lista” e renomear os labels originais para “Lista\Ruby-BR”, “Lista\RSpec” e “Lista\Ruby”, respectivamente. A contra-barra é o caracter mágico dessa extensão. Esta imagem deixa mais claro esse conceito.


JBehave: BDD em Java

Para os programadores Java interessados em experimentar o Desenvolvimento Dirigido por Comportamento (Behaviour Driven Development ou simplesmente BDD), recomendo uma olhada no projeto JBehave. Como o conheci hoje não posso dar grandes dicas de uso (quem sabe em um artigo futuro).

A propósito, aguardem para breve o material (abrangente) que estou preparando sobre a utilização do RSpec dentro do Rails. RSpec é também um framework voltado ao BDD, mas implementado em Ruby. Pretendo, com isso, popularizar o uso dessa grande ferramenta entre os falantes do Português.

Para se ter uma idéia da escassez de conteúdo sobre ambos os frameworks, uma busca no Google por páginas brasileiras referenciando JBehave e RSpec retorna apenas 50 e 455 referências, respectivamente. No caso do RSpec esse número ainda engana, pois há referências a esse nome em outras áreas que não o BDD em Ruby.


Tamanho de um array em Ruby

Em: 12/11/2007 Tags: Ruby Comentários (0) Referencie do seu blog (Trackback)

Em Ruby, para descobrir o tamanho de um array ao menos 3 métodos podem ser utilizados. Confira neste artigo se você sabe utilizá-los corretamente.

Vamos começar com um exemplo trivial:

arr = [1, 2, 3]
arr.length # => 3
arr.size   # => 3

Até aí nenhuma novidade, certo? O método Array#length devolve o tamanho de um array (Array#size é apenas um alias para Array#length). Mas observe o código abaixo:

arr = Array.new
arr[5] = 'caio'
arr.size # => Qual o resultado?

Você errou se disse que o resultado é 1. O resultado é 6. Isso porquê o índice 5, inicializado no nosso exemplo, corresponde ao sexto elemento do array. Do índice 0 a 4 também temos valores inicializados: todos esses índices possuem o valor nil.

arr[0..4] # => [nil, nil, nil, nil, nil]

Mas e se quisermos saber o tamanho de um array excluindo os valores nil? Para essas situações devemos utilizar Array#nitems.

arr = Array.new
arr[5] = 'ruby'
arr[10] = 'caio'
arr.length # => 11
arr.nitems # => 2

Conclusão: se você inicializar os índices de um array manualmente, de maneira não-contínua, você deve lembrar que Array#length e Array#nitems provavelmente retornarão valores diferentes.


Melhorias para os comentários do blog

Para facilitar a vida dos leitores que comentam os posts instalei dois plugins no blog:

  • Ajax comment preview: acrescenta no formulário de submissão de comentário o botão “Preview”, que permite ao usuário prever (com Ajax) como ficará a formatação do texto.
  • Subscribe to comments: acrescenta no formulário de submissão de comentário a opção “Quero receber os comentários deste post no meu email”. Não dá pra ter mais comodidade que isso, hein. Também é possível cancelar o recebimento desses emails para o post em questão.

E algo que esqueci de mencionar no formulário dos comentários é que os posts são formatados com Markdown. Como ninguém é obrigado a conhecer esse formato há um link na página de comentários de cada post, referenciando um artigo da Wikipedia-PT que ensina o básico da sintaxe Markdown.

Continuem comentando ;)


Importando dados default no Rails com YAML

Migrações são a forma “Rails” de resolver o problema da atualização de versões de uma aplicação web, uma vez colocada em produção. Cada migração em geral declara, em seu método up, a definição de uma tabela e a criação de registros default para a mesma. A criação desses registros pode ser feita com puro código Ruby, mas quando precisamos inserir mais de, digamos, 20 registros, pode ser mais interessante utilizar código YAML para fazer esse trabalho.

Considere a migração que cria uma tabela de categorias:

class CreateCategories < ActiveRecord::Migration
  def self.up
    create_table :categories do |t| 
      t.column 'name', :string, :null => false
    end 
  end 

  def self.down
    drop_table :categories
  end 
end

A tabela categories, de fato, não é muito interessante: possui apenas o campo name.

E aqui entra a técnica que justifica este artigo: em vez de inicializar os registros default como instâncias de um Model, é possível utilizar a classe Fixtures para importar esses dados de um arquivo YAML. Observe a migração abaixo:

require 'active_record/fixtures'

class LoadDefaultCategories < ActiveRecord::Migration
  def self.up
    Fixtures.create_fixtures("#{RAILS_ROOT}/db/migrate/defaults", 'categories')
  end 

  def self.down
    Categories.delete_all
  end 
end

Ok, o código ficou bem pequeno, parece interessante… mas o que essa migração faz? A primeira linha importa a classe Fixtures. Precisamos dela, obviamente, pois ela sabe lidar muito bem com arquivos YAML. A única linha do método up realiza duas tarefas:

  • Transforma os dados contidos no arquivo YAML db/migrate/defaults/categories.yml em instâncias do nosso modelo Category
  • Insere os dados (ActiveRecord::Base#save) na nossa base de desenvolvimento

O método Fixtures#create_fixtures recebe dois parâmetros importantes:

  • O primeiro é o caminho para o diretório de fixtures que se deseja importar. Sim, de fato não estamos criando fixtures (mais sobre isso no fim do artigo).
  • O segundo é o nome de um model (presente nesse diretório) que se deseja importar. 'categories', por exemplo, será correspondente ao arquivo categories.yml dentro daquele diretório. Esse arquivo deve seguir o padrão de definição de fixtures do Rails.

O arquivo db/migrate/defaults/categories.yml poderia ter o seguinte conteúdo:

ruby:
  id: 1
  name: 'ruby'

rails:
  id: 2
  name: 'rails'

vim:
  id: 3
  name: 'vim'

Não inventei essa técnica, mas perdi a referência onde a aprendi, infelizmente. Ela é uma pequena gambiarra dentro do Rails, evidentemente, pois utiliza uma classe (Fixtures) que tem por objetivo carregar dados YAML para dentro da base de testes durante a execução de unit testes ou specs. Como a técnica não apresentou problemas até agora, continuarei com ela até tropeçar em uma solução melhor.


Nova interface do Gmail

Em: 08/11/2007 Tags: Gmail Comentários (2) Referencie do seu blog (Trackback)

Em 29 de outubro foi anunciada uma reconstrução da interface do Gmail. Acredito que muitos usuários já tenham disponíveis em suas contas a opção de mudar para a nova “cara”, e gostaria de compartilhar com os leitores minhas impressões.

Entre os aspectos positivos:

  • Navegação tornou-se mais rápida
  • Tela de gerenciamento de contatos melhorou muito
  • Novos atalhos de teclado (veja listagem deles mais abaixo)

… mas toda mudança traz seus problemas:

  • Meu tema carregado pela Stylish não funciona mais
  • A integração com o Google Reader não funciona mais
  • A integração com o Google Calendar não funciona mais
  • O uso da nova interface é “compulsório”: você até pode escolher trocar pela antiga versão, mas essa troca não é preservada em autenticações posteriores

Como sou viciado em usabilidade e interfaces orientados para teclado (o Vim não me deixa mentir), aproveito para destacar os novos atalhos do Gmail (acho que eles não poderiam ter sido mais felizes nas escolhas desses atalhos, pois tornarão a navegação - pelo menos a minha - muito mais veloz):

  • <Shift> + i: marca a mensagem como lida
  • <Shift> + u: marca a mensagem como não-lida
  • [: arquiva a mensagem atual e move para a anterior
  • ]: arquiva a mensagem atual e move para a próxima
  • z: desfaz a última ação (desde que ela tenha gerado a mensagem de “Undo” no topo da interface)
  • <Shift> + n: atualiza a conversação (ideal quando alguém manda uma resposta para o email que você está lendo)

Próxima página »