segunda-feira, 18 de junho de 2012

Rails: Auxílio a migrações complexas

Oi,
Penso que estamos de acordo quanto ao Rails ser uma ferramenta poderosa, flexível (a partir da versão 3.0) e Pesada. Se não estamos, podemos mandar vir umas cervejas e discutir o assunto. Mas uma das facetas que tenho descoberto e apreciado mais desde a reformulação que atingiram com a versão 3.0 é como a dissociação dos vários módulos que introduzem comportamento específico em determinadas componentes permite que esses mesmos módulos sejam individualmente ferramentas poderosas para efectuar tarefas que reflectem esse comportamento específico, mas fora do contexto onde estes módulos são normalmente utilizados. Soou um pouco confuso, mas passo a explicar com um exemplo específico.

Há cerca de 8 meses, no projecto em que correntemente trabalho, começámos a desenvolver uma aplicação web que interagiria com os dados da aplicação principal que estávamos (ainda estamos) a manter. A decisão tomada na altura foi a de desenvolver a segunda aplicação em cima da primeira, com acesso à mesma base de dados, achando que iriamos poupar tempo com isso, sendo os dados unicamente referentes à segunda aplicação guardados em tabelas com o âmbito explicitamente definido no nome (nome-tipo: aplicacao2_users, aplicacao2_profiles, etc...), o código-fonte referente à segunda aplicação protegido dentro do mesmo âmbito (namespace), e sendo criado um encaminhamento (routing) específico para esse âmbito sobre um hóspede (host) diferente. Deste modo, conseguimos criar uma coisa inaudita, um verdadeiro "pague 2 compre 1", uma salganhada tão grande que agora estamos a sofrer as consequências dessa decisão. O que estava errado aqui? Pois, quis-se desenvolver 2 aplicações completamente diferentes, com lógica completamente diferente, uma em cima da outra, em vez de se dissociar os ambientes e posteriormente providenciar um mecanismo de sincronização para os dados comuns relevantes. Interagir com os mesmos dados não significa necessariamente interagir com a mesma representação dos mesmos. Ao invés, aumentámos uma base de código já de si gigantesca, misturámos dados de âmbitos diferentes nas mesmas tabelas, e criámos ainda mais confusão na camada de apresentação (CSS) e do scripting do lado do cliente (o qual não vou abordar em pormenor agora).

Depois de assumido o erro, chegou-se à conclusão que era necessário separar as aplicações. Qualquer coisa de herculeano para uma aplicação que já está em produção faz 6 meses. Fiquei encarregue do primeiro passo nessa direcção: criar uma segunda base de dados e migrar os dados relevantes para segunda aplicação para ela. Algumas migrações eram fáceis: somente copiar uma tabela de um lado para o outro e zás. Outras envolviam alguma dor de cabeça: uma inserção aqui, outra deleção ali, uma actualização acolá... e tudo testadinho, que era para a barra não cair quando fosse para produção.

Não sei quanto a vocês, mas o meu uso de migrações em Rails sempre foi geralmente limitado a criação de tabelas, remoção/inserção de colunas, com o ocasional preenchimento de colunas utilizando directamente o comando execute. Então o que aconteceu quando comecei a escrever as migrações? O meu conhecimento estava limitado ao execute (e também a alguns métodos de selecção), então bombei no execute para tudo o que era peixe. Saiu o maior mufufu. Aqui fica um exemplo:



# pesquisar atributos
attrs = select_all("SELECT * FROM assets i WHERE i.owner_id = 1 AND i.image_owner_type = 'User::Lawyer'")
# actualizar para a nova tabela
attrs['origin'] = "app1"

attributes, values = attrs.to_a.transpose
values.map!{|attr| attr.nil? ? "NULL" : attr.kind_of?(Time) ? "\"#{attr.to_s(:db)}\"" : attr}
execute "INSERT INTO images (#{attributes.join(",")}) VALUES (#{values.join(',')});"
image_id = Integer(select_value("SELECT id FROM images ORDER BY id DESC LIMIT 1;"))
# usar o image_id em outras operações...


É, o resultado foi o previsto. Migrações que funcionavam, testadinhas e bonitinhas, mas que ninguém conseguia ler (e com algumas limitações, se quiserem investigar esse pedaço de código mais a fundo). Surgiu mais tarde o caso em que tinha que introduzir mais algumas operações no meio desta barafunda, e foi aí que eu pensei que estava na altura de descobrir como é que realmente se fazia aquilo a que eu me propunha, e como é que o Rails fazia para gerir toda esta comunicação com a base de dados, pois seguramente que não usavam o execute. Resolvi então começar a estudar alguma documentação, e comecei... pelo execute.

E foi aí que entrei no maravilhoso mundo da comunicação com a base de dados auxiliada por Rails. Como alguns de vocês  conhecem, as migrações são uma sub-componente de uma sub-componente do Rails, o nosso conhecido ActiveRecord. Graças ao esforço de dissociação empregue na versão 3 do AR, tudo o que envolve a comunicação com a base de dados foi extraído para um módulo à parte chamado ConnectionAdapters (http://apidock.com/rails/ActiveRecord/ConnectionAdapters). No domínio deste âmbito encontram-se definidos adaptadores para vários tipos possíveis de base de dados que herdam de um adaptador abstracto (AbstractAdapter). Nesse adaptador abstracto é incluído um módulo chamado DatabaseStatements, que era exactamente o que estava à procura. Neste módulo estão definidos todos os métodos com os quais se podem enviar declarações para a base de dados, cada um deles reflectindo o comportamento-base que a base de dados tem para com cada uma destas declarações. Logo, existe um método chamado insert, que retorna o id da última entrada inserida; o método update, que retorna o número de linhas afectadas na tabela utilizada, entre outras funções úteis... ah, o execute também tá definido aqui! (http://apidock.com/rails/v3.2.1/ActiveRecord/ConnectionAdapters/DatabaseStatements)
Tornou-se claro para mim que este módulo tinha que ser incluído nas migrações, dado que o execute andava por ali à mão de semear. Dei uma olhada no código-fonte da classe ActiveRecord::Migration, e dei conta que as migrações são corridas no contexto da classe ActiveRecord::Base. Tive a minha confirmação. Daí à reescrita das migrações foi um passo:

  


# pesquisar atributos
attrs = select_all("SELECT * FROM assets i WHERE i.owner_id = 1 AND i.image_owner_type = 'User::Lawyer'")
# actualizar para a nova tabela
attrs['origin'] = "app1"

attributes, values = attrs.to_a.transpose
attributes.map!{|a|quote_column_name(a)}.join(",")
values.map!{|a|quote(a)}.join(",")
  image_id = insert "INSERT INTO images (#{attributes}) VALUES (#{values});"
# usar o image_id em outras operações...


Poupei duas linhas inconsequentes e encurtei outras tantas. Mas continuo ali com aquela declaração SQL explícita que me dá comichão na garganta... mas, enquanto o pessoal do Rails não encontrar um modo de "ARelizar" essas chamadas aos métodos do DatabaseStatements, penso que isto é o melhor que se consegue (corrijam-me se estiver enganado). Ah, ali aquelas chamadas a quote_column_name e quote são naturalmente funções auxiliares que colocam as aspas de segurança respectivas ao redor dos atributos e valores. Outra das boas lições desta viagem pelo código-fonte do Rails.

Como a minha tarefa seguinte seria criar um módulo que migrasse dados de uma aplicação para a outra, e como esta migração teria que se passar directamente entre as duas bases de dados (o objectivo final é ter 2 aplicações), decidi implementar o conhecimento adquirido. Criei então um Mixin que tratava da sincronização (criação, actualização e deleção) de uma entidade na base de dados 1 com a base de dados 2. Para isso, criei uma classezinha que encapsulava todas as rotinas de uma forma bonitinha e que podia ser chamada aplicando uma variante similar ao ARel, um DSLzinho limitado a esse âmbito. O resultado foi a classe seguinte:

 

      class BaseStatements
        attr_reader :connection

        def initialize(connection)
          @connection = connection
        end

        def select_all(*fields)
          conditions = fields.extract_options!
          table_name = fields.pop
          @connection.select_all("SELECT #{fields.empty? ? '*' : quote_keys(fields).join(",")} FROM #{table_name}
                                 #{' WHERE ' unless conditions.blank?}#{parse_conditions(conditions)}")
        end

        def select_value(field, table_name, conditions)
          @connection.select_value("SELECT #{field} FROM #{table_name} WHERE #{parse_conditions(conditions)}")
        end

        def insert(table_name, attributes)
          keys, values = attributes.to_a.transpose
          @connection.insert("INSERT INTO #{table_name} (#{quote_keys(keys).join(",")}) VALUES (#{quote(values).join(",")})")
        end

        def update(table_name, attributes, conditions)
          @connection.update("UPDATE #{table_name} SET #{quote_keys(attributes)}=#{quote(v)}"}.join(",")} 
                                                   WHERE #{parse_conditions(conditions)}")
        end

        def delete(table_name, conditions)
          @connection.delete("DELETE FROM #{table_name} WHERE #{parse_conditions(conditions)}")
        end

        private
        def quote_keys(keys)
          keys.map{|a|@connection.quote_column_name(a)}  
        end
        
        def quote(values)
          values.map{|a|@connection.quote(a)}
        end 
 
        def parse_conditions(cond)
          return @connection.quote(cond) unless cond.kind_of?(Hash)
          cond.map do |k, v|
            case k
                when :not then "NOT #{parse_conditions(v)}"
                when :in then "IN (#{v.map{|el|@connection.quote(el)}.join(",")})"
                else "#{@connection.quote_column_name(k)}#{v.kind_of?(Hash) ? ' ' : v.nil? ? ' IS ' : ' = '}#{parse_conditions(v)}"
            end
          end.join(" AND ")
        end
      end

Instanciando um objecto desta classe, podia então realizar declarações SQL com algum nível de complexidade numa linguagem mais descritiva e legível. Alguns exemplos:

handler = BaseStatements.new(ActiveRecord::Base.connection)
 
handler.select_all(:id, :name, :users, :email => "donald@ducks.com", :city => "Buxumburra")
# "SELECT id, name FROM users WHERE email = 'donald@ducks.com' AND city = 'Buxumburra';"  
handler.insert({:email => "donald@ducks.com", :city => "Buxumburra"}, :users)
# "INSERT INTO users (email,city) VALUES('donald@ducks.com','Buxumburra');" 
handler.update({:city => "Cidade do Cabo"}, :users, {:email => "donald@ducks.com})
# "UPDATE users SET city='Cidade do Cabo' WHERE email='donald@ducks.com';" 
handler.update({:email => "donalda@ducks.com", :users, :city => {:not => {:in => ["Buxumburra","Kinshasa"]}} 
# "UPDATE users SET email='donalda@ducks.com' WHERE city NOT IN ('Buxumburra','Kinshasa');" 
handler.delete(:users, :id => 1) 
# "DELETE FROM users WHERE id=1;"
 
 
Como disse, é um DSLzinho, não cobre todas as possibilidades, foi uma solução para um contexto específico. Claro que existem outras questões que se colocam em relação ao tipo de solução que eu encontrei para este tipo de problema. Não estaria mais correcto fazer tudo na base de dados através de um stored procedure? Sim. Não seria mais moderno e elegante no contexto web comunicar com a aplicação 2 através de um serviço remoto, por exemplo uma web API? Possivelmente. Em todo o caso, valeu a pena descobrir e explorar este pedacinho do mundo do Rails.

domingo, 17 de junho de 2012

Tipificação de variáveis e programação defensiva

Oi,


Surgiu há uns dias uma discussão interessante no trabalho. Um colega meu refactorizou uma componente que tinha programado fazia uns meses, e questionava-se quando ao melhor modo de se proteger contra aquilo que chamou a tipificação fraca na linguagem de programação Ruby (e com o que eu na altura concordei). Ele tomou a decisão de, segundo ele, verificar o tipo das variáveis passadas como argumento a cada função, protegendo-de dessa forma contra comportamentos inesperados, possíveis excepções que fossem lançadas, etc., e chamou ao que estava a fazer "programação defensiva". Quis discutir o resultado comigo, e aí começaram a surgir as diferenças na opinião de cada um. Eu achei o que ele estava a fazer contra-producente, visto a própria linguagem de programação (que dizem ser uma "linguagem orientada a objectos pura") já providenciar algum tipo de identificação desse tipo de excepções, ás quais ele podia aplicar algum tipo de tratamento, mas pior que isso, aplicar esse tipo de abordagem em cada método iria só "poluir" visualmente estes, desviando a atenção da lógica da rotina, e desviando-se também das práticas aconselhadas na programação em Ruby. Outro possível efeito secundário seria a ocorrência de algum tipo de "falha silenciosa".



def plus(a, b)
return nil unless a.kind_of?(Integer)
return nil unless b.kind_of?(Integer)
a + b
end



A discussão começou a aquecer, pelo que achei que era melhor rever os conceitos antes de continuar a "bater na cabeça do ceguinho".

Comecemos então pelo conceito acima referenciado de "tipificação fraca na linguagem de programação Ruby". Algo que, para que fique escrito em algum lado, eu repeti (erradamente) várias vezes em diversas ocasiões. Conceptualmente, o que diferencia a tipificação dita "fraca" da dita "forte" é a imposição (ou falta) de restrições na interoperabilidade entre valores de tipos diferentes. De acordo com esta perspectiva, Ruby é uma linguagem com tipificação forte (com algumas excepções). Então porquê a confusão estabelecida no início? Pois, é a falta de declaração do tipo de uma variável. Mas esse já é um conceito diferente. Trata-se agora de um outro conceito: o dinamismo da linguagem. Uma linguagem é considerada de tipificação estática quando o tipo  do valor referenciado pelas variáveis é verificado em tempo de compilação, e dinâmica quando este é verificado em tempo real. Em Ruby, uma variável não é mais do que um "ponteiro", cujo valor pode ser qualquer coisa. Daí ser possível fazer coisas em Ruby como:


a = 2
a = "a"


No entanto, algo como isto:


a = 1
a = a + "2"


lançaria uma excepção. (Curiosamente, isto funcionaria em Javascript, o que só aumenta mais o meu desprezo pela especificação de uma linguagem tão útil como pessimamente estruturada).

Logo, concluindo, Ruby é uma linguagem "dinâmica e fortemente tipificada".

Passemos agora para a programação defensiva. O objectivo-base deste conceito é assegurar a continuação do funcionamento de uma componente de software protegendo-se contra mudanças eventuais futuras no código-fonte. Ou seja, eliminar a lei de Murphy equação. Segundo o artigo da Wikipedia sobre o assunto, propõe-se portanto a aumentar a qualidade do código-fonte e reduzir o número de erros, tornar o código-fonte compreensível, a fazer o software comportar-se de uma maneira previsível. Acho isso tudo óptimo. Aliás, acho cada um desses pontos um tema em si, e já um pouco para lá daquilo que a programação defensiva supostamente previne. Mas assim de repente, uns quantos pontos pessoais:
  • eliminar a lei de Murphy; bonito, mas... isso é só para Deus e para o Chuck Norris. Existe uma razão para se falar tanto da lei de Murphy - ela é omnipresente;
  • erros evitam-se de diversas formas, por exemplo testando o código, deste modo providenciando uma especificação de uso para quem vier pegar nesse código seguidamente;
  • melhorar a legibilidade do código é óptimo, e eu sou um apologista disso, mas existem outras formas de complementar a compreensão do código escrito, tal como documentá-lo (ainda sou mais apologista deste ponto);
  • fazer o software comportar-se de maneira previsível é um objectivo ambicioso. O que é definido como "previsível"? Que fazer nos casos excepcionais? Será que estes serão automaticamente compreendidos pelas rotinas que têm que interagir com esses valores "previsíveis"? Nestas alturas, é sempre bom vistoriar os requisitos de novo. 

Então, voltando ao ponto inicial, é a situação do exemplo descrito no início do texto programação defensiva?

  • Se as rotinas que utilizarem aqueles métodos nunca testarem os casos excepcionais, sejam eles valores nulos, excepções customizadas, etc., a lei de Murphy vai aparecer... para além do esforço acrescido de testar os casos excepcionais;
  • Ele evitaria mais erros testando e criando uma especificação de uso (como eventualmente fez) para outros programadores, minimizando o impacto do erro humano;
  • A legibilidade não foi melhorada, na minha opinião. Aquelas duas linhas que testam se o tipo do valor é um número inteiro toldam a minha vista um pouco (sim, estou a ser sarcasticamente exagerado, mas acho que compreendem onde quero chegar);
  • A rotina comporta-se de forma previsível? Pois, não sei. Se o requisito da rotina é identificar os casos em que os valores não são inteiros, e depois tratá-los de forma adequada, tudo bem; caso contrário, o erro persiste. E se eu tiver que tratar sequências de caracteres de forma diferente de números reais? Será que posso usar a rotina com métodos reais? Será que produz um resultado esperado?

Resumindo e concluindo, acho que foi tudo uma boa ideia, mas aquele tratamento de erros não faz bem nem mal, simplesmente não acrescenta nada. Chamei-lhe "programação paranóica".

Manifesto de intenções

Oi

Pareceu-me bem iniciar com um "Oi" o blog, é curto, seco e directo ao assunto. A sua função é saudar de uma forma cordial e semi-informal quem quer que seja que esteja a ler o que foi, é e vai ser escrito por aqui. Deixa ver o que acontece se eu repetir.

Oi

E pronto, está feito. Passemos à fase seguinte, passando a responder ao "quem somos", "para onde vamos", "de onde vimos".

"Ngimbo" é uma palavra do português (se não é, passou a ser) que assim por alto significa "foice". Eu gosto mais de traduzir como "ferramenta de trabalho". Porquê "Ngimbo"? Porque dá um contexto ao blog. Já lá chegamos.

Eu sou alguém. Que vive em parte incerta. Porquê a anonimidade? Bom, por enquanto, porque sim. Estudei Engenharia Informática no Instituto Superior Técnico em Lisboa e presentemente trabalho no ramo, mais especificamente planificando e desenvolvendo aplicações-web. Porque revelei esta informação pessoal? Porque, sendo o objectivo deste blog divagar um pouco mais pelos assuntos relacionados com as tecnologias de informação em geral, achei que seria melhor estabelecer uma espécie de perfil profissional que me qualifique de certo modo para falar sobre o assunto em questão.

Porquê um blog? E porquê agora? Bem, por várias razões.
Já me questionaram em várias entrevistas de emprego se eu administrava ou participava em algum tipo de blog cujo tema fosse relacionado com as tecnologias de informação. Eu geralmente respondia que sim, já administrei e participei em blogs (com uma frequência díspar, como podem comprovar "cuscando" o meu perfil), mas em nenhum deles alguma vez se tinha escrito alguma coisa sobre tecnologias de informação. Ao que se seguiam geralmente um olhar que era um misto de desapontamento e preconceito (profissional). Tornou-se claro para mim que, para se ser alguém no ramo, é preciso não só saber, mas também mostrar que se sabe (de preferência por esta ordem, mas não obrigatoriamente).
Por outro lado, já por várias vezes senti a necessidade de extravasar alguma tensão e desapontamento com a forma como alguns processos decorrem nos projectos em que eu trabalho e divagar/discutir sobre eles. Sendo uma pessoa de opiniões fortes, gosto de defender algumas ideias que acho importantes para o bom funcionamento de um projecto nesta área, sempre tentando ser flexível com a minha opinião e aceitando sugestões ou mesmo críticas quando estou errado. Errare humanum est, e eu gosto sempre de espaço para errar, de preferência não muitas vezes.
Porquê um blog em português sobre tecnologias de informação? Bem, em primeiro lugar, é a minha língua nativa. Pessoalmente acho que escrevo e me expresso melhor em português que em qualquer outra língua. Por outro lado, estou farto de inglês. Todos os dias sou bombardeado com inglês, escrito ou falado, por mim e por outros. E a maior parte das vezes por gente cuja língua nativa está longe de ser o inglês. Ou seja, na maior parte das vezes, sou bombardeado com inglês errado. Sei de muita gente, lusófona e não só, que tomou a decisão de escrever os seus textos em inglês, porque achou que desse modo, chegava a uma audiência mais abrangente. Acho muito bem. Acho também que desse modo, enchem a Rede com mais conteúdos em inglês errado, perpetuam o estatuto do inglês como a língua de facto franca da actualidade, dessa forma minificando tanto a importância da sua língua nativa como a riqueza que a língua inglesa possa ter, e contribuem indirectamente para a suave e aborrecida mutação da raça humana para o mínimo denominador comum. O que é engraçado, porque vai totalmente no sentido contrário da tecnologia. O primeiro, e durante algum tempo, único esquema de codificação de caracteres usado na representação de texto em computadores chama-se ASCII, acrónimo para American Standard Code for Information Interchange, ou em português"Código Padrão Americano para o Intercâmbio de Informação". Hoje em dia existem mil-e-um esquemas de codificação que representam todos os alfabetos existentes à face da Terra de várias formas. Até há bem pouco tempo, o esquema para URIs (Identificador Uniforme de Recursos) só aceitava caracteres do alfabeto inglês. Hoje em dia já começam a ser aceites outro tipo de caracteres. Ou seja, a tecnologia tenta expandir o seu âmbito de forma a chegar a mais gente. Se continuar a existir este esforço de standardização da massa humana, será que não é uma perda de tempo esta constante adaptação da tecnologia?
Portanto, em português. Não é um sentimento de posse ou nacionalismo que me move, mas mais uma questão de juntar o útil ao agradável. Quem souber ler, sabe. Quem não souber, ou aprende ou usa um desses tradutores online que existem. Jesus falava em aramaico. Hoje em dia ninguém fala aramaico e toda a gente conhece Jesus e sabe do que ele falou. Não queria comparar-me a Jesus, mas se os jogadores de futebol o fazem a toda a hora...

Para finalizar, queria dizer que este blog não vai ser escrito de acordo com o novo Acordo Ortográfico. Os meus manos brasileiros que me desculpem, não o faço por apelo nacionalista ou o que quer que seja, simplesmente porque não aprendi essa versão do português, e isso só me iria dificultar a comunicação um pouco mais. Falem quando não entenderem, e a "galera" traduz, valeu?

Saudações