No talk que participei do Canal DotNet sobre NHibernate Avançado falamos sobre o mapeamento de heranças e prometi escrever um post abordando o assunto. Então hoje falaremos sobre como mapear herança com o Fluent NHibernate. Pra quem ainda não leu, sugiro primeiro ler o meu artigo sobre mapeamento de classes e teste de mapeamento.

Para mapear herança, podemos seguir 2 abordagens diferentes. Podemos criar herança utilizando tabela única ou tabela por subclasse.

Mapeamento utilizando tabela única

A utilização de tabela única é feita com uma única tabela para a classe pai e para as classes filhas. A vantagem é que não se faz o uso de Joins para obter uma instância das subclasses, porém, pode acabar ficando com uma tabela com muitas colunas na sua aplicação desnecessariamente.

Para o exemplo, criei as seguintes classes:

public class Pessoa
{
    public virtual int Id { get; set; }
    public virtual string Nome { get; set; }
    public virtual string Endereco { get; set; }
    public virtual string Numero { get; set; }
    public virtual string Cidade { get; set; }
    public virtual string Pais { get; set; }
    public virtual string UF { get; set; }
    public virtual string Telefone { get; set; }
}

public class PessoaFisica : Pessoa
{
    public virtual DateTime DataNascimento { get; set; }
    public virtual string CPF { get; set; }
}

public class PessoaJuridica : Pessoa
{
    public virtual string RazaoSocial { get; set; }
    public virtual string CNPJ { get; set; }
}

No nosso exemplo, teremos uma classe de Pessoa e suas derivações: PessoaFisica e PessoaJuridica. Para armazenar as classes no banco, utilizei a tabela Pessoa.

CREATE TABLE [dbo].[Pessoa](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Cidade] [varchar](50) NOT NULL,
	[Nome] [varchar](50) NOT NULL,
	[Numero] [varchar](10) NOT NULL,
	[Pais] [varchar](50) NOT NULL,
	[Endereco] [varchar](255) NOT NULL,
	[Telefone] [varchar](10) NOT NULL,
	[UF] [varchar](2) NOT NULL,
	[Tipo] [varchar](50) NOT NULL,
	[CNPJ] [varchar](14) NULL,
	[RazaoSocial] [varchar](50) NULL,
	[CPF] [varchar](11) NULL,
	[DataNascimento] [datetime] NULL,
 CONSTRAINT [PK_Pessoa] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))

E agora, vamos conferir as classes de mapeamento:

public class PessoaMap : ClassMap<Pessoa>
{
    public PessoaMap()
    {
        Table("Pessoa");
        Id(x => x.Id, "[Id]").GeneratedBy.Identity();
        Map(x => x.Cidade, "[Cidade]").Not.Nullable().Length(50);
        Map(x => x.Nome, "[Nome]").Not.Nullable().Length(50);
        Map(x => x.Numero, "[Numero]").Not.Nullable().Length(10);
        Map(x => x.Pais, "[Pais]").Not.Nullable().Length(50);
        Map(x => x.Endereco, "[Endereco]").Not.Nullable().Length(255);
        Map(x => x.Telefone, "[Telefone]").Not.Nullable().Length(10);
        Map(x => x.UF, "[UF]").Not.Nullable().Length(2);
        DiscriminateSubClassesOnColumn("Tipo").Not.Nullable().Length(50);
    }
}

public class PessoaFisicaMap : SubclassMap<PessoaFisica>
{
    public PessoaFisicaMap()
    {
        Map(x => x.CPF, "[CPF]").Not.Nullable().Length(11);
        Map(x => x.DataNascimento, "[DataNascimento]").Not.Nullable();
    }
}

public class PessoaJuridicaMap : SubclassMap<PessoaJuridica>
{
    public PessoaJuridicaMap()
    {
        Map(x => x.CNPJ, "[CNPJ]").Not.Nullable().Length(14);
        Map(x => x.RazaoSocial, "[RazaoSocial]").Not.Nullable().Length(50);
    }
}

Como podem ver, a classe PessoaMap como de costume herda da classe ClassMap. Porém, PessoaFisica e PessoaJuridica herdam de SubclassMap, sinalizando que se tratam de classes herdadas.

Um detalhe importante, como estamos armazenando várias entidades em uma única tabela, é preciso criar uma coluna para armazenar o tipo do registro. O mapeamento é feito utilizando o DiscriminateSubClassesOnColumn, essa coluna deve ser do tipo varchar.

Agora é a hora de executar o projeto de exemplo e analizar o código SQL executado:

static void Main(string[] args)
{
    ISessionFactory sessionFactory = Fluently.Configure()
                    .Database(MsSqlConfiguration.MsSql2012.ConnectionString("Data Source=localhost;Initial Catalog=HerancaNHibernate;User ID=sa;Password=NHibernate@123")
                    .ShowSql().FormatSql())
                    .Mappings(m =>
                        m.FluentMappings
                        .AddFromAssemblyOf<PessoaMap>())
                    .BuildSessionFactory();

    ISession session = sessionFactory.OpenSession();

    PessoaFisica pf = new PessoaFisica()
    {
        Cidade = "Rio de Janeiro",
        CPF = "45042960716",
        DataNascimento = DateTime.Now.AddYears(-20),
        Endereco = "Rua de Teste",
        Nome = "Fulano da Silva",
        Numero = "3434",
        Pais = "Brasil",
        Telefone = "99999-9999",
        UF = "RJ"
    };

    session.SaveOrUpdate(pf);

    PessoaJuridica pj = new PessoaJuridica()
    {
        Cidade = "Rio de Janeiro",
        CNPJ = "00000000000",
        RazaoSocial = "Nome Razao Social",
        Endereco = "Rua de Teste Empresa",
        Nome = "Empresa Teste",
        Numero = "0000",
        Pais = "Brasil",
        Telefone = "99999-9999",
        UF = "RJ"
    };

    session.SaveOrUpdate(pj);
    session.Flush();

    var pessoas = session.Query<Pessoa>().Count();
    System.Console.WriteLine($"Quantidade de entidades do tipo Pessoa: {pessoas}");

    var pessoasFisicas = session.Query<PessoaFisica>().Count();
    System.Console.WriteLine($"Quantidade de entidades do tipo PessoaFisica: {pessoasFisicas}");

    var pessoasJuridicas = session.Query<PessoaJuridica>().Count();
    System.Console.WriteLine($"Quantidade de entidades do tipo PessoaJuridica: {pessoasJuridicas}");
}

Como resultado temos a seguinte saída no console:

NHibernate:
    INSERT
    INTO
        Pessoa
        ([Cidade], [Nome], [Numero], [Pais], [Endereco], [Telefone], [UF], [CPF], [DataNascimento], Tipo)
    VALUES
        (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, 'PessoaFisica');
    select
        SCOPE_IDENTITY();
    @p0 = 'Rio de Janeiro' [Type: String (4000:0:0)],
    @p1 = 'Fulano da Silva' [Type: String (4000:0:0)],
    @p2 = '3434' [Type: String (4000:0:0)],
    @p3 = 'Brasil' [Type: String (4000:0:0)],
    @p4 = 'Rua de Teste' [Type: String (4000:0:0)],
    @p5 = '99999-9999' [Type: String (4000:0:0)],
    @p6 = 'RJ' [Type: String (4000:0:0)],
    @p7 = '45042960716' [Type: String (4000:0:0)],
    @p8 = 1999-03-31T11:54:01.5098998-03:00 [Type: DateTime2 (8:0:0)]
NHibernate:
    INSERT
    INTO
        Pessoa
        ([Cidade], [Nome], [Numero], [Pais], [Endereco], [Telefone], [UF], [CNPJ], [RazaoSocial], Tipo)
    VALUES
        (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, 'PessoaJuridica');
    select
        SCOPE_IDENTITY();
    @p0 = 'Rio de Janeiro' [Type: String (4000:0:0)],
    @p1 = 'Empresa Teste' [Type: String (4000:0:0)],
    @p2 = '0000' [Type: String (4000:0:0)],
    @p3 = 'Brasil' [Type: String (4000:0:0)],
    @p4 = 'Rua de Teste Empresa' [Type: String (4000:0:0)],
    @p5 = '99999-9999' [Type: String (4000:0:0)],
    @p6 = 'RJ' [Type: String (4000:0:0)],
    @p7 = '00000000000' [Type: String (4000:0:0)],
    @p8 = 'Nome Razao Social' [Type: String (4000:0:0)]
NHibernate:
    select
        cast(count(*) as INT) as col_0_0_
    from
        Pessoa pessoa0_
Quantidade de entidades do tipo Pessoa: 2
NHibernate:
    select
        cast(count(*) as INT) as col_0_0_
    from
        Pessoa pessoafisi0_
    where
        pessoafisi0_.Tipo='PessoaFisica'
Quantidade de entidades do tipo PessoaFisica: 1
NHibernate:
    select
        cast(count(*) as INT) as col_0_0_
    from
        Pessoa pessoajuri0_
    where
        pessoajuri0_.Tipo='PessoaJuridica'
Quantidade de entidades do tipo PessoaJuridica: 1

Podemos observar, a coluna tipo sendo utilizada para obter as classes derivadas PessoaFisica e PessoaJuridica.

Mapeamento utilizando tabela por subclasse

A utilização de tabela por subclasse é feita com uma tabela para a classe pai e uma tabela para cada classe filha. Como vantagem teremos tabelas menores, porém, cada busca por um registro filho, terá join entre tabelas.

No exemplo, utilizarei as mesmas classes Pessoa, PessoaFisica e PessoaJuridica que utilizei no exemplo de tabela única. As alterações são feitas nas tabelas do banco e no mapeamento.

Agora teremos uma tabela para cada classe e uma para cada subclasse:

 
CREATE TABLE [dbo].[Pessoa](
	[PessoaId] [int] IDENTITY(1,1) NOT NULL,
	[Cidade] [varchar](50) NOT NULL,
	[Nome] [varchar](50) NOT NULL,
	[Numero] [varchar](10) NOT NULL,
	[Pais] [varchar](50) NOT NULL,
	[Endereco] [varchar](255) NOT NULL,
	[Telefone] [varchar](10) NOT NULL,
	[UF] [varchar](2) NOT NULL,
 CONSTRAINT [PK_Pessoa] PRIMARY KEY CLUSTERED 
(
	[PessoaId] ASC
))

CREATE TABLE [dbo].[PessoaFisica](
	[PessoaFisicaId] [int] NOT NULL,
	[DataNascimento] [datetime] NOT NULL,
	[CPF] [varchar](11) NOT NULL,
 CONSTRAINT [PK_PessoaFisica] PRIMARY KEY CLUSTERED 
(
	[PessoaFisicaId] ASC
))

CREATE TABLE [dbo].[PessoaJuridica](
	[PessoaJuridicaId] [int] NOT NULL,
	[CNPJ] [varchar](14) NOT NULL,
	[RazaoSocial] [varchar](50) NOT NULL,
 CONSTRAINT [PK_PessoaJuridica] PRIMARY KEY CLUSTERED 
(
	[PessoaJuridicaId] ASC
))

No mapeamento, as classes PessoaFisica e PessoaJuridica continuam herdando de SubclassMap. Entretanto, diferente do mapeamento com tabela única, eu preciso mapear a tabela referente a essa classe e a sua chave estrangeira. No caso do mapeamento tabela por subclasse, eu não utilizo uma coluna de tipo para identificar as subclasses.

public PessoaMap()
{
    Table("[Pessoa]");
    Id(x => x.Id, "[PessoaId]").GeneratedBy.Identity();
    Map(x => x.Cidade, "[Cidade]").Not.Nullable().Length(50);
    Map(x => x.Nome, "[Nome]").Not.Nullable().Length(50);
    Map(x => x.Numero, "[Numero]").Not.Nullable().Length(10);
    Map(x => x.Pais, "[Pais]").Not.Nullable().Length(50);
    Map(x => x.Endereco, "[Endereco]").Not.Nullable().Length(255);
    Map(x => x.Telefone, "[Telefone]").Not.Nullable().Length(10);
    Map(x => x.UF, "[UF]").Not.Nullable().Length(2);         
}

public class PessoaFisicaMap : SubclassMap<PessoaFisica>
{
    public PessoaFisicaMap()
    {
        Table("[PessoaFisica]");
        KeyColumn("[PessoaFisicaId]");
        Map(c => c.CPF, "[CPF]").Not.Nullable().Length(11);
        Map(c => c.DataNascimento, "[DataNascimento]").Not.Nullable();
    }
}

public class PessoaJuridicaMap : SubclassMap<PessoaJuridica>
{
    public PessoaJuridicaMap()
    {
        Table("[PessoaJuridica]");
        KeyColumn("[PessoaJuridicaId]");
        Map(c => c.CNPJ, "[CNPJ]").Not.Nullable().Length(14);
        Map(c => c.RazaoSocial, "[RazaoSocial]").Not.Nullable().Length(50);
    }
}

Agora, executando o mesmo exemplo, teremos a seguinte saída no console:

NHibernate:
INSERT
INTO
[
Pessoa] (
[Cidade], [Nome], [Numero], [Pais], [Endereco], [Telefone], [UF]
)
VALUES
(@p0, @p1, @p2, @p3, @p4, @p5, @p6);
select
SCOPE_IDENTITY();
@p0 = ‘Rio de Janeiro’ [Type: String (4000:0:0)],
@p1 = ‘Fulano da Silva’ [Type: String (4000:0:0)],
@p2 = ‘3434’ [Type: String (4000:0:0)],
@p3 = ‘Brasil’ [Type: String (4000:0:0)],
@p4 = ‘Rua de Teste’ [Type: String (4000:0:0)],
@p5 = ‘99999-9999’ [Type: String (4000:0:0)],
@p6 = ‘RJ’ [Type: String (4000:0:0)]
NHibernate:
INSERT
INTO
[
PessoaFisica] (
[CPF], [DataNascimento], [PessoaFisicaId]
)
VALUES
(@p0, @p1, @p2);
@p0 = ‘45042960716’ [Type: String (4000:0:0)], @p1 = 1999-03-31T12:50:34.6842203-03:00 [Type: DateTime2 (8:0:0)], @p2 = 5 [Type: Int32 (0:0:0)]
NHibernate:
INSERT
INTO
[
Pessoa] (
[Cidade], [Nome], [Numero], [Pais], [Endereco], [Telefone], [UF]
)
VALUES
(@p0, @p1, @p2, @p3, @p4, @p5, @p6);
select
SCOPE_IDENTITY();
@p0 = ‘Rio de Janeiro’ [Type: String (4000:0:0)],
@p1 = ‘Empresa Teste’ [Type: String (4000:0:0)],
@p2 = ‘0000’ [Type: String (4000:0:0)],
@p3 = ‘Brasil’ [Type: String (4000:0:0)],
@p4 = ‘Rua de Teste Empresa’ [Type: String (4000:0:0)],
@p5 = ‘99999-9999’ [Type: String (4000:0:0)],
@p6 = ‘RJ’ [Type: String (4000:0:0)]
NHibernate:
INSERT
INTO
[
PessoaJuridica] (
[CNPJ], [RazaoSocial], [PessoaJuridicaId]
)
VALUES
(@p0, @p1, @p2);
@p0 = ‘00000000000’ [Type: String (4000:0:0)], @p1 = ‘Nome Razao Social’ [Type: String (4000:0:0)], @p2 = 6 [Type: Int32 (0:0:0)]
NHibernate:
select
cast(count() as INT) as col_0_0_ from [Pessoa] pessoa0_ Quantidade de entidades do tipo Pessoa: 2 NHibernate: select cast(count() as INT) as col_0_0_
from
[PessoaFisica] pessoafisi0_
inner join
[Pessoa] pessoafisi0_1_
on pessoafisi0_.[PessoaFisicaId]=pessoafisi0_1_.[PessoaId]
Quantidade de entidades do tipo PessoaFisica: 1
NHibernate:
select
cast(count(*) as INT) as col_0_0_
from
[PessoaJuridica] pessoajuri0_
inner join
[Pessoa] pessoajuri0_1_
on pessoajuri0_.[PessoaJuridicaId]=pessoajuri0_1_.[PessoaId]
Quantidade de entidades do tipo PessoaJuridica: 1

Agora quando inserimos uma PessoaFisica, o NHibernate insere um registro na tabela Pessoa e outro na Tabela PessoaFisica. O mesmo acontece com PessoaJuridica.

Analisando a busca, verificamos que é feito um join no select quando buscamos PessoaFisica ou PessoaJuridica

Com isso abordamos as duas maneiras de mapear herança com o Fluent NHibernate e que cada uma possui suas vantagens e desvantagens para implementação. Lembrando que o exemplo se encontra no meu Github.

Qualquer dúvida ou sugestão, basta entrarem em contato comigo. Até a próxima !