使用实体框架(或其他ORM?)管理几个几乎相同的客户端数据库
本文关键字:几个 数据库 客户端 其他 实体 ORM 管理 框架 | 更新日期: 2023-09-27 18:08:31
我正在制作一个ASP原型。. NET Web API,需要与几个几乎相同的数据库进行通信。我们的每个客户都有自己的数据库结构实例,但有些客户专门与他们拥有的其他系统集成。例如,在一个数据库中,Client
表可能有AbcID
列来引用另一个系统中的表,但其他数据库不会有这个列。除此之外,这两个表在名称和列上是相同的。列也可以有不同的长度,例如varchar(50)
而不是varchar(40)
。在一些数据库中,可能会有一个额外的表。我先把重点放在解决不同列的问题上。
我希望使用ORM来处理API的数据访问层,现在我正在尝试使用实体框架。我已经解决了如何从api调用动态连接到不同的数据库,但现在它们必须在结构上完全相同。
我尝试用数据库优先的方法设置双。edmx模型,但这会导致模型之间的类名冲突。因此,我尝试了代码优先,并提出了这个(这不起作用)。
DbContext扩展:在构造函数中,我检查正在访问哪个数据库,如果它是一个特殊的数据库,我将其标记为模型配置。
public partial class MK_DatabaseEntities : DbContext
{
private string _dbType = "dbTypeDefault";
public DbSet<Client> Client { get; set; }
public DbSet<Resource> Resource { get; set; }
public MK_DatabaseEntities(string _companycode)
: base(GetConnectionString(_companycode))
{
if(_companycode == "Foo")
this._dbType = "dbType1";
}
// Add model configurations
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Configurations
.Add(new ClientConfiguration(_dbType))
.Add(new ResourceConfiguration());
}
public static string GetConnectionString(string _companycode)
{
string _dbName = "MK_" + _companycode;
// Start out by creating the SQL Server connection string
SqlConnectionStringBuilder sqlBuilder = new SqlConnectionStringBuilder();
sqlBuilder.DataSource = Properties.Settings.Default.ServerName;
sqlBuilder.UserID = Properties.Settings.Default.ServerUserName;
sqlBuilder.Password = Properties.Settings.Default.ServerPassword;
// The name of the database on the server
sqlBuilder.InitialCatalog = _dbName;
sqlBuilder.IntegratedSecurity = false;
sqlBuilder.ApplicationName = "EntityFramework";
sqlBuilder.MultipleActiveResultSets = true;
string sbstr = sqlBuilder.ToString();
return sbstr;
}
}
ClientConfiguration: 在Client
的配置中,我在将属性映射到数据库列之前检查标志。
public class ClientConfiguration : EntityTypeConfiguration<Client>
{
public ClientConfiguration(string _dbType)
{
HasKey(k => k.Id);
Property(p => p.Id)
.HasColumnName("ID")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
if (_dbType == "dbType1")
{
Property(p => p.AbcId).HasColumnName("AbcID");
}
Property(p => p.FirstName).HasColumnName("FirstName");
Property(p => p.LastName).HasColumnName("LastName");
}
}
客户端类:这就是我的Client
类的样子,这里没有什么奇怪的。
public class Client : IIdentifiable
{
public int Id { get; set; }
public string AbcId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public interface IIdentifiable
{
int Id { get; }
}
备份解决方案是使用原始SQL查询来处理有问题的表,其余的使用ORM,但如果有一些我没有想到的方法来做到这一点,那将是非常棒的。现在我正在尝试实体框架,但我不反对尝试其他ORM,如果它可以做得更好。
Code First支持这种场景:
1)两个模型的通用实体:
public class Table1
{
public int Id { get; set; }
public string Name { get; set; }
}
2)表2的基本版本
public class Table2A
{
public int Id { get; set; }
public int Name2 { get; set; }
public Table1 Table1 { get; set; }
}
3)表2的"扩展"版本继承了版本A,并添加了一个额外的列
public class Table2B : Table2A
{
public int Fk { get; set; }
}
4)基本上下文,仅包括公共实体。请注意,这里有一个接受连接字符串的构造函数,因此没有无参数构造函数。这将强制继承上下文提供其特定的连接字符串。
public class CommonDbContext : DbContext
{
public CommonDbContext(string connectionString)
:base(connectionString)
{
}
public IDbSet<Table1> Tables1 { get; set; }
}
5)上下文A,继承公共上下文,增加Table2A
,忽略Table2B
public class DbContextA : CommonDbContext
{
public DbContextA() : base("SimilarA") { } // connection for A
public IDbSet<Table2A> Tables2A { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Ignore<Table2B>(); // Ignore Table B
}
}
上下文B,继承了common,包括
Table2B
DbContextB: CommonDbContext{public DbContextB():base("SimilarB") {}//B的连接公共IDbSet表2b{获取;设置;}}
使用此设置,您可以实例化DbContextA
或DbContextB
。一个优点是它们都继承了CommonDbContext
,所以无论具体实现是版本a还是版本B,您都可以使用这个基类的变量来访问公共实体。您只需要更改为具体类型来访问a或B的特定实体(本示例中的Table2A
或Table2B
)。
您可以使用工厂或DI或其他方式根据DB获取所需的上下文。例如,这可能是您的工厂实现:
public class CommonDbContextFactory
{
public static CommonDbContext GetDbContext(string contextVersion)
{
switch (contextVersion)
{
case "A":
return new DbContextA();
case "B":
return new DbContextB();
default:
throw new ArgumentException("Missing DbContext", "contextVersion");
}
}
}
注意:这是工作样例代码。你当然可以根据你的具体情况调整它。我想让它保持简单,以展示它是如何工作的。对于您的情况,您可能需要更改工厂实现,并在A和B上下文构造函数中公开连接字符串,并在工厂方法
中提供它。处理实体的不同类
处理每个DbContext
的不同实体最简单的方法是使用多态性和/或泛型。
如果使用多态性,则需要实现使用基类类型(作为参数和返回类型)的方法。这些形参和变量将保存基类或派生类(Table2A
或Table2B
)的实体。在这种情况下,每个上下文都将接收正确类型的实体,并且它将直接工作而不会出现问题。
问题是当你的应用是多层的,使用服务或是一个web应用。在这种情况下,当你使用基类的多态行为可能会丢失,你需要处理基类的实体。(例如,如果你让用户在web应用程序表单中编辑派生类的实体,表单只能处理基类的属性,当它被发送回来时,派生类的属性将丢失)在这种情况下,你需要智能地处理它(见下面的注释):
对于读取的目的,如果您有Table2B
,则可以直接强制转换为Table2A
。您可以实现Table2A
的功能并直接使用它。也就是说,你可以返回基类的集合或单个值(在许多情况下隐式强制转换就足够了)。别再担心了。
对于插入/更新,您必须采取额外的步骤,但这并不太难。您需要在您的上下文中实现接收/返回Table2A
参数的方法,或者在另一层,这取决于您的体系结构。例如,您可以使基本上下文抽象,并为此定义虚拟方法。(见下面的例子)。然后,您需要为每个特定情况制定正确的实现。
- 如果你收到一个
Table2A
,但需要在Table2B
中插入它,只需用AutoMapper
或ValueInjecter
将实体a映射到实体B,并使用默认值填充剩余的属性(注意AutoMapper
和EF动态代理:它不会工作)。 - 如果您收到
Table2A
并且需要更新Table2B
,只需从DB中读取现有实体并重复映射过程(对于这种情况,ValueInjecter
将比AutoMapper
更少麻烦)。
这是一个非常简单的例子,但你需要根据你的具体情况调整它:
在CommonDbContext
类中,为基类型声明虚方法,如下所示:
public virtual Table2A GetTable2AById(int id);
public virtual void InsertTable2A(Table2A table);
你也可以使用泛型接口/方法,而不是抽象类/虚拟方法,像这样:
public T GetTable2AById<T>(int id)
{
// The implementation
}
在这种情况下,您应该向T
类型添加必要的约束,如where T: Table2A
或您需要的约束(class
new()
)。
注在这种情况下并不确切地说多态性丢失了,因为您可以使用WCF或Web API制作多态Web服务,使您的UI适应实体的实际类(每种情况都有模板)等等。这取决于你需要什么或者想要实现什么。
我也经历过。
In all serious: dump EF;它会带来很多痛苦和折磨,没有任何好处。
你最终要做的(戴上我的算命帽)是,你将去掉所有基于ef的代码,创建一个抽象对象模型,然后编写一系列后端,将所有不同的数据库结构来回映射到这个干净的抽象对象模型。您将使用原始SQL或轻量级的东西,如Dapper或BLToolkit。