JPA 2.1 – AttributeConverters na prática com Hibernate

abril 10th, 2015

Depois de um longo e tenebroso inverno, voltamos às atividades.

Este post é focado em quem sabe o que é um AttributeConverter e quer usar na prática, escrito em um tom irônico :-)

Eis que finalmente a spec do JPA permite criar, de forma portável, conversores para suportar tipos arbritários. E como trabalhamos com Java 8, queremos usar java.time.LocalDate, java.time.YearMonth e afins. Além disso, queremos enums que tenham persistência com mnemômicos (ex: “M” para “MASCULINO” e não “MASCULINO” na coluna do banco). Na teoria, basta escrever os converters. Já na prática…

Você vai e escreve sua primeira implementação. Aí você vai fazer os testes e pensa… “Mas e null, como é tratado?” Você lê o Javadoc e… não fala! Ok, vamos ver o que o Hibernate faz na prática com este caso: pra o 4.3.8, versão atual de agora, o conversor não é chamado para valores null. Mas será que continuará assim? Procurando no Google, você descobre que a próxima versão, segundo a HHH-8697, irá chamar o conversor para valores null. Ok, então vamos criar uma superclasse utilitária chamada NullSafeAttributeConverter que para null, retorna null e chama um método protected para quando o valor não é nulo.

Aí, você muda seus conversores e tudo para de funcionar. Algo como:

Caused by: org.hibernate.AssertionFailure: Could not extract ParameterizedType representation of AttributeConverter
definition from AttributeConverter implementation class

E aí você descobre via HHH-8854 que o Hibernate não sabe resolver os tipos do seu conversor quando há superclasses genéricas. Ou seja, tem que fazer extends NullSafeAttributeConverter<LocalDate, Date> implements AttributeConverter<LocalDate, Date>.

Ufa, vamos tentar fazer os conversores de enum. Pode-se usar uma interface com default methods com uma API ou anotações e ter um conversor genérico. Só que… e como saber a subclasse específica da Enum em questão para fazer a conversão (ex: Sexo ou Estado)? Não tem como! Ah, mas via API do Hibernate tem como… Mas não dá pra registrar automaticamente para todas as enums; teria que fazer um registro por tipo de qualquer forma. Qual a solução? Acabamos usando processadores de anotações para gerar o código dos AttributeConverters com @Converter(autoApply = true).

Dá trabalho? Dá. Porém, quando funciona é lindo e produtivo!

E você, teria interesse e/ou gostaria de colaborar com um projeto open-source com estas implementações?

Como fazer cronogramas – 7 anos depois

agosto 10th, 2013

Hoje precisei explicar para o Denis Tiago, que vai cobrir minhas férias na TecSinapse, como fazer cronogramas e reli o post do blog. É muito engraçado reler e ver que hoje não faço as coisas de modo lá muito diferente. Ou eu já sabia o que fazia há 7 anos ou não aprendi muito :-)

Só pensei em uma coisa que faltava no post original: lembre-se das férias, folgas, feriados, eventos e afins ao fazer o seu cronograma. O impacto, especialmente das férias das pessoas, é enorme. E se elas não tiverem marcado as férias? Distribua todos estes dias nas atividades através de uma proporção entre os dias calculados pelos passos anteriores e a quantidade dessas datas recursivamente (porque conforme o prazo se estender, haverão outras folgas, férias etc) e lembre-se ao controlar a execução de que você não está “adiantado”, é só porque as pessoas não tiraram os dias ainda.

Quem sabe um dia faço um software que implemente decentemente esta metodologia, combinando a venda tradicional de software com execução ágil?

Precisa-se de APIs de data e hora que funcionem – ou uma resposta a “O eterno problema de calcular a diferença de dias entre duas datas em Java”

outubro 29th, 2012

Recentemente, o pessoal da Caelum, empresa dos meus amigos Paulo & Guilherme Silveira e mais um monte de gente que admiro, fez um post sobre O eterno problema de calcular a diferença de dias entre duas datas em Java. Leiam, se ainda não o fizeram, senão o resto não fará sentido. O problema é que essas API são bastante deficientes e diria até perigosas.

Eis que o ObjectLabKit eu conheci meio recentemente e já tinha dado uma olhada. Não é uma API feita em cima de imutabilidade, com calendários baseados em nomes apenas e em que os feriados só podem ser setados uma vez – ou seja, num ambiente de classpath compartilhado, i.e. container, a chance de dar zebra é enorme.

O Jollyday é ainda mais perigoso. Além de ser cheio de synchronized pra tudo que é lado, utiliza o perigoso toDateMidnight() no código – que lança exceção no dia em que começa o horário de verão. Além disso, no isHoliday(LocalDate) ele usa o equals() de LocalDate – que compara usando a cronologia, emitindo false para holidayManager.isHoliday(new LocalDate(2012, 1, 1)) – e em outros o Interval.contains(ReadablePartial), que basicamente trabalha com o valor em milissegundos, gerando resultados inconsistentes.

O conceito do LocalDate está quebrado até no joda-time e o Stephen Colebourne, criador da API e que lidera a JSR-310 comigo, já admitiu que a ideia de colocar a cronologia dentro do objeto e todo mundo assumir que não existe cronologia foi um erro de design. É por isso que o LocalDate da JSR não sofre do mesmo mal e é exclusivo para o formato ISO.

Trabalhar com data e hora é tão complicado que o código da JSR já foi refatorado diversas vezes até chegar no estado atual – que espero estar suficientemente bom agora e não padecer do mesmo tipo de problema. Pra isso, precisamos do seu feedback no projeto. Ao testar, leva em conta que poderão haver mais refactorings ainda antes de sua integração no JDK, justamente para evitar problemas do tipo.

Estou trabalhando numa API de cálculo de dias úteis e feriados para uso nos projetos da TecSinapse, empresa na qual estou trabalhando agora, que pretendemos tornar pública se tudo der certo, com implementações em cima do Joda-Time e da JSR-310. Se você tiver interesse em ser um early reviewer e impedir que erros como esse aconteçam na nossa solução também, entre em contato e eu avisarei quando estivermos prontos :-)

TypedQuery e select new – dois excelentes recursos pouco utilizados

agosto 21st, 2012

Este ano acabei participando de projetos com duas excelentes equipes e tive muitas oportunidades de discutir arquitetura com elas, além de revisar código. Uma coisa que me surpreendeu foi como dois recursos do JPA, que fazem muita diferença, ainda estão pouco difundidos.

O primeiro é o uso de TypedQuery , introduzida a partir do JPA 2. O uso, para quem está acostumado com Query, é bem simples:

TypedQuery<Entidade> query = em.createNamedQuery("Entidade.minhaQuery", Entidade.class);
List<Entidade> resultados = query.getResultList();

Conforme o exemplo acima, os métodos que criam uma instância de Query no EntityManager também possuem outra versão sobrecarregada que recebe o tipo a ser retornado e criam uma TypedQuery. Essa interface estende Query fazendo com que todos os retornos que antes eram Object passem a ser fortemente tipados, evitando casts e o uso do famigerado @SuppressWarnings("unchecked").

Além disso, os métodos de EntityManager que instanciam TypedQuery verificam, no momento da criação, se o retorno realmente pode ser atribuído para o tipo desejado, lançando uma exceção se não for o caso. Assim, temos erros fail-fast, ou seja, ao invés de descobrirmos no acesso a List retornada que houve um erro de tipagem, temos uma exceção lançada exatamente na linha em que se originou o problema, facilitando sua correção.

Uma limitação de TypedQuery é que ela só pode ser criada para queries que retornem exatamente um elemento no select. Os casos em que se faz projection, com Object[], não poderiam se beneficiar do recurso. Não poderiam porque há outro recurso não tão conhecido em JPA, mas poderoso, que é o SELECT NEW:

SELECT NEW br.com.michaelnascimento.exemplo.ClasseArbitraria(e.umaPropriedade, e.outraPropriedade) FROM Entidade e

A query JPA acima retorna uma instância de ClasseArbitraria, chamando o construtor que receberá as propriedades como parâmetros uma vez por “linha” retornada. Precisa-se usar o FQN (fully-qualified name, ou em termos práticos, pacote + nome) da classe pois ela não precisa estar alistada na persistence unit.

Uma das grandes vantagens do uso de SELECT NEW não é só permitir o uso de TypedQuery com projections, mas também evitar ainda mais casts nessa situação, além de permitir o uso de boas práticas de OO nesses cenários. Eu normalmente uso inner classes das entidades ou classes do mesmo pacote nestes cenários, às vezes até mesmo implementando uma interface comum com as minhas entidades. Assim, posso manter o encapsulamento, tanto por invocar métodos não-públicos da outer class quanto por permitir que outras classes de negócio trabalhem de forma transparente tanto com a entidade como com a minha abstração da projection – a que costumo me referir como view nos projetos.

Espero ajudar a difundir o uso dessas técnicas úteis no Brasil. E você, já as conhecia?

PS: Eu achei que ia fazer pelo menos um post a cada quinze dias, tsc, tsc :-/

Imports organizados a la Eclipse no NetBeans

julho 31st, 2012

Que eu prefiro o NetBeans ao Eclipse já deve ser um fato conhecido. Muitas pessoas tem preconceito com o NetBeans, mas recentemente passei um tempo trabalhando ao lado de um time excelente que usava Eclipse e tentei eu mesmo dar uma nova chance a IDE que afrontava a Sun (ou vocês acham que esse nome foi dado por qual razão), justamente para que não fosse eu o preconceituoso.

A verdade é que mesmo com esses usuários experientes e acima da média, diversas funcionalidades que eu nem imaginava que o Eclipse não tinha realmente não estavam lá (conforme for lembrando, vou mencionando, mas a mais frustante foi não conseguir criar um package-info.java via wizard).

No meu retorno para o NetBeans, esbarrei num problema que não tornava a experiência de uso das duas IDEs no projeto transparente: a organização dos imports. O NetBeans organiza imports por padrão assim:


import br.com.michaelnascimento.projeto.ClasseA;
import br.com.michaelnascimento.projeto.ClasseB;
import java.util.Collection;

enquanto o Eclipse faz:


import java.util.Collection;

import br.com.michaelnascimento.projeto.ClasseA;
import br.com.michaelnascimento.projeto.ClasseB;

Perguntei ao meu amigo Michel Graciano se não havia algum plugin para fazer isso no NetBeans e isso está presente desde a versão 7.1 e eu não tinha percebido.

Bast entrar na tela a seguir:

Formatting -> Import options

Formatting -> Import options

e mandar agrupar os imports, seguindo a mesma lógica do Eclipse.

A mesma funcionalidade está disponível também por projeto, permitindo seguir de forma fácil essa configuração somente nos projetos compartilhados entre as IDEs.

Por que deprecated nao e depreciado

outubro 1st, 2010

A questão da tradução do material de TI sempre foi polêmica por si só. Alguns acham que desenvolver tem mais é que saber inglês ou que escrevendo artigos em português você limita o público alvo do seu trabalho. Eu até acho que o desenvolvedor pra ser bom vai ter que aprender inglês, mas não precisa começar sabendo. Acho que deve haver pelo menos material introdutório sobre todo tipo de assunto em português.

Uma tarefa que exige muito cuidado é a tradução de termos essenciais ao assunto sendo estudado. Em Java, um termo mal traduzido é deprecated, normalmente traduzido depreciado. Esta palavra realmente pode ter esse sentido, mas não da forma que ela é usada em Java.

Além de significar depreciado, a palavra também tem o sentido que não é aprovado, que é o sentido correto. De forma que uma boa tradução para “a deprecated method” seria “um método não-aprovado” ou talvez “desaprovado” (a segunda forma me soa um pouco estranha). Alguns utilizam “obsoleto” ou “desencorajado”, o que também é válido no contexto de uso (e muuuito melhor que depreciado).

Eu ficaria muito feliz se a Mundo J e a Java Magazine abolissem terminantemente o uso de depreciado como tradução de deprecated.

Para mais informações sobre esse segundo sentido da palavra, vejam o dicionário de Cambridge.

Customizando a pesquisa dos itens no JComboBox

setembro 10th, 2010

Assim como o último post, este também é baseado em uma pergunta feita na lista do genesis há alguns meses atrás.

Um usuário percebeu que num JComboBox com um ComboModel de Strings, ao digitar os primeiros caracteres do item, ele é selecionado automaticamente, ao passo que isso não acontecia com o combo populado pelo genesis. Logo que comecei a investigar o issue, percebi que o combo usa o valor retornado pelo toString() dos objetos contidos no model para implementar essa funcionalidade, o que claramente não é adequado para a maioria das implementações de model que não usam Strings.

Deparei-me com a interface JComboBox.KeySelectionManager, que permitiu resolver meu problema, usando o valor de display do bean para que a pesquisa seja executada.

Uma ideia interessante que tive foi de buscar os itens que contém o termo, como ocorre em alguns componentes do NetBeans. Isso é bastante útil quando muitos itens possuem o mesmo começo, como no exemplo abaixo. Fica dada a dica ;-)

package br.com.michaelnascimento.test;

import java.awt.EventQueue;
import java.awt.FlowLayout;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JComboBox.KeySelectionManager;
import javax.swing.JFrame;
import javax.swing.WindowConstants;

public class KeySelectionManagerDemo extends JFrame {
   public KeySelectionManagerDemo() {
      initComponents();
      jComboBox1.setKeySelectionManager(new KeySelectionManager() {
         private final long delay = 500;
         private long lastTime = -1;
         private String searchTerm;

         @Override
         public int selectionForKey(char key, ComboBoxModel model) {
            final long currentTime = System.currentTimeMillis();
            final int size = model.getSize();
            final String[] formatted = new String[size];
            final Object selectedItem = model.getSelectedItem();
            int selectedIndex = -1;

            for (int i = 0; i < size; i++) {
               Object element = model.getElementAt(i);

               if (selectedItem == element && selectedIndex == -1) {
                  selectedIndex = i;
               }

               formatted[i] = element.toString();
            }

            key = Character.toLowerCase(key);
            int start = selectedIndex;

            if (lastTime == -1 || currentTime - lastTime > delay) {
               searchTerm = String.valueOf(key);
            } else {
               searchTerm += key;
            }

            lastTime = currentTime;

            for (int i = Math.max(start, 0); i < size; i++) {
               if (matches(formatted[i])) {
                  return i;
               }
            }

            for (int i = 0; i <= start; i++) {
               if (matches(formatted[i])) {
                  return i;
               }
            }

            return -1;
         }

         private boolean matches(final String s) {
            return s.toLowerCase().contains(searchTerm);
         }
      });
   }

   // <editor-fold defaultstate="collapsed" desc="Generated Code">
   private void initComponents() {

      jComboBox1 = new JComboBox();

      setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
      getContentPane().setLayout(new FlowLayout());

      jComboBox1.setModel(new DefaultComboBoxModel(new String[] {
            "São Bento do Sapucaí", "São Bernardo do Campo",
            "São Caetano do Sul", "São Carlos", "São Francisco",
            "São João da Boa Vista", "São João das Duas Pontes",
            "São João de Iracema", "São João do Pau Dalho",
            "São Joaquim da Barra", "São José da Bela Vista",
            "São José do Barreiro", "São José do Rio Pardo",
            "São José do Rio Preto", "São José dos Campos",
            "São Lourenço da Serra", "São Luiz do Paraitinga",
            "São Manuel", "São Miguel Arcanjo", "São Paulo", "São Pedro",
            "São Pedro do Turvo", "São Roque", "São Sebastião",
            "São Sebastião da Grama", "São Simão", "São Vicente" }));
      jComboBox1.setName("jComboBox1"); // NOI18N
      getContentPane().add(jComboBox1);

      pack();
   }// </editor-fold>

   public static void main(String args[]) {
      EventQueue.invokeLater(new Runnable() {
         public void run() {
            new KeySelectionManagerDemo().setVisible(true);
         }
      });
   }
   // Variables declaration - do not modify
   private JComboBox jComboBox1;
   // End of variables declaration
}

Determinando a ordem dos componentes em Swing

setembro 8th, 2010

Há alguns meses atrás foi feita uma pergunta na lista do genesis cuja resposta envolvia saber como determinar a ordem dos componentes numa interface Swing.

Quando falamos de ordem do ponto de vista do usuário, não necessariamente falamos da ordem em profundidade, que refletiria a “árvore” dos componentes em determinado container, mas sim a ordem de foco. Existe uma interface que foi adicionada no Java 1.4 chamada FocusTraversalPolicy, que serve justamente para isso. Abaixo um exemplo de como determinar a ordem usando os nomes de componentes dados por quem fez a pergunta:

package br.com.michaelnascimento.test;

import java.awt.Component;
import java.awt.EventQueue;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import org.jdesktop.layout.GroupLayout;
import org.jdesktop.layout.LayoutStyle;

public class ComponentOrder extends JFrame {
   public ComponentOrder() {
      initComponents();
   }

   private void findOrder() {
      List<Component> components = Arrays.asList(new Component[] {login, senha,
            endereco, empresa});
      Collections.shuffle(components);

      for (Component component : components) {
         System.out.println(component.getName());
      }

      Map<Component, Integer> componentPerPosition = new HashMap<Component,
            Integer>();
      Component c = getFocusTraversalPolicy().getFirstComponent(this);
      int position = 0;

      while (componentPerPosition.size() < components.size()) {
         if (c == null) {
            break;
         }

         if (components.contains(c)) {
            componentPerPosition.put(c, position);
         }

         position++;
         c = getFocusTraversalPolicy().getComponentAfter(this, c);
      }

      for (Component component : components) {
         System.out.println(component.getName() + ": " + componentPerPosition.
               get(component));
      }
   }

   // <editor-fold defaultstate="collapsed" desc="Generated Code">
   private void initComponents() {

      jPanel1 = new JPanel();
      login = new JTextField();
      senha = new JTextField();
      endereco = new JTextField();
      empresa = new JTextField();

      setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

      jPanel1.setName("jPanel1"); // NOI18N

      login.setText("jTextField1");
      login.setName("login"); // NOI18N

      senha.setText("jTextField1");
      senha.setName("senha"); // NOI18N

      GroupLayout jPanel1Layout = new GroupLayout(jPanel1);
      jPanel1.setLayout(jPanel1Layout);
      jPanel1Layout.setHorizontalGroup(
         jPanel1Layout.createParallelGroup(GroupLayout.LEADING)
         .add(jPanel1Layout.createSequentialGroup()
            .addContainerGap()
            .add(jPanel1Layout.createParallelGroup(GroupLayout.LEADING)
               .add(login, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
               .add(senha, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
            .addContainerGap(31, Short.MAX_VALUE))
      );
      jPanel1Layout.setVerticalGroup(
         jPanel1Layout.createParallelGroup(GroupLayout.LEADING)
         .add(jPanel1Layout.createSequentialGroup()
            .addContainerGap()
            .add(login, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(LayoutStyle.RELATED)
            .add(senha, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addContainerGap(43, Short.MAX_VALUE))
      );

      endereco.setText("jTextField1");
      endereco.setName("endereco"); // NOI18N

      empresa.setText("jTextField1");
      empresa.setName("empresa"); // NOI18N

      GroupLayout layout = new GroupLayout(getContentPane());
      getContentPane().setLayout(layout);
      layout.setHorizontalGroup(
         layout.createParallelGroup(GroupLayout.LEADING)
         .add(layout.createSequentialGroup()
            .addContainerGap()
            .add(layout.createParallelGroup(GroupLayout.LEADING)
               .add(jPanel1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
               .add(endereco, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
               .add(empresa, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
            .addContainerGap(290, Short.MAX_VALUE))
      );
      layout.setVerticalGroup(
         layout.createParallelGroup(GroupLayout.LEADING)
         .add(layout.createSequentialGroup()
            .addContainerGap()
            .add(jPanel1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(LayoutStyle.RELATED)
            .add(endereco, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(LayoutStyle.RELATED)
            .add(empresa, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
            .addContainerGap(137, Short.MAX_VALUE))
      );

      pack();
   }// </editor-fold>

   public static void main(String args[]) {
      EventQueue.invokeLater(new Runnable() {
         @Override
         public void run() {
            final ComponentOrder co = new ComponentOrder();
            co.setVisible(true);
            co.findOrder();
         }
      });
   }
   // Variables declaration - do not modify
   private JTextField empresa;
   private JTextField endereco;
   private JPanel jPanel1;
   private JTextField login;
   private JTextField senha;
   // End of variables declaration
}

Quando x + 1 == x (ou mais uma razao pra nao usar float/double)

maio 29th, 2008

O melhor livro técnico que comprei nos últimos tempos foi o Java Puzzlers, que inspira esse post. E digo isso nem tanto pelos corner cases obscuros do Java que você fica conhecendo ao ler o livro (que são divertidíssimos e assustadores ao mesmo tempo) e sim pelos princípios de design que se pode extrair dele. Fortemente recomendado.

Um dos puzzlers do livro mostra um problema com float e double que eu despercebi por todos esses anos, já que não uso esses tipos pra nada. Código como:

float f1 = 16777216f;
float f2 = f1 + 1;
System.out.println(f1 == f2);

imprime true. Sim, true, você não leu errado. O erro na representação da parte fracionária, na verdade, não está limitado a ela; ele atinge a parte inteira e vai ficando mais grave à medida que o número cresce. Com números maiores, você pode somar 50, 100 ou mais e simplesmente ver o valor permanecer o mesmo, porque o tipo de dado não permite representar esses valores.

Eu particularmente achava que o MAX_VALUE e o MIN_VALUE ocorriam justamente antes do erro se manifestar na parte inteira, mas pude comprovar que não. Na verdade, o padrão IEEE 754 dita que isso seja assim, o que significa que toda linguagem que suporta tipos flutuantes segundo esse padrão vai apresentar o mesmo comportamento – que em tese não é um bug, mas uma limitação esperada do design.

Obviamente existem situações específicas em que estes tipos de dados são apropriados e/ou podem-se utilizar técnicas para minimizar ou compensar o erro de representação. Para a maioria das aplicações do mundo, o uso de BigDecimal – ou semelhantes, em outras linguages – é basicamente obrigatório.

Dada a aplicabilidade limitada desses tipos de dados, acho que talvez as linguagens que suportem float e double deveriam requerer que os tipos fossem explicitamente importados, para garantir que o desenvolvedor pelo menos tivesse que fazer esforço pra obter a arma antes de atirar no próprio pé…

Rapidas

abril 29th, 2008
  • A JSR-310 está indo bem. Estamos desenvolvendo a RI e o TCK de forma aberta através do site do projeto no java.net. Muitas coisas já estão plenamente funcionais e estáveis e um Early Draft se aproxima. Contamos com a sua participação!
  • A release 3.1 do genesis deve sair nos próximos dias. Nenhum bug novo foi encontrado após a 3.1-RC2. Em breve, atualizaremos o roadmap também.
  • A sala da minha palestra com o Stephen no JavaOne encheu e vamos repetir a palestra na sexta :-)
  • Fui indicado ao JCP Awards na categoria Participant of the Year. Se vou ganhar ou não, vamos descobrir na próxima terça
  • No sábado da próxima semana, vou participar do Scala lift off , que reunirá os grandes nomes do Scala e da comunidade. Se você ainda não conhece a linguagem, recomendo.
  • Estamos contratando na Summa. Caso tenha interesse, é só me mandar o cv. Se eu te conheço mas você acha que eu talvez não lembre exatamente de você, basta refrescar minha memória ;-)