动态更改模型 - 数据库优先

8
我的数据库是使用EF 6.0的Data-First方法连接MS-SQL。我正在与数百个数据库同步(表结构在所有数据库上几乎相同),必要时动态更改连接字符串。
问题在于,某些数据库的结构与其他数据库略有不同。在所有数据库上,我都有一个名为X的表,该表具有一个名为Y的列,Y可以是位或字节。

sql tables shown

EF生成了一个基于数据库的模型类,其中列Y被定义为字节。因此,在查询时,它显然会抛出异常。

表'X'上的'Y'属性无法设置为System.Boolean值。您必须将该值设置为System.Byte。

exception image

在数据库优先方法中,是否有一种方式可以动态更改模型以解决此问题?或者在将返回值分配给模型之前,将其转换为字节以防止异常?

从数据库的角度来看,你只剩下一个选择,那就是更新模式。而从查询的角度来看,你可以根据模型属性/列的类型动态构建表达式。 - Nkosi
简单的想法:你能将其建模为对象或创建一个自己的类来处理位和字节输入吗?考虑到现有的可能性,可以使用隐式转换将double分配给int。 - Chrᴉz remembers Monica
@GertArnold,5天后,非常欢迎您提出任何建议。 - Stavm
5个回答

6
在数据库优先模式下,有一种方法可以完成此操作。简而言之,在配置文件中创建两组映射和模型文件,并选择其中的一组。
模型文件
当创建 EDMX 时,EF 会创建三个文件:
- 存储模型(*.ssdl)。 - 类(或概念)模型(*csdl)。 - 这两个模型之间的映射 (*msl)。
这些文件被嵌入为资源文件在编译后的程序集中,通常您不需要了解它们的存在。在运行时,EF 将从程序集中加载这些文件,由配置文件中连接字符串的资源路径进行指示,通常看起来像...
metadata=res://*/...

可以将另一组资源文件嵌入程序集中,并相应地修改连接字符串,但需要几个步骤才能实现。

为简洁起见,我将“映射和模型文件”称为“模型文件”。

添加两组模型文件

第1步 - 创建第一组文件

创建第一组文件就是创建一个EDMX。我使用了一个非常简单的数据库表:

CREATE TABLE [dbo].[Person](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
    [IsActive] [bit] NOT NULL,
    CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED ([Id] ASC))
ALTER TABLE [dbo].[Person] ADD  CONSTRAINT [DF_Person_IsActive]  DEFAULT ((1)) FOR [IsActive]

在一个简单的C#控制台应用程序中,我基于这个表创建了一个EDMX。
第2步-添加部分类
在我的情况下,只创建了一个Person类:
public partial class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
}

在EF中,属性IsActive必须映射到bit数据库字段,因此无法将其简单地映射到byte(或tinyint)字段,正如您已经发现的那样。我们必须添加第二个属性来支持字节字段:
partial class Person
{
    public byte IsActiveByte { get; set; }
}

主要的挑战在于如何将这两个属性中的任意一个映射到数据库中的一个字段,具体取决于其数据类型。
第三步 - 复制并修改第二组
现在,第一组的模型文件已经被嵌入到程序集中。我们想将它们作为常规文件提供,以便复制和修改。这可以通过将"元数据工件处理"设置从默认值(嵌入到输出程序集)临时更改为"复制到输出目录"来完成。现在构建项目,并在bin/Debug文件夹中找到这三个文件。
将"元数据工件处理"设置恢复为默认值,将文件移动到项目的根目录,并将它们复制到第二组。我最终拥有了这些文件,其中"BitModel"是原始文件。
BitModel.csdl
BitModel.msl
BitModel.ssdl
ByteModel.csdl
ByteModel.msl
ByteModel.ssdl

为了使ByteModel文件支持Person.IsActiveByte属性,我进行了以下更改(原始行/编辑后的行):

  • csdl:

    <Property Name="IsActive" Type="Boolean" Nullable="false" />
    <Property Name="IsActiveByte" Type="Byte" Nullable="false" />
    
  • ssdl:

    <Property Name="IsActive" Type="bit" Nullable="false" />
    <Property Name="IsActive" Type="tinyint" Nullable="false" />
    
  • msl:

    <ScalarProperty Name="IsActive" ColumnName="IsActive" />
    <ScalarProperty Name="IsActiveByte" ColumnName="IsActive" />
    

现在可以删除BitModel文件。

第四步 - 将第二个集合作为资源嵌入

下一步是将ByteModel文件添加到项目中,并在它们的属性中将“Build Action”设置为“嵌入式资源”,然后重新构建项目。

这些文件的嵌入方式与EF最初的方式稍有不同。在反汇编器中检查.exe文件会显示它们的资源名称为<namespace>.<filename>,在我的情况下是: BitOrBye.ByteModel.csdl等。

第五步 - 添加连接字符串

EF向项目添加了一个类似于以下内容的连接字符串...

<add name="DbBitContext" 
    connectionString="metadata=res://*/BitModel.csdl
                              |res://*/BitModel.ssdl
                              |res://*/BitModel.msl;
    provider=System.Data.SqlClient;
    provider connection string=&quot;data source=.\sql2016;initial catalog=DbBit;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework&quot;"
    providerName="System.Data.EntityClient" />

我复制了这个连接字符串并注释掉了原来的那个。在复制的连接字符串中,我修改了资源路径:

<add name="DbBitContext" 
    connectionString="metadata=res://*/BitOrByte.ByteModel.csdl
                              |res://*/BitOrByte.ByteModel.ssdl
                              |res://*/BitOrByte.ByteModel.msl;
    ... />

现在,装配件已准备好连接到一个数据库,其中 Person.IsActive 字段是一个 tinyint。属性 Person.IsActive 不再是映射属性,Person.IsActiveByte 是。

输入之前的连接字符串和上下文映射到 bit 字段,所以现在可以使用连接字符串来确定支持哪种类型的数据库,"BitModel" 或 "ByteModel"。

限制

在 LINQ-to-Entities 查询中,只能访问映射属性。例如,像这样的查询...

context.People.Where(p => p.Id > 10).Select(p => p.Name).ToList()

...是可以的。但是当"BitModel"被激活时,像这样的查询...

context.People.Where(p => p.IsActiveByte == 1).Select(p => p.Name).ToList()

...将会抛出臭名昭著的The specified type member 'IsActiveByte' is not supported in LINQ to Entities异常。

当然,您已经有了这个限制。您可能希望向类中添加未映射的属性,将字节和位属性的值传递到一个属性中,在应用程序代码中使用该属性。

一种可能的解决方法是使用EntityFramework.DynamicFilters。这个小宝石可以让您在上下文中定义全局过滤器,可以打开和关闭。因此,可以定义两个全局过滤器...

modelBuilder.Filter("IsActiveBit", (Person p) => p.IsActive, true);
modelBuilder.Filter("IsActiveByte", (Person p) => p.IsActiveByte, 1);

根据连接字符串可以推断出所连接的数据库类型,因此您需要添加一个相应的数据库驱动程序。


谢谢。如果我理解正确,这需要我事先知道数据库的列需要读取为字节或位。 - Stavm
是的,这意味着在创建和使用第一个上下文之前,您需要知道这一点。出于性能考虑,EF会将模型+映射一次存储到应用程序域中。因此,您可以编写代码,在运行时通过常规的ADO.Net从数据库读取适当的元数据,然后决定提供哪个实体连接字符串给新的上下文。 - Gert Arnold
据我理解,这有点类似于创建两个不同的EDMX“映射”文件。如果像Colin在这里建议的那样,已经创建了两个EDMX文件,两个dbContexts,那么我想我可以使用ADO.net读取元数据并决定使用哪个dbContext。将来维护起来不是更简单吗?无论如何,你的答案非常有启发性,+1。 - Stavm
我认为在一个程序集中使用两个使用相同实体类集合的EDMX会很困难。每个EDMX都想要创建自己的类集合,因此你会遇到冲突(即使这些类在不同的命名空间中)。 - Gert Arnold
1
重要的是要记住,基于数据库的上下文始终从这些嵌入式资源文件中读取其模型和映射元数据。如果您想让上下文具有不同的模型,则需要另一个EDMX或另一组模型文件。子类型不是一个选项。当然,正如所说,使用代码优先方式会更加容易。 - Gert Arnold
记录一下,我简单尝试了你提出的“为什么不创建两个EDMX文件”的选项?可以有两个映射到同一个Person类的edmx文件,但是最后保存的EDMX会根据自己的配置修改类。你可以添加一个包含IsActiveByte属性的部分类文件,并始终、始终将“BitContext”最后保存,但我认为这非常容易出错。 - Gert Arnold

4

我想给你提供一些考虑的选择:

选择1:

如果可能的话,请在数据库端调整模式,因为从长远来看,这将只会给你带来麻烦。

选择2:

或者转而使用“代码优先”以腾出空间进行一些微调。此外,如果你依赖EDMX,这个特性在EF Core中已被删除。

选择3:

给该表格特别处理,例如,将其从主上下文中排除,并创建一个新的上下文来管理它。

除此之外,据我所知,大概不会有其他方法可以解决。

很抱歉我只能给你一些想法。


这确实是我的选择。但希望有一种方法可以绕过我还不知道的方式。 - Stavm

2

在我看来,这不是一个以数据库为先的尝试。它更像是一种“从数据库生成类的代码优先”尝试。

  1. 生成主模式
  2. 从主模式生成代码
  3. 使用生成的代码连接到不同的数据库

你所做的错误是使用了错误的主模式。你试图使用不兼容的数据类型进行访问。 编写一个具有兼容数据类型的模式(或直接编写代码)。最简单的方法是仅使用字符串属性,稍后再进行映射。

示例类

[Table("dbo.G")]
public class G
{
    public string Id { get; set; }
    [Column("CI_BlockWithID")]
    public string CiBlockWithIdStr { get; set; }
    public int CiBlockWithId
    {
        get { return Convert.ToInt32(this.CiBlockWithIdStr); }
        set { this.CiBlockWithIdStr = value.ToString(); }
    }
}

该示例展示了一个以代码为基础的片段来解释其运作原理。在此示例中,您需要使用兼容数据类型的代码 - 字符串。
问题是如何获得这段代码?
a. Code-First (你不想这么做)
b. 强制代码生成器选择其他数据类型(从主模式生成)。
请注意,强类型比使用字符串更好。这只应该向您展示机制。如果您正在读取 TinyInt 和 Byte,则可以尝试使用 Int32 作为主类型等(根据 db-provider)。在 MySQL 中,我们使用字符串来读取 DateTimes 和 Enums。

所以基本上你建议的是将所有东西转换为“代码优先”(在数据库中定义字段为varchar是多余的,一旦我有自定义getter和setter,我可以做任何我想做的事情,在数据库优先的情况下,我无法覆盖它们)。 - Stavm
基本建议是创建一个没有数据的模式。这将成为您的主模式。您可以在此模式中调整数据类型,并将其用作“数据库优先”方法的基础。示例代码显示了首次尝试编写代码的情况,这应该解释了为什么需要具有兼容数据类型的主模式。 - kara
在数据优先的情况下,您无法覆盖模型类的getter和setter。最好的方法是扩展该类,因为它是部分类。如果我的主方案将CI_BlockWithID定义为varchar,它将得到相同的异常。不会进行任何隐式转换。这不会让我进一步发展。 - Stavm
我不是模型优先方面的专家,但在代码优先方面它是可以工作的,因为每个数据都可以被读取为字符串。你尝试使用varchar了吗?你的错误是一个字节和布尔类型之间的转换问题,它们不兼容。 - kara
1
仅翻译文本内容:为了好玩,如预期所示,在 'CiBlockWithId' 上的属性无法设置为 'System.Boolean' 值。您必须将此属性设置为 'System.String' 类型的非空值。 - 将数据库列更改为 varchar。然后从数据库更新模型,最后将数据库列更改为 bit,并尝试查询。 - Stavm
显示剩余2条评论

1
  1. Could you add a view to all the databases that does the conversion from bit to byte within its defining sql? Then include the view instead of the table. You may also have to use stored procedures to do updates and inserts. Effectively you'd be using the view to make the databases appear identical to a single DbContext.

  2. Could you have multiple DbContexts that inherit from a base context, then dynamically change the context when required, instead of changing connection strings? I use a Unit of Work / Repository pattern with dependency injection. My Unit of Work is dependent on a DbContext:

    public class UnitOfWork 
    {
        private readonly DbContext context;
    
        public UnitOfWork(DbContext context)
        {
            this.context = context;
        }
    }
    
我定义了在应用程序启动期间应注入工作单元的内容。如果您使用此模式,则会在当前切换连接字符串的点注入正确的 DbContext

我猜(2)可能会起作用,尽管为了在一个表的一个单独列上允许一些灵活性而维护两个EDMX文件,感觉就像用A-1坦克杀蚊子。令我惊讶的是,对于我来说听起来并不那么牵强的情况,这竟然成为了一个如此棘手的问题。敢问当处理遗留数据库时,这可能是一个潜在的常见问题。 - Stavm
我意识到我在这里谈论了继承。使用Code First,这是一个相对容易的选择。我认为,像其他人建议的那样放弃数据库优先,会极大地减轻头痛。 - Colin
把你的上下文分开可能有其他好处。我认为第二种方法似乎是一个不错的选择。不过,工作单元模式与这个问题无关,它是一种反模式。 - Aluan Haddad

-1
您可以为实体创建一个部分类,并添加一个属性,以处理任何情况:
public partial class X
{
    public int TrueY
    {
        //add verification logic here
    }
}

有什么情况?你是什么意思?EF非常严格,CLR数据类型可以映射到哪种存储数据类型。 - Gert Arnold
@GertArnold,我的意思是,如果您不知道可以添加哪种类型的属性,您可以在部分类中添加其他属性,在那里您可以尝试int.TryParse(MyProp)等操作。 - Max
问题是:如何将既可以是int类型也可以是bit类型的数据库字段映射到类模型中的一个属性。即所谓"MyProp"的映射是个问题。 - Gert Arnold
是的,我刚刚重新阅读了问题,你在Model-first方法中是正确的,如果你将这个属性更改为string类型,然后使用部分类方法或其他方法来处理它,那么是可以实现的。 - Max
那太模糊了。试着把你的答案转化成一个可行的例子。 - Gert Arnold
@Max,问题出在此之前。当EF尝试使用值填充模型时,它会抛出异常。您不能添加非映射属性并期望它依赖于您从未获得的值。如果您在模型中忽略该值,则不会获取该值;如果您不忽略它,则会出现异常;如果您试图聪明地将其类型设置为“dynamic”或“object”,EF会向您抛出“结果不符合模型”的异常。陷入了进退两难的境地。 - Stavm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接