Desenvolvimento - ASP. NET

db4o - Banco de Dados Orientado a Objetos

Banco de Objetos serão a próxima onda na área de desenvolvimento e a plataforma .Net já dispõe de um excelente banco de dados orientado a objetos, o db4o.

por Cassio R. Eskelsen



Parte I - Introdução

De uns tempos para cá tenho percebido uma procura crescente por ferramentas que facilitem a integração entre o mundo orientado a objetos (a linguagem e o framework) e o mundo relacional (o banco de dados). As tais ferramentas de mapeamento objeto relacional nada mais são do que um "tradutor" entre duas línguas totalmente diferentes.

Em todas as traduções você acaba perdendo as sutilezas de uma língua ou tendo que usar muito mais palavras para expressar um conceito que é relativamente simples na língua de origem. No mapeamento O/R não é diferente: você acaba perdendo uma série de recursos da programação OO, ou tendo que escrever muito mais código para simular no banco de dados algo simples na linguagem (como por exemplo, uma propriedade do tipo array ou o gerenciamento de uma herança).

Particularmente acredito que estaremos em um mundo perfeito quando todos os banco de dados permitirem que você simplesmente pegue seu objeto do jeito que ele está e jogue-o no banco de dados(vou passar a chamar de banco de objetos) sem se preocupar com camadas e mais camadas de código para "traduzir" um objeto em query.

Já existem várias tentativas de se fazer isso, incluindo algumas muito avançadas como o Prevayler (bambooprevalence e xprevail nas encarnações .Net). No entanto, a falta de confiabilidade nos equipamentos e sistema operacional aliadas a uma certa dose de preconceito fazem com que essa solução ainda seja rotulada como "algo para o futuro".

Na outra ponta existem soluções como o LINQ da Microsoft, uma feature que estará disponível oficialmente no C# 3.0 mas que já pode ser testado agora. Não considero que isso seja verdadeiramente uma solução pois misturar conceitos relacionais (selects, wheres, etc) em uma linguagem OO como o C# é o que poderíamos chamar de "código alienígena".

Um banco realmente OO deve permitir que você faça suas consultas de forma orientada a objetos, como se estivesse pesquisando em uma Array, List ou qualquer outro container de objetos.
É aqui que se encaixa o db4o! Claro que existem outras soluções como o Caché, mas prefiro me ater ao db4o pelo seu custo acessível e por já estar mais integrado a plataforma .Net

db4objects

O db4objects(db4o) surgiu a alguns anos atrás inicialmente apenas para Java. Com a grande semelhança de código entre o .Net e o Java, foi um pulo para que fosse criada uma versão .Net. Hoje, as versões .Net e Java caminham lado a lado, tendo ambas os mesmos recursos.

Com esse banco você pode desenvolver aplicações WEB, Windows.forms e Compact Framework. Você não precisa instalar nem configurar um servidor de banco de dados. Basta enviar junto com sua aplicação uma pequena dll. Claro que você pode fazer uma aplicação cliente/servidor. O próprio db4o provê recursos para que isso seja feito, mas sempre de uma forma simples, sem a necessidade de ser um PHD em configuração de banco de dados.

O db4o tem um mecanismo de replicação muito útil para quem tem necessidade por exemplo, de manter bancos off-line parte do tempo e
on-line o restante (quem desenvolve para forças de venda deve imaginar o que estou falando!).

Quando falo em db4o, algumas perguntas são inevitáveis:

a) Não tenho problemas de performance? Não. Alguns testes mostram inclusive que o db4o é muito mais rápido que soluções que envolvam o uso de Nhibernate por exemplo. Veja benchmarks aqui: http://www.db4o.com/about/productinformation/benchmarks/

b) Ele não é caro como o Caché? Não. A licença do db4o é open-source dual como a do MySql, ou seja, se você está desenvolvendo para uso dentro de sua empresa, criando seu website ou desenvolvendo um programa GPL, ele é gratuito para você.
Mas mesmo que você precise distribuir sua aplicação, o custo da licença runtime é muito baixo.

db4o na prática

Nesse primeiro artigo sobre o db4o irei listar algumas operações básicas para que você veja a facilidade que é trabalhar com um banco origentado a objetos. Nos artigos seguintes irei mostrar um Membership provider para db4o e dicas de como deixar o trabalho com o db4o mais prático e rápido.

Instalação

Você pode puxar o db4o nesse link: http://www.db4o.com/community/ Os exemplos que farei já serão baseados na nova versão 5.0.
A instalação não fará nada mais do que descompactar os arquivos em um diretório.
Você terá a seguinte estrutura de diretórios

\dll: as dlls do db4o com duas versões: compact framework e para uso em PC
\doc: help e um excelente tutorial
\src: fontes do db4o e testes unitários que podem servir de fonte de estudo

Utilização

Para você utilizar o db4o em seus projetos basta referenciar a respectiva dll e adicionar um "using com.db4o"


Criando um database

Não existe no db4o um processo formal de criação do database nem das respectivas classes. A medida em que você for salvando seus objetos o db4o faz tudo para você.Em um código que usa o db4o como persistência tudo gira em torno de ObjectContainer. É o ObjectContainer que irá gravar, ler e alterar seus dados.
O arquivo de dados do db4o tem normalmente a extensão .yap.

A primeira operação que faremos é "conectar" no arquivo:

ObjectContainer objectBag = Db4o.OpenFile("arquivo.yap");


Isso abrirá o arquivo (se já existente) ou criará um novo. Claro que você pode colocá-lo em qualquer diretório em que sua aplicação tenha direito de acesso/gravação.

Pelo intellisense do Visual Studio você poderá ver que objectBag tem uma série de métodos e entre eles podemos citar:

Set: grava objetos na base
Get: recupera um objeto da base procurando-o através de um template
Delete: apaga um objeto
Query: faz uma pesquisa na base retornando uma lista do tipo ObjectSet
Query<T>: faz uma pesquisa retornando uma lista tipada
Commit e Rollback: confirma/anula uma transação (semelhante ao commit/rollback dos RDBMs)

Inserindo dados

Para nossos testes iremos criar duas classes muito simples:

    public class Autor
    {        
        private string _nome;
        private string _email;
        
        public string Nome
        {
            get { return _nome; }
            set { _nome = value; }
        }
        public string Email
        {
            get { return _email; }
            set { _email = value; }
        }
        public override string ToString()
        {
            return this.Nome+"("+this.Email+")";
        }
    }
    public class Livro
    {
        private Autor _autor;
        private string _titulo;
        private string _isbn;

        public string ISBN
        {
            get { return _isbn; }
            set { _isbn = value; }
        }	
        public string Titulo
        {
            get { return _titulo; }
            set { _titulo = value; }
        }
        public Autor Autor
        {
            get { return _autor; }
            set { _autor = value; }
        }
        public override string ToString()
        {
            return string.Format("Titulo: {0}\nAutor: {1}\nISBN: {2}\n\n", this.Titulo, this.Autor.Nome, this.ISBN);
        }	
    }

Por default, O db4o grava o conteudo de todos os campos. Caso você não deseja gravar algum campo, você deve marcá-lo
com o atributo [Transient]

Note que não estou as classes não descendem de nenhuma classe base, e também não estou implementando nenhuma interface específica.

Vamos criar uma pequena massa de dados e salvá-los na base:
	static void CriaDados()
        {           

            Autor raul = new Autor();
            raul.Nome = "Raul Wazlawick";
            raul.Email = "raul@acme.com";

            Livro analise = new Livro();
            analise.Autor = raul;
            analise.ISBN = "8535215646";
            analise.Titulo = "Análise e Projetos de Sistemas de Informação Orientados a Objetos";
            objectBag.Set(analise);

            Autor fabio = new Autor();
            fabio.Nome = "Fabio Camara";
            fabio.Email = "fabio@acme.com";

            Livro dominando = new Livro();
            dominando.Autor = fabio;
            dominando.Titulo = "Dominando o Visual Studio.Net com C#";
            dominando.ISBN = "8575021095";
            objectBag.Set(dominando);

            Livro cinquentaEOito = new Livro();
            cinquentaEOito.Autor = fabio;
            cinquentaEOito.Titulo = "58+ Soluções em .Net";
            cinquentaEOito.ISBN = "8575021583";
            objectBag.Set(cinquentaEOito);
        }

Em ordem o que fiz:

- Criei uma nova instância de Autor (o Raul)
- Criei uma nova instância de Livro (o de Análise)
- Disse para a instância de Livro que seu autor é o Raul;
- Gravei na base a instância de Livro. Como o autor ainda não estava gravado, o db4o grava-o automaticamente também.
- Repeti a operação com um novo autor e dois novos livros

Percebam que a gravação foi muito simples. Nada de xml de mapeamento, nada de atributos, nada de pilhas de camadas para fazer uma
simples gravação de dados.
Recuperando dados

O db4o tem 3 tipos de pesquisa possíveis:

* QBE (query by example): recupera dados através de um objeto modelo. É a forma mais simples, mas possui uma série de restrições

* SODA query: um estilo de query que até pouco tempo foi o mais indicado para ser usado pelo db4o. Um ponto negativo é a utilização de
strings para identificar os campos na pesquisa o que não permite a checagem de erros na compilação. O ponto positivo é a sua
velocidade de pesquisa

* Native Queries: lançadas recentemente no db4o, permitem uma pesquisa de forma nativa orientada a objetos e checagem de tipos durante a compilação.

Recuperando dados via QBE

Vamos criar uma pequena função de pesquisa de autor:

	static Autor ProcuraAutor(string nome)
        {
            Autor proto = new Autor();
            proto.Nome = nome;
            ObjectSet result = objectBag.Get(proto);
            if (result.HasNext())
                return (Autor)result.Next();
            else
                throw new Exception(nome + "  nao encontrado");
        }


Em ordem, o que fiz:

- Criei um objeto Autor para servir de template (proto)
- pesquisei no container atual o objeto proto através de Get. O resultado será retornado em um ObjectSet
- Verifico se existe algum objeto no ObjectSet com a função HasNext()
- Se existir um objeto, retorno o mesmo através da função Next()

É importante salientar que o ObjectSet não contem objetos instanciados automaticamente. Eles são apenas instanciados quando forem explicitamente chamados,como no exemplo acima, com a função Next(). O db4o chama isso de Ativação de Objetos e assim reduz-se a quantidade de memória utilizada.

Recuperando dados via SODA

A pesquisa via SODA permite estabelecer critérios mais detalhados e também permite ordenar o resultado. Essa pesquisa permite acessar diretamente os nós de um graph de pesquisa:

	static void LivrosDoAutor(Autor autor)
        {
            Query query = objectBag.Query();
            query.Constrain(typeof(Livro));
            query.Descend("_autor").Constrain(autor);
            ObjectSet resultado = query.Execute();
            if (resultado.Count > 0)
            {
                while (resultado.HasNext())
                {
                    Console.WriteLine(resultado.Next());
                }
            }
            else
            {
                Console.WriteLine("Não há nenhum livro do autor {0} cadastrado", autor.Nome);
            }
        }


Em ordem, o que fiz:

- Criei uma nova Query
- Estabeleci como critério apenas os objetos do tipo livro;
- Dentro dos livros quero que sejam listados apenas os objetos cujo campo "_autor" seja igual ao objeto Autor passado como parâmetro. Aqui está o probleminha que citei mais acima. Se alterarmos o nome do campo na classe, temos que tomar cuidado de alterar o nome nas pesquisas também.
- Mandei rodar a query através de query.Execute()
- Mostra todos os resultados da query na tela.

Se você quiser ordernar os livros em ordem descendente, poderia acrescentar (antes da linha ObjectSet resultado = query.Execute(); ):

query.Descend("_titulo").OrderDescending();

Recuperando dados via Native Queries

Acredito que vocês acharão esse modo o melhor de todos, pois aqui você usa a própria linguagem de desenvolvimento para montar suas pesquisas!

Os exemplos utilizarão Generics, mas você também pode fazer as pesquisas no framework 1.1 (vou dar um exemplo mais abaixo)

Vamos criar uma nova versão do exemplo de pesquisa via SODA, agora usando native queries:

	static void LivrosDoAutorNQ(Autor autor)
        {
            IList livros = objectBag.Query(delegate(Livro candidato) 
                {
                    bool podeAdicionar = candidato.Autor == autor;
                    return podeAdicionar; 
                });
            foreach (Livro livro in livros)
            {
                Console.WriteLine(livro);
            }
        }

Para fins didáticos fui um tanto quanto prolixo.

Em ordem, o que eu fiz:

- Em uma IList<Livro> (uma lista só de livros) coloquei o conteúdo da Query tipada. Cada elemento Livro da base é verificado pelo delegate. Se atender a condição, ele é adicionado à lista.
- Faço um foreach nos resultados e mostro o resultado na tela

No caso do Framework 1.1 teremos um pouco mais de trabalho pois não temos nele métodos anônimos. Teremos que criar uma classe de pesquisa herdando de com.db4o.query.Predicate.

	public class PesquisaLivros : Predicate
    {
        private Autor _autor;
        public PesquisaLivros(Autor autor)
        {
            _autor = autor;             
        }
        public Boolean Match(Livro livro)
        {
            return (livro.Autor == _autor);
        }
    }

usando:
	static void LivrosDoAutorNQFramework11(Autor autor)
        {
            System.Collections.IList livros = objectBag.Query(new PesquisaLivros(autor));                
            foreach (Livro livro in livros)
            {
                Console.WriteLine(livro);
            }
        }


Um pouco mais trabalhoso, não? Mais um motivo para você migrar urgentemente para a versão 2.0 do .Net ;-)

Um outro exemplo: se você quiser pegar todos os objetos do tipo Livro na base:

	static void Catalogo()
        {
            foreach (Livro livro in objectBag.Query(typeof(Livro)))
            {
                Console.WriteLine(livro);
            }
        }

E no próximo capítulo...

Meu objetivo nessa primeira parte foi só introduzir o assunto. Creio que muitos nunca ouviram falar em db4o portanto preferi mostrar apenas as operações básicas. Nos próximos artigos mostrarei mais alguns exemplos de utilização.

Por enquanto sugiro que você puxe o instalador do db4o e faça algumas brincadeiras. O instalador disponibiliza um tutorial muito detalhado de todas as funções do db4o.

Baixe o código.

Até a próxima!

/**
 * Exemplo de utilização do db4o.
 * Criado por Cássio Rogério Eskelsen (eskelsen at gmail.com)
 * 29/11/2005
 * */

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using com.db4o ;
using com.db4o.query;

namespace db4oParte1
{
    class Program
    {
        static ObjectContainer objectBag;
 
        static void Main(string[] args)
        {
            // Para fins de teste, sempre uma base limpa
            FileInfo fi = new FileInfo("arquivo.yap");
            fi.Delete();            

            // "conecta" com o banco
            objectBag = Db4o.OpenFile("arquivo.yap");

            // cria massa de testes
            CriaDados();

            Autor fabio = ProcuraAutor("Fabio Camara");
            Console.WriteLine("-------------------------------------------------");
            Console.WriteLine("Autor: "+fabio);
            Console.WriteLine("----------------LIVROS DO AUTOR------------------");
            LivrosDoAutor(fabio);
            Console.WriteLine("-------------------------------------------------\n");

            Autor raul = ProcuraAutor("Raul Wazlawick");
            Console.WriteLine("Autor: " + raul);
            Console.WriteLine("-----LIVROS DO AUTOR (usando Native Queries)-----");
            LivrosDoAutorNQ(raul);
            Console.WriteLine("-------------------------------------------------\n");

            Console.WriteLine("-----------------Catálogo completo---------------\n");
            Catalogo();

            Console.Read();
            objectBag.Close();
        } 

        static void CriaDados()
        {           

            Autor raul = new Autor();
            raul.Nome = "Raul Wazlawick";
            raul.Email = "raul@acme.com";

            Livro analise = new Livro();
            analise.Autor = raul;
            analise.ISBN = "8535215646";
            analise.Titulo = "Análise e Projetos de Sistemas de Informação Orientados a Objetos";
            objectBag.Set(analise);

            Autor fabio = new Autor();
            fabio.Nome = "Fabio Camara";
            fabio.Email = "fabio@acme.com";
 
            Livro dominando = new Livro();
            dominando.Autor = fabio;
            dominando.Titulo = "Dominando o Visual Studio.Net com C#";
            dominando.ISBN = "8575021095";
            objectBag.Set(dominando);

            Livro cinquentaEOito = new Livro();
            cinquentaEOito.Autor = fabio;
            cinquentaEOito.Titulo = "58+ Soluções em .Net";
            cinquentaEOito.ISBN = "8575021583";
            objectBag.Set(cinquentaEOito);
        }

        static Autor ProcuraAutor(string nome)
        {
            Autor proto = new Autor();
            proto.Nome = nome;            
            ObjectSet resultado = objectBag.Get(proto);            
            if (resultado.HasNext())
                return (Autor)resultado.Next();
            else
                throw new Exception(nome + "  nao encontrado");
        }

        static void LivrosDoAutor(Autor autor)
        {
            Query query = objectBag.Query();
            query.Constrain(typeof(Livro));
            query.Descend("_autor").Constrain(autor);            
            ObjectSet resultado = query.Execute();
            
            if (resultado.Count> 0)
            {
                while (resultado.HasNext())
                {
                    Console.WriteLine(resultado.Next());
                }
            }
            else
            {
                Console.WriteLine("Não há nenhum livro do autor {0} cadastrado", autor.Nome);
            }
        }

        static void LivrosDoAutorNQ(Autor autor)
        {
            IList livros = objectBag.Query(delegate(Livro candidato) 
                {
                    bool podeAdicionar = candidato.Autor == autor;
                    return podeAdicionar; 
                });
            foreach (Livro livro in livros)
            {
                Console.WriteLine(livro);
            }
        }

        static void LivrosDoAutorNQFramework11(Autor autor)
        {
            System.Collections.IList livros = objectBag.Query(new PesquisaLivros(autor));                
            foreach (Livro livro in livros)
            {
                Console.WriteLine(livro);                
            }
        }

        static void Catalogo()
        {
            foreach (Livro livro in objectBag.Query(typeof(Livro)))
            {
                Console.WriteLine(livro);
            }
        }
    }

 
    public class PesquisaLivros : Predicate
    {
        private Autor _autor;
        public PesquisaLivros(Autor autor)
        {
            _autor = autor;             
        }
        public Boolean Match(Livro livro)
        {
            return (livro.Autor == _autor);
        }
    }
 
    public class Autor
    {        
        private string _nome;
        private string _email;
        
        public string Nome
        {
            get { return _nome; }
            set { _nome = value; }
        }
        public string Email
        {
            get { return _email; }
            set { _email = value; }
        }
        public override string ToString()
        {
            return this.Nome+"("+this.Email+")";
        }
    }

    public class Livro
    {
        private Autor _autor;
        private string _titulo;
        private string _isbn;
 
        public string ISBN
        {
            get { return _isbn; }
            set { _isbn = value; }
        }   

        public string Titulo
        {
            get { return _titulo; }
            set { _titulo = value; }
        }

        public Autor Autor
        {
            get { return _autor; }
            set { _autor = value; }
        }

        public override string ToString()
        {
            return string.Format("Titulo: {0}\nAutor: {1}\nISBN: {2}\n\n", this.Titulo, this.Autor.Nome, this.ISBN);
        }   
    }

}
Cassio R. Eskelsen

Cassio R. Eskelsen - Desenvolvedor C# desde 2001. Atualmente trabalha como desenvolvedor independente de soluções na área de CRM e mantém um site relacionado ao projeto Mono e a Linguagem Boo: http://www.br-mono.org.