Divisão de entidade quando a coluna-chave tem nomes diferentes?
-
28-10-2019 - |
Pergunta
Estou usando o Entity Framework 4.3.1 Code-First e preciso dividir uma entidade entre duas tabelas. As tabelas têm uma chave primária compartilhada e é 1 para 1, mas as colunas não são nomeadas da mesma forma em cada tabela.
Não controlo o layout dos dados, nem posso solicitar alterações.
Então, por exemplo, as tabelas SQL podem ser
E esta seria minha entidade ...
public class MyEntity
{
public int Id {get; set;}
public string Name {get;set}
public string FromAnotherTable {get;set;}
}
E aqui está o mapeamento que tenho.
public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
public MyEntityMapping()
{
this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
this.Property(e => e.Name).HasColumnName("MyDatabaseName");
this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
{
m.Properties(e =>
{
e.Id,
e.Name
});
m.ToTable("MainTable");
});
this.Map(m =>
{
m.Properties(e =>
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
}
Como a chave compartilhada entre eles tem um nome de coluna diferente, não tenho certeza de como mapeá-la. Este mapeamento irá compilar, mas falhará em tempo de execução porque EF emite SQL procurando pela coluna "ThePrimaryKeyId" na tabela "ExtendedTable", que não existe.
EDITAR Para esclarecer, o que defini acima pode (e funciona) se o PK na "ExtendedTable" seguir as convenções de nomenclatura. Mas isso não acontece e eu não posso mudar o esquema.
Basicamente, o que eu preciso que o EF emita é uma instrução SQL como
SELECT
[e1].*, /*yes, wildcards are bad. doing it here for brevity*/
[e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2] /*Could be left join, don't care. */
ON [e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]
Mas a única coisa que parece querer emitir é
SELECT
[e1].*,
[e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2]
ON [e1].[ThePrimaryKeyId] = [e2].[ThePrimaryKeyId] /* this column doesn't exist */
Editar Tentei a abordagem 1 para 1 novamente por sugestão da NSGaga. Não funcionou, mas aqui estão os resultados. Entidades
public class MyEntity
{
public int Id { get; set; }
public int Name { get; set; }
public virtual ExtEntity ExtendedProperties { get; set; }
}
public class ExtEntity
{
public int Id { get; set; }
public string AnotherTableColumn { get; set; }
public virtual MyEntity MainEntry { get; set; }
}
Aqui estão as classes de mapeamento
public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
public MyEntityMapping()
{
this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
this.Property(e => e.Name).HasColumnName("MyDatabaseName");
this.ToTable("MainTable");
this.HasKey(e => e.Id);
this.HasRequired(e => e.ExtendedProperties).WithRequiredPrincipal(f => f.MainEntry);
}
}
public class ExtEntityMapping : EntityTypeConfiguration<ExtEntity>
{
public ExtEntityMapping()
{
this.Property(e => e.Id).HasColumnName("NotTheSameName");
this.Property(e => e.AnotherTableColumn).HasColumnName("AnotherTableColumn");
this.ToTable("ExtendedTable");
this.HasKey(e => e.Id);
this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties);
}
}
Esta configuração capta a mensagem
"Column or attribute 'MyEntity_ThePrimaryKeyId' is not defined in 'ExtendedTable'"
Alterando a linha final do mapa para
this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties).Map(m => M.MapKey("NotTheSameName"));
Retorna esta mensagem
"Each property name in a type must be unique. property name 'NotTheSameName' was already defined."
Alterar a chave mapeada para usar a coluna da tabela pai, MapKey("ThePrimaryKeyId")
. retorna esta mensagem
"Column or attribute 'ThePrimaryKeyId' is not defined in 'ExtendedTable'"
Remover a propriedade Id
da classe ExtEntity
gera um erro porque a entidade não tem uma chave definida.
Solução
Não consigo encontrar nada que afirme especificamente que o nome da coluna deve ser o mesmo nas duas tabelas;mas também não consigo encontrar nada que diga que não, ou explique como você mapearia esse cenário.Todos os exemplos que encontro têm a chave com o mesmo nome nas duas tabelas.Parece-me que isso é uma lacuna no design do DbContext.
Outras dicas
Estou trabalhando nesse mesmo problema há alguns dias, o que finalmente fiz foi definir o nome da coluna do campo Id dentro do contexto do fragmento de mapeamento.Desta forma, você pode dar ao Id (ou à chave estrangeira dependente do Id) um nome diferente do Id da tabela principal.
this.Map(m =>
{
m.Property(p => p.Id).HasColumnName("NotTheSameName");
m.Properties(e =>
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
Se você executasse e depurasse isso, descobriria que teria algo parecido com o que você deseja:
[e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]
Mova HasColumnName para dentro do mapeamento:
this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
{
m.Properties(e => new
{
e.Id,
e.Name
});
m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
m.Property(e => e.Name).HasColumnName("MyDatabaseName");
m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
m.ToTable("MainTable");
});
this.Map(m =>
{
m.Properties(e => new
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
}
Nenhum Visual Studio aqui, mas tente isso com a abordagem 1 para 1:
this.HasRequired (e=> e.ExtendedProperties) .HasConstraint ((e, m)=> e.Id== m.Id);
Atualização:
Aqui estão alguns links que podem ajudar (não foi possível encontrar um link de referência real)
Comopara declarar um para um relacionamento usando Entity Framework 4 Code First (POCO)
Entity Framework 4 CTP4 Codifique primeiro: como trabalhar com nomes de chave primária e estrangeira não convencionais
E apenas para fornecer (como prometi) um mapeamento 1 para 1 (duas entidades, duas tabelas), pelo que vale a pena.
Aqui está o que funciona para mim e, no seu caso, deve ...
public class MainTable
{
public int ThePrimaryKeyId { get; set; }
public string Name { get; set; }
}
public class ExtendedTable
{
public int NotTheSameNameID { get; set; }
public string AnotherTableColumn { get; set; }
public MainTable MainEntry { get; set; }
}
public class MainDbContext : DbContext
{
public DbSet<MainTable> MainEntries { get; set; }
public DbSet<ExtendedTable> ExtendedEntries { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<MainTable>()
.HasKey(x => new { x.ThePrimaryKeyId });
modelBuilder.Entity<ExtendedTable>()
.HasKey(x => new { x.NotTheSameNameID });
// Extended To Main 1 on 1
modelBuilder.Entity<ExtendedTable>()
.HasRequired(i => i.MainEntry)
.WithRequiredDependent();
}
}
... e um código de teste algo como ...
using (var db = new UserDbContext())
{
foreach (var userid in Enumerable.Range(1, 100))
{
var main = new MainTable { Name = "Main" + userid };
db.MainEntries.Add(main);
var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, MainEntry = main };
db.ExtendedEntries.Add(extended);
}
int recordsAffected = db.SaveChanges();
foreach (var main in db.MainEntries)
Console.WriteLine("{0}, {1}", main.Name, main.ThePrimaryKeyId);
foreach (var extended in db.ExtendedEntries)
Console.WriteLine("{0}, {1}, {2}, {3}", extended.AnotherTableColumn, extended.NotTheSameNameID, extended.MainEntry.Name, extended.MainEntry.ThePrimaryKeyId);
}
Isso cria o seguinte script SQL, tabelas ...
CREATE TABLE [MainTables] (
[ThePrimaryKeyId] [int] NOT NULL IDENTITY,
[Name] [nvarchar](4000),
CONSTRAINT [PK_MainTables] PRIMARY KEY ([ThePrimaryKeyId])
)
CREATE TABLE [ExtendedTables] (
[NotTheSameNameID] [int] NOT NULL,
[AnotherTableColumn] [nvarchar](4000),
CONSTRAINT [PK_ExtendedTables] PRIMARY KEY ([NotTheSameNameID])
)
CREATE INDEX [IX_NotTheSameNameID] ON [ExtendedTables]([NotTheSameNameID])
ALTER TABLE [ExtendedTables] ADD CONSTRAINT [FK_ExtendedTables_MainTables_NotTheSameNameID] FOREIGN KEY ([NotTheSameNameID]) REFERENCES [MainTables] ([ThePrimaryKeyId])
E uma observação, conforme nossa discussão acima ...
Esta não é a 'divisão' - mas
(a) O código primeiro IMO não permite nada parecido (eu tentei isso primeiro e também modifiquei as migrações manualmente, mas é 'internamente' tudo baseado nos nomes de coluna esperados sendo os mesmos e parece não haver maneira de contornar isso, pois pelo menos esta versão do EF.
(b) estrutura da tabela - as tabelas podem ser feitas para parecer exatamente o que você precisa (como eu disse antes, usei para relacionar as tabelas de associação aspnet existentes (que não pude alterar) em minha tabela de usuário que tem um próprio usuário -id apontando para a tabela externa / aspnet e id.
É verdade, você não pode fazer isso usando uma classe de modelo C # - mas o lado C # é muito mais flexível e se você pode controlar o C # isso deve dar o mesmo efeito, pelo menos na minha opinião (como no teste, você pode acessá-lo sempre por meio da entidade estendida, tanto as colunas estendidas quanto as principais e elas são sempre correspondidas 1 a 1 e permanecem 'em sincronia'.
Espero que isso ajude algum
NOTA: você não precisa se preocupar com o fk id etc. - apenas sempre acesse e adicione a entrada Principal via MainEntry e id-s ficará bem.
EDITAR:
Você também pode fazer o seguinte, para ter a aparência de ter que lidar com apenas uma classe (ou seja, uma espécie de divisão)
public class ExtendedTable
{
public int NotTheSameNameID { get; set; }
public string AnotherTableColumn { get; set; }
public string Name { get { return MainEntry.Name; } set { MainEntry.Name = value; } }
// public int MainID { get { return MainEntry.ThePrimaryKeyId; } set { MainEntry.ThePrimaryKeyId = value; } }
internal MainTable MainEntry { get; set; }
public ExtendedTable()
{
this.MainEntry = new MainTable();
}
}
... e use-o assim ...
var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, Name = "Main" + userid };
... também você pode reverter a direção do fk executando o WithRequiredPrincipal
em vez de dependente.
(também todas as referências devem ser sem 'virtuais' se você tiver requerido um-para-um)
(e MainTable pode ser feito 'interno' como está aqui, então não é visível de fora - não pode ser aninhado porque o EF não permite - é tratado como NotMapped)
... bem, isso é o melhor que pude fazer :)
Parece que foi corrigido no Entity Framework 6. Veja este problema http://entityframework.codeplex.com/ workitem / 388
Eu gostaria de sugerir o uso de algumas anotações de dados como esta:
MainTable
---------
MainTableId
DatabaseName
ExtendedTable
----------
NotTheSameName
AnotherColumn
public class MainTable
{
[Key]
public int MainTableId { get; set; }
public string DatabaseName { get; set; }
[InverseProperty("MainTable")]
public virtual ExtendedTable ExtendedTable { get; set; }
}
public class ExtendedTable
{
[Key]
public int NotTheSameName { get; set; }
public string AnotherColumn { get; set; }
[ForeignKey("NotTheSameName")]
public virtual MainTable MainTable { get; set; }
}
Enfrentei esse problema e resolvi adicionando atributo de coluna para corresponder aos nomes de coluna.
[Key]
[Column("Id")]
public int GroupId { get; set; }