Este artigo foi traduzido por máquina.

Acesso a dados

Criando um aplicativo de tarefas para desktop com o NHibernate

Oren Eini

Baixar o código de exemplo

NHibernate é um mapeador relacional do objeto (ou / M), tasked com tornando o trabalhar com um banco de dados como é trabalhar com objetos na memória. É um dos mais popular OR / M estruturas para desenvolvimento do Microsoft .NET Framework. Mas a maioria dos usuários do NHibernate são isso no contexto de aplicativos, portanto, há relativamente pouco informações sobre a criação de aplicativos NHibernate na área de trabalho.

Ao usar NHibernate em um aplicativo, tendem a usar o estilo de solicitação por sessão, que tem várias implicações são fáceis de perder. Eu não se preocupe sessão manter uma referência a entidades carregadas, porque espera que que a sessão irá fora em breve. Não tenho preocupar (muito) de tratamento de erros como eu pode simplesmente anular a solicitação atual e sua sessão associado se ocorrer um erro.

Vida útil da sessão é bem definido. Não preciso atualizar outras sessões sobre quaisquer alterações feitas. Não preciso preocupar mantendo uma transação longa no banco de dados ou até mesmo mantendo uma conexão abrir por um longo tempo, porque eles são apenas alive para a duração de uma única solicitação.

Como você pode imaginar, essas são questões em um aplicativo de área de trabalho. Apenas para ser claro sobre ele, eu estou falando de um aplicativo falando diretamente a um banco de dados. Um aplicativo que usa algum tipo de serviços remotos usando o NHibernate no servidor remoto, sob o cenário por solicitação e não é o foco deste artigo. Este artigo não aborda a cenários ocasionalmente conectados, embora muito a discussão aplicaria a esses cenários.

Criar um aplicativo de desktop do NHibernate não é muito diferente de criação de um aplicativo de desktop usando qualquer outra tecnologia de persistência. Muitos dos desafios que pretendo contorno neste artigo são compartilhados entre todas as tecnologias de acesso a dados:

  • Gerenciando o escopo de unidades de trabalho.
  • Reduzindo a duração de conexões de banco de dados aberto.
  • Propagar entidade altera para todas as partes do aplicativo.
  • Suporte a vinculação de dados bidirecional.
  • Reduzindo o tempo de inicialização.
  • Evitando o thread UI de bloqueio ao acessar o banco de dados.
  • Tratamento e resolvendo conflitos de simultaneidade.

Enquanto as soluções que eu dar estão lidando exclusivamente com NHibernate, pelo menos a maioria dos-las também são aplicáveis a outras tecnologias de acesso a dados. Um tal desafio, compartilhado por todas as tecnologias de acesso a dados que estou ciente de, é como gerenciar o escopo unidade do aplicativo de trabalho — ou, em termos do NHibernate, a vida útil da sessão.

Gerenciando sessões

Uma prática ruim comum com aplicativos de desktop NHibernate é ter uma única sessão global para o aplicativo inteiro. É um problema por várias razões, mas três deles são mais importantes. Porque uma sessão mantém uma referência para tudo o que ele carregado, o usuário trabalha no aplicativo vai carregar uma entidade, trabalhar um pouco com ele e esquecer sobre ele. Mas porque a sessão global única é manter uma referência a ele, a entidade nunca é liberada. Em essência, você tem um vazamento de memória no seu aplicativo.

Em seguida, é o problema de tratamento de erro. Se você receber uma exceção (como StaleObjectStateException devido a conflito de simultaneidade), sua sessão e suas entidades carregadas são brindar, porque com NHibernate, uma exceção lançada de uma sessão move a sessão em um estado indefinido. Você não poderá mais usar essa sessão ou quaisquer entidades carregadas. Se você tiver somente uma única sessão global, significa que você provavelmente precisará reiniciar o aplicativo, provavelmente não é uma boa idéia.

Finalmente, é o problema de transação e tratamento de conexão. Enquanto não abrir uma sessão é semelhante a abertura de uma conexão de banco de dados, usar uma única sessão significa que são muito mais provável que mantenha transações e conexão por mais tempo que você deve.

Outro igualmente incorreto e, infelizmente, quase como comum praticar NHibernate é micromanaging a sessão. Um exemplo típico é código como este:

public void Save(ToDoAction action) {
  using(var session = sessionFactory.OpenSession())
  using(var tx = session.BeginTransaction()) {
    session.SaveOrUpdate(action);

    tx.Commit();
  }
}

O problema com esse tipo de código é que ele remove muitas vantagens que você adquire em usando NHibernate. NHibernate está fazendo um pouco de trabalho para manipular o gerenciamento de alteração e persistência transparente para você. Micromanaging a sessão recorta logoff capacidade do NHibernate para isso e move onus de fazer funcionar para você. E é mesmo sem mencionar problemas causa com carregamento lento a linha para baixo. Em qualquer sistema onde já vi uma abordagem tentada, os desenvolvedores tinham mais difícil trabalhar para resolver esses problemas.

Uma sessão deve não ser mantida aberta por muito tempo, mas ele deve também não ser mantido aberto por um tempo muito curto fazer uso de recursos do NHibernate. Geralmente, tente corresponder a vida útil da sessão para a ação real que está sendo executada pelo sistema.

A prática recomendada para aplicativos de desktop é usar uma sessão por formulário, para que cada formulário do aplicativo tem sua própria sessão. Cada formulário geralmente representa um distinto parte do trabalho que o usuário deseja executar, portanto, a vida útil da sessão correspondente para o tempo de vida do formulário funciona muito bem na prática. O benefício adicional é que você não tiver mais um problema com vazamentos de memória, porque quando você fechar um formulário no aplicativo, também dispose da sessão. Isso tornaria todas as entidades que foram carregadas por sessão qualificada para recuperação pelo coletor de lixo (GC).

Há razões adicionais para preferindo uma única sessão por formulário. Você pode aproveitar controle de alterações do NHibernate, portanto, ele irá liberar todas as alterações para o banco de dados quando você confirmar a transação. Ele também cria uma barreira de isolamento entre formulários diferentes, portanto, você pode confirmar as alterações para uma única entidade sem se preocupar com as alterações para outras entidades são mostradas em outros formulários.

Enquanto esse estilo de gerenciar a vida útil da sessão é descrito como uma sessão por formulário, na prática você geralmente gerenciar sessão por apresentador. O código do Figura 1 é obtido da classe base apresentador.

Figura 1 do Gerenciamento de sessão do apresentador

protected ISession Session {
  get {
    if (session == null)
      session = sessionFactory.OpenSession();
    return session;
  }
}

protected IStatelessSession StatelessSession {
  get {
    if (statelessSession == null)
      statelessSession = sessionFactory.OpenStatelessSession();
    return statelessSession;
  }
}

public virtual void Dispose() {
  if (session != null)
    session.Dispose();
  if (statelessSession != null)
    statelessSession.Dispose();
}

Como você pode ver, eu ociosamente abrir uma sessão (ou uma sessão sem estado) e manter aberta até que eu dispose do apresentador. Esse estilo bastante perfeitamente corresponde à vida útil do próprio formulário e permite-me associar uma sessão separada com cada apresentador.

Manter conexões

Apresentadores, você não precisa se preocupar sobre como abrir ou fechar a sessão. Na primeira vez que você acessar uma sessão é aberto para você, e ele será dispose próprio corretamente também. Mas e quanto a conexão de banco de dados associado com a sessão? Tem você mantendo uma conexão de banco de dados aberto para como o usuário está exibindo o formulário?

A maioria dos bancos de dados energicamente precisar manter uma transação aberta por longos períodos de tempo. Ele geralmente resulta em causando erros ou deadlocks da linha para baixo. Conexões abertas podem causar problemas semelhantes, porque um banco de dados pode aceitar apenas conexões tantas antes da execução de recursos para manipular a conexão.

Para maximizar o desempenho do servidor de banco de dados, você deve manter vida útil de transação para um mínimo e fechar conexões assim que possível, confiar no pool de conexão para garantir tempos de resposta rápido quando você abrir uma nova conexão.

Com NHibernate, a situação é muito o mesmo, exceto que NHibernate contém vários recursos que existem explicitamente faça coisas mais fácil para você. A sessão NHibernate não tem uma associação um-para-um com uma conexão de banco de dados. Em vez disso, NHibernate gerencia a conexão de banco de dados internamente, abrindo e fechando conforme necessário. Isso significa que você não precisa manter algum tipo de estado do aplicativo para se desconectar e reconectar-se ao banco de dados conforme necessário. Por padrão, NHibernate minimizará a máxima extensão duração na qual uma conexão é aberta.

Você precisa se preocupar sobre como fazer transações menor possível. Em particular, uma das coisas que você não deseja é mantenha uma transação aberta para a vida útil do formulário. Isso forçará NHibernate para manter a conexão aberta para a duração da transação. E porque o tempo de vida de um formulário é medido em tempos de resposta humano, é mais do que provável que você acabaria mantendo até uma transação e a conexão por períodos maiores que está realmente íntegro.

O que você geralmente irá fazer é abrir transações separadas para cada operação que você fizer. Let’s examinar um modelo de formulário que mostra uma lista de tarefas simples, meu aplicativo de exemplo de escolha (consulte do Figura 2). O código para manipular esse formulário é bem simples, como você pode ver no do Figura 3.

A To-Do List Application

Figura 2 de de um aplicativo de lista de tarefas

Figura 3 Criando o formulário de tarefas

public void OnLoaded() {
  LoadPage(0);
}

public void OnMoveNext() {
  LoadPage(CurrentPage + 1);
}

public void OnMovePrev() {
  LoadPage(CurrentPage - 1);
}

private void LoadPage(int page) {
  using (var tx = StatelessSession.BeginTransaction()) {
    var actions = StatelessSession.CreateCriteria<ToDoAction>()
      .SetFirstResult(page * PageSize)
      .SetMaxResults(PageSize)
      .List<ToDoAction>();

    var total = StatelessSession.CreateCriteria<ToDoAction>()
      .SetProjection(Projections.RowCount())
      .UniqueResult<int>();

    this.NumberOfPages.Value = total / PageSize + 
               (total % PageSize == 0 ? 0 : 1);
    this.Model = new Model {
      Actions = new ObservableCollection<ToDoAction>(actions),
      NumberOfPages = NumberOfPages,
      CurrentPage = CurrentPage + 1
    };
    this.CurrentPage.Value = page;

    tx.Commit();
  }
}

Tenho três operações: Carregando o formulário pela primeira vez, mostrando a primeira página e paginação voltar e Avançar pelos registros.

Cada operação, eu começar e confirmar uma transação separada. Dessa forma que não consumir qualquer recursos no banco de dados e não precisa se preocupar sobre transações longas. NHibernate abrirá uma conexão ao banco de dados automaticamente ao iniciar uma nova transação e fechar depois que a transação é concluída.

Sessões sem estado

Há outro subtlety pequeno eu deve observar: Não Estou usando um ISession. Em vez disso, Estou usando IStatelessSession em seu lugar para carregar os dados. Sessões sem monitoração de estado são normalmente usadas na manipulação de dados em massa, mas nesse caso, estou fazendo uso de uma sessão sem estado para resolver problemas de consumo de memória.

É uma sessão sem estado, bem, sem estado. Ao contrário de uma sessão normal, ele não manter uma referência a entidades que ele carrega. Como tal, é perfeitamente adequada para carregar entidades para fins de exibição. Que tipo de tarefa, você geralmente carregar as entidades do banco de dados, lança no formulário e esquecer sobre eles. Uma sessão sem monitoração de estado é exatamente o que você precisa nesse caso.

Mas sessões sem estado vêm com um conjunto de limitações. Foco atenção deve entre eles nesse caso é que sessões sem estado não oferecem suporte a carregamento lento, não envolvem próprios no modo de evento NHibernate usual e não fazer uso do NHibernate do cache de recursos.

Por esses motivos, geralmente uso-los para consultas simples onde quero apenas Mostrar as informações de usuário sem fazer nada complicado. Em casos onde desejo mostrar uma entidade para edição, eu ainda poderia fazer uso de uma sessão sem estado, mas eu tendem a evitar que a favor de uma sessão normal.

No formulário principal do aplicativo, eu se esforçar para tornar todos os dados somente para exibição e tente tornar use sessões sem estado sozinho. O formulário principal reside para desde que o aplicativo é aberto e uma sessão monitorador vai ser um problema, não apenas porque ele manterá uma referência a entidades que ele carregado, mas porque provavelmente causar problemas se a sessão lança uma exceção.

NHibernate considera uma sessão que emitiu uma exceção para ser em um estado indefinido (somente Dispose tem um comportamento definido nesse caso). Você precisaria substituir a sessão e porque entidades carregado por uma sessão monitorador manter uma referência a ele, seria necessário limpar todas as entidades que foram carregadas por sessão agora extinta. É muito mais simples usar uma sessão sem estado nessa circunstância.

Entidades carregadas por sessões sem estado não faz para o estado da sessão e recuperar de um erro em uma sessão sem monitoração de estado é simples, como fechar a sessão atual sem estado e abrir uma nova.

Manipulando dados

Figura 4 mostra o modelo de tela de edição. Os desafios enfrentam quando precisar editar entidades?

Editing Entities

Figura 4 Editando entidades

Bem, você realmente tem dois desafios separados aqui. Primeiro, você deseja ser capaz de usar o NHibernate controle de alterações, para que você possa exibir uma entidade (ou um gráfico de objeto de entidade) e ter NHibernate apenas persistem ele quando você terminar. Segundo, depois que você salvar uma entidade, você deseja garantir que cada formulário também exibe esta entidade é atualizado com novos valores.

O primeiro item de negócios é realmente razoavelmente fácil manipular. Tudo o que precisa fazer é fazer uso da sessão associado ao formulário e que é. Figura 5 mostra o código orientando esta tela.

Figura 5 uma entidade na sessão de edição

public void Initialize(long id) {
  ToDoAction action;
  using (var tx = Session.BeginTransaction()) {
    action = Session.Get<ToDoAction>(id);
    tx.Commit();
  }

  if(action == null)
    throw new InvalidOperationException(
      "Action " + id + " does not exists");

  this.Model = new Model {
    Action = action
  };
}

public void OnSave() {
  using (var tx = Session.BeginTransaction()) {
    // this isn't strictly necessary, NHibernate will 
    // automatically do it for us, but it make things
    // more explicit
    Session.Update(Model.Action);

    tx.Commit();
  }

  EventPublisher.Publish(new ActionUpdated {
    Id = Model.Action.Id
  }, this);

  View.Close();
}

Obtenha a entidade do banco de dados no método Initialize(id) e atualizar no método OnSave. Observe que isso seja feito em duas transações separadas, em vez de manter uma transação alive por um longo período de tempo. Também é esta chamada EventPublisher estranha. O que é que tudo sobre?

EventPublisher está aqui para lidar com outro desafio: Quando cada formulário tem sua sessão, cada formulário tem 
instances diferentes das entidades que trabalhar com. Na face do, que parece um desperdício. Por que deve você carregar a mesma ação várias vezes?

Na realidade, ter essa separação entre os formulários simplificará o aplicativo consideravelmente. Considere o que aconteceria se compartilhada as instâncias da entidade do tabuleiro. Nessa situação, você encontraria sozinho com um problema em qualquer cenário concebível editar. Considere o que aconteceria se você exibir uma entidade em dois formulários que permitem a edição dessa entidade. Que pode ser uma grade editável e um formulário de edição detalhadas, por exemplo. Se você fizer uma alteração para a entidade na grade, abra o formulário Editar detalhadas e salvá-lo, o que aconteceria a alteração feita na grade editável?

Se você empregar uma instância de entidade única em todo o aplicativo, é provável que salvar o formulário de detalhes também causaria você salvar as alterações feitas usando a grade. Que provavelmente não é algo que você gostaria de fazer. Compartilhar uma instância da entidade também torna muito mais difícil fazer coisas como cancelar um formulário de edição e todas as alterações não salvas desaparecem.

Esses problemas simplesmente não existem quando você usar uma instância de entidade por formulário, é uma coisa boa, como isso é mais ou menos obrigatório quando você usar uma sessão por abordagem de formulário.

Eventos de publicação

Mas ainda não totalmente expliquei a finalidade de EventPublisher ainda. É realmente bem simples. Em vez de uma única instância da entidade no aplicativo, você pode ter muitos, mas o usuário ainda gostaria de ver a entidade (uma vez corretamente salva) atualizada em todos os formulários que mostram dessa entidade.

No meu exemplo faço isso explicitamente. Sempre que salvar uma entidade, Posso publicar um evento dizendo que fiz assim e qual entidade. Isso não é um evento .NET padrão. Um evento .NET requer uma classe para assinar diretamente. Que realmente não funciona para esse tipo de notificação porque ele exigiria cada formulário para registrar eventos em todos os outros formulários. Apenas tentando gerenciar que seria um pesadelo.

O EventPublisher é uma publicação - inscrever-se o mecanismo que uso para desassociar um editor de sua assinante. A única aspectos comuns entre eles é a classe EventPublisher. Uso o tipo de evento (ActionUpdated no do Figura 5) para decidir quem informar sobre o evento.

Let’s examinar do outro lado do que agora. Quando atualizo uma ação de tarefas pendentes, gostaria de mostrar os valores atualizados no formulário principal, que mostra uma grade de tarefas ações. Aqui está o código relevante do apresentador que formulário:

public Presenter() {
  EventPublisher.Register<ActionUpdated>(
    RefreshCurrentPage);
}

private void RefreshCurrentPage(
  ActionUpdated actionUpdated) {
  LoadPage(CurrentPage);
}

Na inicialização, me registro método RefreshCurrentPage para o evento ActionUpdated. Agora, sempre que esse evento é gerado, que simplesmente atualizo a página atual por LoadPage chamada, que você já estiver familiarizado com.

Isso é realmente uma implementação bastante lenta. Eu não importa se a página atual estiver mostrando a entidade editada; apenas atualizo-lo assim mesmo. Uma implementação mais complexa (e eficiente) só seria atualizar os dados de grade se a entidade atualizada é mostrada nessa página.

A principal vantagem de usar a publicação-inscrever mecanismo dessa maneira é desassociação Editor e assinantes. Eu não se preocupa com no formulário principal formulário Editar publica o evento ActionUpdated. A idéia de publicação de evento e se inscrever publicar é um fundamento na criação vagamente acopladas interfaces de usuário e é amplamente abordado no Composite Application Guidance (msdn.microsoft.com/library/cc707819 ) da equipe de padrões e práticas da Microsoft.

Há outro caso a pena considerar: O que aconteceria se você tiver dois formulários de edição para a mesma entidade aberto ao mesmo tempo? Como você obter novos valores do banco de dados e mostrar ao usuário?

O código a seguir é tirado do apresentador Editar formulário:

public Presenter() {
  EventPublisher.Register<ActionUpdated>(RefreshAction);
}

private void RefreshAction(ActionUpdated actionUpdated) {
  if(actionUpdated.Id != Model.Action.Id)
    return;
  Session.Refresh(Model.Action);
}

Este código registra o evento ActionUpdated e se a entidade que você está editando, pergunte NHibernate para atualizar do banco de dados.

Esse modelo explícito de atualizar a entidade do banco de dados também oferece a chance de tomar decisões sobre o que deve acontecer. Deve atualizar automaticamente, apagando todas as alterações do usuário? Você deve perguntar ao usuário? Tentar silenciosamente mesclar as alterações? Essas são todas as decisões que agora você tem a chance de lidar com uma maneira simples.

Na maioria dos casos, no entanto, acho que simplesmente atualizando a entidade é bastante suficiente, pois você geralmente não permitir atualizando de uma única entidade em paralelo (pelo menos não por um único usuário).

Enquanto esse código de atualização de entidade realmente atualizará os valores a instância da entidade, como você vai fazer responder UI essa alteração? Você tem dados ligados os valores de entidade para os campos de formulário, mas você precisa de alguma maneira de informando UI que esses valores foram alterados.

O Microsoft .NET Framework fornece a interface INotifyPropertyChanged, qual a maioria das estruturas UI entender e saber como trabalhar com. Aqui está a definição de INotifyPropertyChanged:

public delegate void PropertyChangedEventHandler(
  object sender, PropertyChangedEventArgs e);

public class PropertyChangedEventArgs : EventArgs {
  public PropertyChangedEventArgs(string propertyName);
  public virtual string PropertyName { get; }
}

public interface INotifyPropertyChanged {
  event PropertyChangedEventHandler PropertyChanged;
}

Um objeto que implementa essa interface deve disparar o evento PropertyChanged com o nome da propriedade foi alterada. A UI inscreverá para o evento PropertyChanged e sempre que uma alteração é aumentada em uma propriedade que está vinculada, ele atualizará a vinculação.

Implementar isso é muito fácil:

public class Action : INotifyPropertyChanged {
  private string title;
  public virtual string Title {
    get { return title; }
    set {
      title = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Title"));
    }
  }

  public event PropertyChangedEventHandler 
    PropertyChanged = delegate { };
}

Enquanto simples, ele é bastante repetitivo código e só é necessário para satisfazer às questões de infra-estrutura de IU.

Interceptando criação da entidade

Não quero ter escrever código apenas para obter a ligação de dados UI funcionando corretamente. E acontece, não realmente preciso.

Um dos requisitos NHibernate é tornar todos os métodos e propriedades em suas classes virtual. NHibernate requer isso para tratar preocupações de carregamento lento adequadamente, mas você pode aproveitar esse requisito por outros motivos.

Uma coisa que você pode fazer é aproveitar a palavra-chave virtual para injetar o mix de seu próprio comportamento. Fazer isso usando uma técnica chamada aspect-Oriented programação (AOP). Em essência, levar uma classe e adicionar comportamentos adicionais a essa classe em tempo de execução. O mecanismo exato de como implementar isso está fora do escopo deste artigo, mas é encapsulado na classe DataBindingFactory, cuja definição é:

public static class DataBindingFactory {
  public static T Create<T>();
  public static object Create(Type type);
}

Toda a implementação da classe é cerca de 40 linhas não extremamente complexas. Ele faz é levar um tipo e produzir uma instância desse tipo também totalmente implementa o contrato INotifyPropertyChanged. Em outras palavras, o teste a seguir funcionará:

ToDoAction action = DataBindingFactory.Create<ToDoAction>();
string changedProp = null;
((INotifyPropertyChanged)action).PropertyChanged 
  += (sender, args) => changedProp = args.PropertyName;
action.Title = "new val";
Assert.Equal("Title", changedProp);

Dado que, tudo o que precisa fazer agora é fazer uso do DataBindingFactory sempre que você criar uma nova classe nos apresentadores. A principal vantagem de se obter de um sistema é que agora, se você deseja fazer uso do modelo de domínio NHibernate em um contexto de apresentação não, você pode simplesmente não utilizam o DataBindingFactory e obter um modelo de domínio completamente livre de preocupações de apresentação.

Ainda há um problema, porém. Enquanto você pode criar novas instâncias de entidades usando DataBindingFactory, muito o tempo você terá que lidar com instâncias criadas pelo NHibernate. Obviamente, NHibernate sabe nada sobre seu DataBindingFactory e não pode fazer usá-lo. Mas antes de despair, você pode fazer uso de um dos pontos de extensão mais úteis com NHibernate, o interceptador. Interceptador do NHibernate permite que você assumir, em essência, algumas das funcionalidades que NHibernate está realizando internamente.

Uma das funcionalidades que o interceptador permite que você assumir é a criação de novas instâncias de entidades. Figura 6 mostra um interceptador cria instâncias de entidades usando o DataBindingFactory.

Figura 6 interceptando Entity criação

public class DataBindingInterceptor : EmptyInterceptor {
  public ISessionFactory SessionFactory { set; get; }

  public override object Instantiate(string clazz, 
    EntityMode entityMode, object id) {

    if(entityMode == EntityMode.Poco) {
      Type type = Type.GetType(clazz);
      if (type != null) {
        var instance = DataBindingFactory.Create(type);
        SessionFactory.GetClassMetadata(clazz)
          .SetIdentifier(instance, id, entityMode);
        return instance;
      }
    }
    return base.Instantiate(clazz, entityMode, id);
  }

  public override string GetEntityName(object entity) {
    var markerInterface = entity as
      DataBindingFactory.IMarkerInterface;
    if (markerInterface != null)
      return markerInterface.TypeName;
    return base.GetEntityName(entity);
  }
}

Substituir o método de criação de uma instância e manipular o caso onde podemos obter uma entidade com um tipo que você reconhece. Em seguida, vá para criar uma instância da classe e defina sua propriedade de identificador. Você também precisa ensiná NHibernate entender que tipo uma instância criada via DataBindingFactory pertence, fazer no método GetEntityName do intercepter.

A única coisa esquerda agora é configurar NHibernate com novo interceptador. A seguir é obtida da classe BootStrapper, responsável por configurar o aplicativo:

public static void Initialize() {
  Configuration = LoadConfigurationFromFile();
  if(Configuration == null) {
    Configuration = new Configuration()
      .Configure("hibernate.cfg.xml");
    SaveConfigurationToFile(Configuration);
  }
  var intercepter = new DataBindingIntercepter();
  SessionFactory = Configuration
    .SetInterceptor(intercepter)
    .BuildSessionFactory();
  intercepter.SessionFactory = SessionFactory;
}

Por enquanto, ignorar a semântica de configuração — será endereço que em um bit. O ponto importante é criar o interceptador, definida na configuração e criar a fábrica de sessão. A última etapa é definir a fábrica de sessão no interceptador. É um pouco estranho, admito, mas que é a maneira mais simples de obter a fábrica de sessão apropriado para o interceptador.

Depois que o interceptador cabeada, cada instância de entidade NHibernate cria agora suporte INotifyPropertyChanged notificações sem a necessidade de fazer qualquer trabalho em todos os. Considero isso bastante uma solução elegante para o problema.

Há alguns que diriam que escolher uma solução é um problema de uma perspectiva de desempenho sobre a implementação de codificação disco rígido. Na prática, que acontece a ser uma suposição false. A ferramenta que Estou usando (castelo proxy dinâmica) para executar esta extensão no instantaneamente de classes foi intensamente otimizada para assegurar desempenho ótimo.

Endereçamento de desempenho

Falando de desempenho, uma preocupação adicional em aplicativos desktop que não têm em aplicativos é tempo de inicialização. Em aplicativos é bastante comum para decidir favorecer inicialização mais vezes para aumentar o desempenho de solicitação. Aplicativos desktop, desejar reduzir o tempo de inicialização tanto quanto possível. Na verdade, um trapacear comuns com o aplicativo de desktop é simplesmente mostrar uma captura de tela do aplicativo para o usuário até que o aplicativo termina iniciando.

Infelizmente, tempo de inicialização NHibernate é um pouco longo. Isso ocorre principalmente porque NHibernate está realizando um lote de inicialização e verifica na inicialização, para que possa executar mais rápido durante a operação normal. Há duas maneiras comuns de tratamento esse problema.

A primeira é iniciar NHibernate em um thread de segundo plano. Enquanto isso significa que a UI mostrará até muito mais rápido, ele também cria uma complicação 
for o próprio aplicativo, porque você não pode mostrar o usuário nada do banco de dados até concluir a inicialização da fábrica de sessão.

Outra opção é serializar a classe de configuração do NHibernate. Uma grande quantidade de custo relacionada à inicialização NHibernate está relacionada ao custo de validar as informações passadas para a classe de configuração. A classe de configuração é uma classe serializável e, portanto, você pode pagar preço somente uma vez após o qual você pode atalho o custo por carregar uma instância já validada de armazenamento persistente.

Que é a finalidade de LoadConfigurationFromFile e SaveConfigurationToFile, serialização e desserialização configuração do NHibernate. Usando essas que ter somente para criar tempo de configuração o primeiro iniciar o aplicativo. Mas há um pequeno catch que deve estar ciente de: Você deve invalidar a configuração em cache se o assembly de entidades ou o arquivo de configuração NHibernate foi alterado.

O código de exemplo para este artigo contém uma implementação completa que ‘ s atento isso e invalida o arquivo em cache se tiver alterado a configuração ou as entidades.

Há outro problema de desempenho que você tenha que lidar com. Chamar o banco de dados é uma das operações mais caras que torna o aplicativo. Como tal, não é algo que você gostaria de fazer no segmento UI do aplicativo.

Tais obrigações são freqüentemente relegadas a um thread de segundo plano e você pode fazer o mesmo com NHibernate, mas tenha em mente que a sessão NHibernate não é thread-safe. Enquanto você pode fazer uso de uma sessão de vários threads (ela tem sem afinidade de thread), você não deve usar uma sessão (ou suas entidades) em vários threads em paralelo. Em outras palavras, é perfeitamente bem usar a sessão em um thread de segundo plano, mas você deve serializar o acesso à sessão e não permitir acesso simultâneo a ele. Usando a sessão de vários threads em paralelo irá resultar em comportamento indefinido; em outras palavras, ele deve ser evitado.

Felizmente, há algumas medidas relativamente simples que você pode tomar para garantir acesso a sessão é serializado. A classe System.ComponentModel.BackgroundWorker foi explicitamente projetada para tratar esses tipos de tarefas. Ele permite que você executar uma tarefa em um thread de segundo plano e notificar quando é concluída, tomando cuidado o problema de sincronização de thread UI, é tão importante nos aplicativos de desktops.

Você viu anteriormente como gerenciar uma entidade existente, fiz diretamente no thread de UI de edição. Agora, salve uma nova entidade let’s em um thread de segundo plano. O código a seguir é a inicialização do apresentador criar novo:

private readonly BackgroundWorker saveBackgroundWorker;

public Presenter() {
  saveBackgroundWorker = new BackgroundWorker();
  saveBackgroundWorker.DoWork += 
    (sender, args) => PerformActualSave();
  saveBackgroundWorker.RunWorkerCompleted += 
    (sender, args) => CompleteSave();
  Model = new Model {
    Action = DataBindingFactory.Create<ToDoAction>(),
    AllowEditing = new Observable<bool>(true)
  };
}

O BackgroundWorker é usado para executar o processo que foi dividido em duas partes distintas de gravação real. Além da divisão, é muito semelhante à maneira como eu manipulado no cenário de edição. Outro bit interessante que você precisa prestar atenção é a propriedade AllowEditing; esta propriedade é usada para bloquear a UI no formulário quando você estiver executando um salvamento operação. Dessa forma, você pode usar com segurança a sessão em outro thread, sabendo que haverá acesso simultâneo a sessão ou em qualquer sua entidade pelo formulário.

O próprio processo salvando deve ser bastante familiar para você agora. Let’s examinar o método OnSave primeiro:

public void OnSave() {
  Model.AllowEditing.Value = false;
  saveBackgroundWorker.RunWorkerAsync();
}

Esse método é responsável pela desabilitação de edição no formulário e, em seguida, kicking desativar o processo de plano de fundo. Em segundo plano, executar a gravação real. O código não deve vir como uma surpresa:

private void PerformActualSave() {
  using(var tx = Session.BeginTransaction()) {
    Model.Action.CreatedAt = DateTime.Now;
    
    Session.Save(Model.Action);
    tx.Commit();
  }
}

Quando salvar real para o banco de dados é concluída, o BackgroundWorker executará parte CompleteSave do processo no thread da interface do usuário:

private void CompleteSave() {
  Model.AllowEditing.Value = true;
  EventPublisher.Publish(new ActionUpdated {
    Id = Model.Action.Id
  }, this);

  View.Close();
}

Reativar o formulário, publicar uma notificação que uma ação foi atualizada (fazendo com que as telas relevantes atualizar também) e finalmente fechar o formulário. Eu suponha que ativar a interface não é estritamente necessário, mas incluí-lo lá para conclusão hospedarei.

Usando essa técnica, você pode tirar proveito de processamento sem violar o contrato threading em instâncias de sessão de plano de fundo. Como sempre, threading é uma ótima maneira de criar um aplicativo mais responsivo, mas a programação multithread não é uma tarefa para aproximou lightly, então, usar essa técnica com cuidado.

Lidando com simultaneidade

Simultaneidade é um tópico complexo com melhor desempenho de vezes e não está limitado a segmentação sozinho. Considere o caso onde você tem dois usuários editem a mesma entidade ao mesmo tempo. Um deles será atingido no botão Enviar primeiro, salvar as alterações no banco de dados. A pergunta é, o que deve acontecer quando o segundo usuário pressiona o salvamento botão?

Isso é chamado de um conflito de simultaneidade e NHibernate tem poucas maneiras de detectar um conflito. A entidade ToDoAction possui um < versão / > campo informa NHibernate que ele precisa executar verificações de simultaneidade otimista explicitamente. Para uma discussão completa sobre as opções de simultaneidade oferece NHibernate, consulte meu blog lançar na de ayende.com/Blog/archive/2009/04/15/nhibernate-mapping-concurrency.aspx .

Essencialmente, soluções de simultaneidade se enquadram em duas categorias amplas:

  • Controle de concorrência pessimista, requer que você mantenha bloqueios no banco de dados e manter a transação aberta por um longo período de tempo. Como discutido anteriormente, isso não é uma boa idéia em um aplicativo de área de trabalho.
  • Controle de simultaneidade otimista, que significa que você pode fechar a conexão de banco de dados durante o usuário “ think time. ” A maioria das opções que NHibernate tem a oferecer está no lado otimista, permitindo que várias estratégias detectar conflitos.

Porque o controle de concorrência pessimista acarreta um custo de desempenho pesado, geralmente não é aceitável. Isso, por sua vez, significa que deve favorecer o controle de simultaneidade otimista. Com concorrência otimista, tente salvar os dados normalmente, mas você está preparado para manipular o caso onde os dados foi alterados por outro usuário.

NHibernate será manifesto essa situação como um StaleObjectStateException durante a salvar ou confirmar processos. Seus aplicativos devem capturar a exceção e se comportam de acordo. Geralmente, isso significa que você precisa mostrar ao usuário, algum tipo de uma mensagem explicando que a entidade foi editada por outro usuário e o usuário precisa refazer suas alterações. Ocasionalmente, você precisa executar operações mais complexas, como oferecendo para mesclar as informações ou permitindo que o usuário decidir qual versão deseja manter.

Porque a primeira opção — exibindo uma mensagem e o usuário precisar refazer quaisquer alterações — é muito mais comum, eu vai mostrar como implementar a que NHibernate e discuta resumidamente como você pode implementar as soluções.

Um problema interessante que enfrentam imediatamente é uma exceção gerada da sessão significa que a sessão não é mais utilizável. Qualquer conflito de simultaneidade mostra no NHibernate como uma exceção. A única coisa que você pode fazer para uma sessão após ele acionou uma exceção é chamar Dispose nele; qualquer operação irá resultar em comportamento indefinido.

Vou para voltar à tela de edição e implementar o tratamento de simultaneidade há como um exemplo. Vou adicionar um botão criar conflitos de simultaneidade para a tela de edição, que executará o seguinte:

public void OnCreateConcurrencyConflict() {
  using(var session = SessionFactory.OpenSession())
  using(var tx = session.BeginTransaction()) {
    var anotherActionInstance = 
      session.Get<ToDoAction>(Model.Action.Id);
    anotherActionInstance.Title = 
      anotherActionInstance.Title + " -";
    tx.Commit();
  }
MessageBox.Show("Concurrency conflict created");
}

Isso cria uma nova sessão e modifica a propriedade título. Isso irá disparar um conflito de simultaneidade ao tentar salvar a entidade no formulário, porque a sessão no formulário não está ciente dessas alterações. Figura 7 mostra como eu estou tratamento que.

Eu simplesmente disposto o código que salva no banco de dados em um bloco catch de try e manipulada a exceção de estado obsoletos por informando o usuário que eu detectado um conflito de simultaneidade. Então eu substituir a sessão.

Figura 7. Manipulando conflitos de simultaneidade

public void OnSave() {
  bool successfulSave;
  try {
    using (var tx = Session.BeginTransaction()) {
      Session.Update(Model.Action);

      tx.Commit();
    }
    successfulSave = true;
  }
  catch (StaleObjectStateException) {
    successfulSave = false;
    MessageBox.Show(
      @"Another user already edited the action before you had a chance to do so. 
     The application will now reload the new data from the database, please retry your 
     changes and save again.");

    ReplaceSessionAfterError();
  }

  EventPublisher.Publish(new ActionUpdated {
    Id = Model.Action.Id
  }, this);

  if (successfulSave)
    View.Close();
}

Observe que estou sempre chamando ActionUpdated, mesmo se recebi um conflito de simultaneidade. Eis o porquê: Mesmo se recebi um conflito de simultaneidade, provavelmente o restante do aplicativo não sabe sobre ele e a entidade foi alterada no banco de dados, portanto, eu poderia também dar o restante do aplicativo a chance de mostrar o usuário novos valores.

Finalmente, apenas fechar o formulário se eu tiver sido bem-sucedida no salvamento ao banco de dados. Até agora, que não há nada muito, mas existe é a substituição de sessão e a entidade que ainda preciso considerar (de consulte do Figura 8).

Figura 8 Atualizando sessões e entidades

protected void ReplaceSessionAfterError() {
  if(session!=null) {
    session.Dispose();
    session = sessionFactory.OpenSession();
    ReplaceEntitiesLoadedByFaultedSession();
  }
  if(statelessSession!=null) {
    statelessSession.Dispose();
    statelessSession = sessionFactory.OpenStatelessSession();
  }
}

protected override void 
  ReplaceEntitiesLoadedByFaultedSession() {
  Initialize(Model.Action.Id);
}

Como você pode ver, eu substituir sessão ou sessão sem estado descartando-los e abrindo novos. No caso de uma sessão, peço também o apresentador para substituir todas as entidades que foram carregadas por sessão falha. NHibernate entidades estão intimamente associadas a sua sessão e quando a sessão se tornar inutilizável, é geralmente melhor substituir as entidades também. Não é necessário — as entidades não vamos parar de funcionar repentinamente — mas coisas como carregamento lento não funcionará mais. Eu gostaria de pagamento em vez o custo de substituir as entidades que tentar descobrir se pode ou não pode desviar o gráfico do objeto em determinados casos.

A implementação de substituição de entidade é feita chamando apenas o método Initialize nesse caso. Este é o mesmo método Initialize que discuti no caso de formulário de edição. Esse método apenas obtém a entidade do banco de dados e define para a propriedade do modelo — nada empolgante. Em cenários mais complexos, ele pode substituir várias instâncias de entidade são usadas em um único formulário.

Para a questão, a mesma abordagem mantém não apenas para conflitos de simultaneidade, mas também para qualquer erro que você pode obter de sessões do NHibernate. Depois de obter um erro, você deve substituir a sessão. E quando você substitui a sessão, você provavelmente devia recarregar quaisquer entidades carregado usando a sessão antiga em uma nova, apenas para ser seguro lado.

Gerenciamento de conflito

Último tópico que desejo tocar durante este artigo é as técnicas de gerenciamento de conflito de simultaneidade mais complexas. Há somente uma opção, basicamente: permitir que o usuário tomar uma decisão entre a versão no banco de dados e a versão que o usuário apenas modificado.

Figura 9 mostra o modelo de tela de mesclagem. Como você pode ver aqui são mostrando o usuário simplesmente ambas as opções e pedindo para escolher qual deles aceitará eles. Qualquer resolução de conflitos de simultaneidade alguma forma é baseada nessa idéia. Você talvez queira apresentar de maneira diferente, mas que é como ele funciona e podem extrapolar daqui.

UI for Managing Change Conflicts

Figura 9 do UI para gerenciar os conflitos de alteração

Na tela editar, altere a resolução de conflito para isso:

catch (StaleObjectStateException) {
  var mergeResult = 
    Presenters.ShowDialog<MergeResult?>(
    "Merge", Model.Action);
  successfulSave = mergeResult != null;

  ReplaceSessionAfterError();
}

Mostrar diálogo de mala direta e se o usuário fez uma decisão sobre a mesclagem, decidir que foi um salvamento bem-sucedido (que seria fechar o formulário de edição). Observe que passo ação editada no momento para a caixa de diálogo mesclagem, portanto, sabe o estado atual da entidade.

O apresentador de diálogo mesclagem é simples:

public void Initialize(ToDoAction userVersion) {
  using(var tx = Session.BeginTransaction()) {
    Model = new Model {
      UserVersion = userVersion,
      DatabaseVersion = 
        Session.Get<ToDoAction>(userVersion.Id),
        AllowEditing = new Observable<bool>(false)
    };  

    tx.Commit();
  }
}

Na inicialização, eu obter a versão atual do banco de dados e mostrar a ele e a versão que o usuário alterou. Se o usuário aceita a versão do banco de dados, não tenho muito a fazer, portanto, basta fechar o formulário:

public void OnAcceptDatabaseVersion() {
  // nothing to do
  Result = MergeResult.AcceptDatabaseVersion;
  View.Close();
}

Se o usuário deseja forçar sua própria versão, é apenas um pouco mais complicado:

public void OnForceUserVersion() {
  using(var tx = Session.BeginTransaction()) {
    //updating the object version to the current one
    Model.UserVersion.Version = 
      Model.DatabaseVersion.Version;
    Session.Merge(Model.UserVersion);
    tx.Commit();
  }
  Result = MergeResult.ForceDatabaseVersion; 
  View.Close();
}

Uso mesclar recursos do NHibernate para pegar todos os valores persistentes na versão do usuário e copiar para a instância da entidade dentro da sessão atual.Na verdade, mescla as duas instâncias, forçando os valores de usuário na parte superior do valor de banco de dados.

Isso é realmente seguro fazer mesmo com a sessão desapareceu e inativa, porque o contrato do método Merge garante que ele não tente atravessar preguiçosos associações carregadas.

Observe que antes eu tentar a mesclagem, defini a propriedade version do usuário à propriedade de versão do banco de dados.Isso é feito porque nesse caso, desejo substituir explicitamente que a versão.

Esse código não tenta manipular conflitos de simultaneidade recursiva (que é, obtendo um conflito de simultaneidade como resultado de resolvendo primeiro).Que é deixada como um exercício para você.

Apesar do número dos desafios descritos neste artigo, criando um aplicativo de desktop NHibernate não é mais difícil do que criando um aplicativo de NHibernate.E, em ambos os cenários, acredito que usar NHibernate facilitará sua vida mais fácil, o aplicativo mais robusto e o sistema geral alterar e trabalhar com.

Oren Eini* (que trabalha sob o pseudônimo Ayende Rahien) é um membro ativo de vários projetos de origem aberto (castelo entre eles e NHibernate) e é o fundador da muitos outros (Rhino Mocks, NHibernate Query Analyzer e Rhino Commons entre eles). Eini também é responsável pelo NHibernate Profiler ( nhprof.com de), um depurador visual para NHibernate. Você pode seguir trabalho do Eini em ayende.com/Blog de.*

Graças aos seguintes especialistas técnicos para revisão deste artigo: Howard Dierking