quinta-feira, 23 de outubro de 2008

Persistência de dados com o db4o

Olá a todos, este é meu primeiro artigo aqui no blog de Arquitetura de Sistemas e Mobilidade. Como o Eduardo já introduziu em um post anterior, também trabalho na Mobiltec, na parte de arquitetura e análise de sistemas, especialmente na parte bancária. Porém, também sou adepto da descoberta e utilização de novas tecnologias, e justamente por isso, irei falar um pouco do db4o. Nos exemplos estou utilizando o LINQ (Language INtegrated Query), mas não irei comentar nada sobre ele por enquanto, deixei isto para um próximo post.
Para quem não conhece o db40, trata-se de uma ferrameta responsável por armazenar os objetos em uma base de dados. Todo o controle do armazenamento dos objetos e do acesso ao mesmo é feito pela ferramenta. Nenhum mapeamento ou conexão com um servidor de dados é necessário. O db4o trabalha com um arquivo em disco para salvar os objetos. Como um exemplo vale mais do que mil palavras: Digamos que possuímos um objeto Task e desejamos armazená-lo, tudo que devemos fazer é:
//Criando um objeto task com duas propriedades "Name" e "Enabled"
Task t = new Task { Name = "nome tarefa", Enabled = true };
//Salvando o objeto
db4oBase = Db40Factory.OpenFile("task.db4o");
db4oBase.Store(t);
db4oBase.Commit();
"Só isso mesmo?", você deve estar se perguntando. Sim, exatamente isso. Essa é justamente a idéia desta ferramenta, ser simples. Segundo os próprios donos da ferramenta: "db4o is the open source object database that enables Java and .NET developers to store and retrieve any application object with only one line of code, eliminating the need to predefine or maintain a separate, rigid data model.". Para saber mais sobre base de dados orientado a objetos, sugiro a leitura da referência http://en.wikipedia.org/wiki/Object_database

Problema ao armazenar um objeto estruturado

Durante o desenvolvimento de uma ferramenta para registrar tarefas, de uso pessoal, me deparei com o seguinte problema:
  • Existe uma classe chamada TimeSheetInfo que armazena as informações sobre o tempo de uma tarefa que possui, por sua vez, uma referência a uma classe chamada TaskInfo que possui as informações da tarefa. Existe também uma função que salva a hora que a tarefa começou, que recebe como parâmetro somente o nome da tarefa. Neste caso, o código abaixo funciona corretamente?
IObjectContainer db4Conn = 
Db4objects.Db4o.Db4oFactory.OpenFile("base.db4o");
try
{
    //Criando uma nova instância para armazenar na base de dados
    TimeSheetInfo timeSheet = new TimeSheetInfo();
    //Vinculando o tempo a tarefa 'teste' já existente
    timeSheet.Task = { Name = "teste", Enabled = true };
    //Colocando as definições da tarefa
    timeSheet.Date = DateTime.Now.Date;
    timeSheet.ClockIn = DateTime.Now;
    timeSheet.NotWorkingTime = _blnNotWorkingTime;
    db4Conn.Store(timeSheet);
    db4Conn.Commit();
}
catch (Exception ex)
{
    db4Conn.Rollback();
    throw;
}
finally
{
    db4Conn.Close();
}

Por mais que possa parecer óbvio, o código acima não funciona da maneira correta. Se consultarmos a base de dados, veremos duas entradas para a tarefa passada como parâmetro. O que aconteceu? O db4o encarou a tarefa criada como uma tarefa nova, e não uma referência a uma já existente. Mas pela lógica do método eu não quero criar uma tarefa nova, e sim vincular um horário a tarefa passada como parâmetro. Isso ocorre porque o db4o trabalha com as instâncias exatas do objeto, ou seja, para atualizar um objeto ou vinculá-lo a outro (como neste exemplo) devemos buscar o objeto e realizar as operações com aquela instância que foi recuperada. Duas modificações são possíveis, uma é receber a classe TaskInfo ao invés do nome. Porém vale lembrar que este objeto deve ser o mesmo que foi armazenado na base de dados, caso contrário continuará criando entradas extras como no exemplo do código acima. E outra alternativa, mais fácil de implementar, mas que pode trazer um pouco de ônus no tempo de execução do método (dependendo do tamanho da base), é buscar o objeto antes e criar o vínculo com o TimeSheetInfo, como no exemplo abaixo:
timeSheet.Task = (from TaskInfo t in db4Conn
                  where t.TaskName == "teste"
                  select t).Single<TaskInfo>();
Conversando com o Eduardo Klein, fui questionado porque eu fiz assim no projeto que estou fazendo. Porque eu passo uma string ao invés do objeto TaskInfo direto? Pensando um pouco, essa é a melhor aborgagem para este tipo de código. A resposta é que eu tinha uma versão do programa que trabalhava com o SQLite e eu simplesmente alterei o DAL para suportar o db4o. Mas realmente, nesse tipo de abordagem, onde o objeto é importante devemos mudar um pouco o paradigma. Ao invés de receber uma string, o correto era receber mesmo o objeto, ou seja, trabalhar com orientação a objetos.

Performance

A performance do db4o depende do tipo de query que se utiliza. Existem basicamente 4 tipos de queries possíveis:
  • Query By Example (QEB)
  • Native Queries
  • LINQ
  • SODA Query API
Cada uma tem suas particularidades e problemas. Para ser sincero, não trabalhei com todas, apenas com o LINQ, mas estudei o tutorial do próprio db4o para saber um pouco de cada uma dessas queries. A mais básica de todas (e a mais limitada) é a QEB. Como o próprio nome diz, cria-se um exemplo de objeto e realiza-se a busca. Várias são as limitações dessa abordagem:
  • Não é possível fazer expressões avançadas com AND, OR, NOT, etc.
  • Não é possível procurar por objetos com campos com valores vazios ou nulos (ou zero no caso de inteiros)
Para contornar essas limitações, o db4o recomenda o uso das Native Queries. Com esse tipo de query é possível realizar várias consultas na mesma linha de código. O LINQ dispensa maiores apresentações. Mas, a mais importantes de todas as queries, é a SODA. Pois todas as anteriores, por baixo dos panos, são transformadas em SODA. Ou seja, SODA Query API é o mais baixo nível do db4o. Com certeza, utilizar diretamente a SODA torna o código mais performático, porém menos legível e mais suscetível a erros, pois as propriedades das classes são passadas como strings para que a consulta seja realizada. Utilizando SODA parece que estamos utilizando as queries que normalmente temos no modelo clássico de DAL. Nem preciso citar que o refactoring fica comprometido nesse caso.

Compatibilidade

O db4o tem versões para Java, .NET Framework 1.1, 2.0 e 3.5 (Compact Framework inclusive). A versão que eu utilizei para testes foi a 3.5 para Compact Framework. Logo, se procurar para uma versão mais leve que o SQLCe, o db4o é uma alternativa interessante.

Conclusões

Utilizar o db4o traz um ganho de legibilidade muito grande para o código, além de diminuir consideravelmente o número de linhas de código a serem implementados. Porém existem algumas questões que ainda não analisei e que devem ser levadas em consideração no caso de utilizarmos o db4o em um projeto mais sério e complexo: performance com grande volume de dados, questões de refatoração com uma base de dados já iniciada (o que aconteceria se retirarmos um parâmetro, por exemplo?).

Links

0 Comments: