Desenvolvimento - ASP. NET

db4o - Banco de Dados Orientado a Objetos - Parte II - Modos de Acesso, Configuração, Performance e Outras Opções

Nesse segundo artigo o autor começa a aprofundar alguns detalhes na utlização do db4o: modos de acesso, configuração, performance, índices, operações em cascata, etc.

por Cassio R. Eskelsen



Parte II - Modos de Acesso, Configuração, Performance e Outras Opções

O primeiro artigo da série dessa série teve uma repercussão surpreendente. Imaginava que havia um interesse por tecnologias pós-relacionais, mas não imaginei que o interesse fosse tão grande assim. Em contato com a equipe do db4o foi me revelado que houve um número alto de visitas a partir do site Linha de Código e que antes desse artigo o Brasil já era o 7º colocado em visitas, dentre os mais de 190 países que já visitaram o site www.db4o.com e com isso, conquistamos um fórum em português no site do db4o.

Nesse segundo artigo vou começar a aprofundar alguns detalhes na utlização do db4o: modos de acesso, configuração, performance, índices, operações em cascata, etc.

Modos de Acesso

O db4o possui 3 modos de acesso: direto, cliente/servidor e cliente/servidor embutido.

Acesso direto

O acesso direto foi o modo utilizado no exemplo do primeiro artigo. Nesse modo acessamos o arquivo com os objetos diretamente, da mesma forma que se faz quando se trabalha com uma "base" Access:

 ObjectContainer _container = Db4o.OpenFile("arquivo.yap");
Conexão direta(C#)

Esse modo de acesso deve ser utilizado preferencialmente apenas em aplicações desktop standalone, aplicações para dispositivos móveis e aplicações embarcadas. Em aplicações onde há acesso simultâneo de 2 ou mais usuários (aplicações web e c/s) esse tipo de acesso não é recomendado.

Em aplicações standalone você pode abrir um ObjectContainer no início da aplicação e deixá-lo aberto por todo tempo de vida da aplicação. Isso vai facilitar o seu desenvolvimento.

Acesso client/server

Essa é a clássica forma onde há um programa "servidor" e um ou mais programas"clientes" acessando esse servidor.

O db4o permite trabalhar com servidores de uma forma transparente. A classe container é a mesma, mas agora ela é instanciada através de Db4o.OpenClient ao invés de Db4o.OpenFile. Obviamente você precisa criar uma aplicação a mais, que será o seu "servidor".

O programa servidor deverá ter as seguintes linhas de código:

ObjectServer db4oServer = Db4o.OpenServer("arquivo.yap", porta); 
db4oServer.GrantAccess(nomeusuario1,senhausuario1); 
db4oServer.GrantAccess(nomeusuario2, senhausuario2); 
... 
db4oServer.GrantAccess(nomeusuariox,senhausuariox);
            
Servidor db4o(C#)

Db4o.OpenServer abre um arquivo com objetos e monta o servidor na porta indicada (um número de porta TCP/IP). Não existe um valor padrão para a porta.

db4oServer.GrantAccess define o nome e senha dos usuários que podem acessar esse servidor. Você pode criar um usuário padrão ou definir um nome e senha para cada usuário.

Do lado cliente, você deverá fazer:

 
ObjectContainer _container = Db4o.OpenClient(host, porta, usuário, senha);
                
Cliente db4o(C#)

O host pode ser o nome da máquina (se for uma rede local) ou o número do IP do servidor.

Algumas observações sobre o modo cliente/servidor:

  • As operações cliente/servidor são realizadas na forma read commited, ou seja, um container só verá as alterações já "commitadas" por outros. Não é necessário abrir uma transação explicitamente: o db4o sempre mantém uma transação ativa para cada container, no entanto, para confirmar a transação é necessário chamar o método Commit() do container;
  • O servidor deverá ter conhecimento das mesmas classes que o cliente, ou seja, se você tiver um projeto com suas Classes e/ou regras de negócio, você deverá fazer referência a esse projeto em seu projeto de Servidor.
Acesso client/server embutido

O db4o permite que você inicie um servidor no mesmo escopo de sua aplicação, ou seja, ao invés de ter um servidor "externo", você terá um servidor dentro de sua aplicação. Isso é especialmente útil em aplicações WEB, onde você tem várias usuários acessando a mesma aplicação ao mesmo tempo e não tem como iniciar um servidor em separado (afinal, como você faria para iniciar um servidor dentro de uma máquina de um provedor?). Também é interessante usar esse método quando suas aplicações standalone necessitarem abrir vários containers simultâneos.

Para iniciar um servidor embutido, você usuará o mesmo método OpenServer do modo cliente/servidor mas iniciará o servidor na "porta" zero. Não é necessário definir os usuários que podem acessar pois o servidor será visível apenas dentro da sua aplicação. Uma aplicação externa não terá acesso pois o container deverá ser iniciado a partir do ObjectServer criado, dessa forma:

            ObjectServer server = Db4o.OpenServer("arquivo.yap", 0);
            try
            {
                ObjectContainer client = server.OpenClient();
                // operações......
                client.Close();
            }
            finally
            {
                server.Close();
            }
                
Cliente/Servidor embutido

Fora esses detalhes na conexão, o restante das operações é idêntico ao modo Cliente / Servidor.

Como muitos já devem estar imaginando, o objeto "ObjectServer" deve ser instanciado uma única vez em toda a aplicação. Esse é um caso típico para aplicação do pattern Singleton. Segue abaixo uma sugestão de aplicação do pattern para garantir que você terá sempre apenas um objeto Server:

class Server
{
    private static ObjectServer server;   

    private static object syncLock = new object();

    protected Server()
    {
        server = Db4o.OpenServer("arquivo.yap", 0);
    }

    public static ObjectServer GetServer()
    {
        if (server  == null)
        {
            lock (syncLock)
            {
                if (server  == null)
                {
                    new Server();
                }
            }
        }

        return server;
    }
}                   
Cliente/Servidor embutido

Para obter uma instância de ObjectContainer, você faria:

ObjectContainer client = Server.GetServer().OpenClient();
                
Usando o singleton

Uma observação importante sobre os containers

Para gravar um novo objeto ou atualizar um objeto existente, o db4o disponibiliza o mesmo comando : ObjectContainer#Set(objeto). Mas aí vem a pergunta:

"Como que o db4o sabe que deve atualizar ou inserir um novo objeto?"

Se o objeto foi carregado em um container, ele estará em seu cache, e assim o db4o sabe que deverá atualizá-lo. Caso contrário, o db4o criará uma nova instância do objeto na base. Então, para atualizar um objeto, você sempre deverá primeiramente carregá-lo na memória a partir da base.

Esse conceito é importante ter em mente pois você deve tormar um cuidado especial para não carregar um objeto em uma instância de um container e salvar em outra instância.

Para entender isso melhor, vamos voltar ao exemplo que criei no primeiro artigo, especialmente nessa parte:

Autor raul = ProcuraAutor("Raul Wazlawick"); 
Console.WriteLine("Autor: " + raul); 
Console.WriteLine("-----LIVROS DO AUTOR (usando Native Queries)-----");
                
Cliente/Servidor embutido

Se após esse trecho de código você quisesse alterar o nome do autor para "Raul W.", talvez pensasse em fazer algo parecido com isso(agora já usando nosso singleton):

Autor raul = ProcuraAutor("Raul Wazlawick"); 
raul.Nome = "Raul W.";
ObjectContainer container = Server.GetServer().OpenClient();
container.Set(raul);
container.Close();            
Cliente/Servidor embutido

Sinto informar que isso não vai funcionar pois o objeto "raul" foi instanciado a partir de um container (o que foi aberto dentro da função ProcuraAutor) e você salvou usando o método Set de outra instância. Na prática o que vai acontecer é a duplicação do objeto na base. Esse é um dos erros mais comuns quando se começa a utilizar o db4o.

Uma das soluções nesse caso seria utilizar o mesmo objeto container onde foi carregado o objeto. Se você verificar aquele código verá que existe um objeto container "global" de nome objectBag. Vamos usá-lo:

Autor raul = ProcuraAutor("Raul Wazlawick"); 
raul.Nome = "Raul W.";
objectBag.Set(raul);
Cliente/Servidor embutido

Configurações

O db4o permite uma série de configurações. Algumas merecem atenção especial para melhorar a performance de utilização.

É importante observar que as configurações devem ser aplicadas ANTES de "startar" o servidor ou ANTES de abrir o arquivo(no caso do acesso direto). Elas não são gravadas junto com a base.

Vou listar algumas configurações importantes.

Db4o.Configure().Freespace().UseIndexSystem() ou Db4o.Configure().Freespace().UseRamSystem();

Quando um objeto é apagado ou atualizado, o espaço ocupado anteriormente pelo mesmo na base, é marcado como "livre" para que outros objetos possam ocupar esse espaço.O db4o possui duas formas para "lembrar" quais são os espaços livres:

UseRamSystem

Mantém na memória duas listas ordenadas por endereço e tamanho.

Vantagens: rápido
Desvantagens: ocupa muita memória quando ocorrem muitas operações de atualização e exclusão e pode haver perda das listas no caso de um desligamento inesperado do sistema

UseIndexSystem

Mantém um sistema de controle de espaço livre baseado em índices.

Vantagens:ACID, não é perdida nenhuma informação a respeito dos espaço livres; baixo consumo de memória; .
Desvantagens:mais lento que o método baseado em RAM, pois a informação sobre os espaços livres é gravada junto a cada commit

A segunda forma passou a ser a opção default na versão 5.0 do db4o, no entanto, para aplicações onde não se espera tanta queda/desligamento abrupto do sistema, a opção mais indicada é a primeira. Para alterar isso, basta adicionar a seguinte linha de código:

 Db4o.Configure().Freespace().UseRamSystem();

Para aplicações baseadas em dispositivos móveis é mais recomendado que voê utilize o IndexSystem.

Db4o.configure().callConstructors(true);

Em máquinas virtuais onde isso é suportado, o db4o cria instâncias dos objetos sem chamar um construtor e para isso ele usa reflection. No entanto, o processo de reflection é um pouco mais lento do que chamar um construtor pré-definido. Se você quer melhorar a perfomance do seu aplicativo, crie sempre um construtor sem parâmetros para suas classes persistidas e chame o método .callConstructors() passando o parâmetro true. Dessa forma o db4o sempre criará instâncias de suas classes chamando o construtor.

Alternativamente, você pode definir isso por classe, ao invés da forma global como exposto acima. Para isso chame:

 Db4o.Configure().ObjectClass(sua_classe).CallConstructor(true);

Alterando o tamanho da base

Por padrão, as bases de dados do db4o podem ter no máximo 2 Gb. Aumentando o tamanho do bloco que o db4o utiliza internamente, o limite de tamanho pode ser aumentado em múltiplos de 2Gb. Qualquer valor entre 1 byte (2GB) e 127 bytes (254GB) pode ser usado. O valor default é 1

Deve-se tomar cuidado para não escolher um valor inapropriado pois pode ocorrer desperdício de espaço e também queda na performance. O valor ideal é 8

Para definir o comando você deve usar:

 Db4o.Configure().BlockSize(novo_tamanho_do_bloco); 
Defragment.main(new String[] {"suabase.yap"});

Ao invés de usar uma única gigantesca base, você também pode criar várias bases menores.

Performance

Abaixo listarei algumas dicas para melhoria da performance das aplicações que usam db4o.

Desligar callbacks

O mecanismo de callback permite que métodos com uma determinada assinatura (você pode ver uma lista nesse link ) sejam chamados em algumas situações, como por exemplo CanDelete, que verifica se um objeto pode ou não ser apagado. No entanto, para verificar se a classe implementa ou não esse método é feito um "escaneamento" na classe em busca desse método. Caso você não utilize o mecanismo de callback, pode desligá-lo usando o seguinte comando:

 Db4o.Configure().Callbacks(false);

Desligar verificação automática de mudança de estrutura

Cada vez que o sistema é iniciado, o db4o, através de reflection, verifica se houve alguma alteração na estrutura da classe (novos campos, etc).

Se você se sentir seguro, pode desabilitar essa verificação automática, cuidando para que quando ocorrer alguma mudança na estrutura, não esquecer de reativar esse recurso para que a estrutura da classe na base seja atualizada.

Para desabilitar, você precisa passar o seguinte comando:

 Db4o.Configure().DetectSchemaChanges(false);

Desfragmentação

Algumas situações geram um pouco de "lixo" na base do db4o:

  • Sempre que um objeto é alterado, ele é gravado em uma nova posição, e a antiga continua a ocupar 8 bytes, até que seja rodada uma fragmentação.
  • Quando você remove um campo de uma classe, o db4o não elimina esse campo da base, apenas o "oculta"

O processo de fragmentação elimina todo esse lixo copiando todos os objetos para um novo arquivo e deve ser executado regularmente. Para executar a desfragmentação, rode o seguinte trecho de código:

 new Defragment().Run("seuarquivodeobjetos", delete);

Índices

Como em qualquer banco, o db4o permite que sejam definidos índices para melhorar a performance de leitura

Da mesma forma que os outros comandos de configuração, esse precisa ser dado ANTES de abrir um objectContainer ou objectServer, no entanto, no entanto, uma vez definido o índice, ele não precisa ser re-definido a cada abertura da base.

Baseado no exemplo anterior, se quiséssemos definir um índice no campo "Nome" da classe Autor, usaríamos o seguinte comando

Db4o.Configure().ObjectClass(typeof(Autor)).ObjectField("Nome").Indexed(true);

Como é de se imaginar, para retirar o índice, basta mudar o parâmetro da função Indexed para false:

Db4o.Configure().ObjectClass(typeof(Autor)).ObjectField("Nome").Indexed(false);

Cenas dos próximos capítulos...

Com esses comandos básicos já é possível "afinar" a utilização do db4o. Existem outras opções, mas iremos com calma.

Se você quiser ver uma aplicação "real" usando o db4o, pode ver o provider que criei para o asp.net 2.0. Esse provider implementa as funções de login e regras de acesso disponíveis no asp.net 2.0. O código é LGPL e está disponível no Source Forge.

Até a próxima!

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.