繁体   English   中英

F#类型提供者与C#接口+实体框架

[英]F# type providers vs C# interfaces + Entity Framework

问题非常技术性,它深深地介于F#/ C#差异之间。 我很可能错过了一些东西。 如果您发现概念错误,请发表评论,我将更新问题。

让我们从C#世界开始吧。 假设我有一个简单的业务对象,称之为Person (但是,请记住,有100多个对象远比我们使用的业务领域中的对象复杂得多):

public class Person : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

我使用DI / IOC,所以我从来没有真正传递过一个Person 相反,我总是使用一个接口(如上所述),称之为IPerson

public interface IPerson
{
    int PersonId { get; set; }
    string Name { get; set; }
    string LastName { get; set; }
}

业务要求是可以从数据库序列化/反序列化该人员。 假设我选择使用Entity Framework,但实际的实现似乎与问题无关。 此时我可以选择引入“数据库”相关类,例如EFPerson

public class EFPerson : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

以及相关的数据库相关属性和代码,为简洁起见,我将跳过,然后使用Reflection复制PersonEFPerson之间的IPerson接口的属性,或者直接使用EFPerson (作为IPerson传递)或执行其他操作。 这是相当无关紧要的,因为消费者总是会看到IPerson ,因此可以随时更改实施,而消费者对此一无所知。

如果我需要添加一个属性,那么我会首先更新接口IPerson (假设我添加一个属性DateTime DateOfBirth { get; set; } )然后编译器将告诉我要修复什么。 但是,如果我从界面中删除该属性(假设我不再需要LastName ),那么编译器将无法帮助我。 但是,我可以编写一个基于反射的测试,它将确保IPersonPersonEFPerson等的属性相同。 这不是真的需要,但它可以完成然后它将像魔术一样工作(是的,我们确实有这样的测试,他们确实像魔术一样工作)。

现在,让我们来到F#世界。 这里我们有类型提供程序,它完全消除了在代码中创建数据库对象的需要:它们由类型提供程序自动创建!

凉! 但是吗?

首先,有人必须创建/更新数据库对象,如果涉及多个开发人员,那么数据库可能并将在不同分支中升级/降级是很自然的。 到目前为止,根据我的经验,当涉及到F#类型的提供者时,这是一个极度痛苦的问题。 即使使用C#EF Code First来处理迁移,也需要一些“广泛的萨满舞蹈”来使F#类型的提供者“满意”。

其次,默认情况下,F#世界中的所有内容都是不可变的(除非我们使其变为可变),因此我们显然不希望向上游传递可变数据库对象。 这意味着一旦我们从数据库加载一个可变行,我们希望尽快将其转换为“本机”F#不可变结构,以便仅使用上游的纯函数。 毕竟,使用纯函数可以减少所需测试的数量,我猜,5到50次,具体取决于域。

让我们回到我们的Person 我现在将跳过任何可能的重新映射(例如数据库整数到F#DU情况和类似的东西)。 所以,我们的F# Person看起来像那样:

type Person =
    {
        personId : int
        name : string
        lastName : string
    }

因此,如果“明天”我需要将dateOfBirth : DateTime添加到此类型,那么编译器将告诉我所有需要修复的地方。 这很好,因为C#编译器不会告诉我在哪里需要添加出生日期,...除了数据库。 F#编译器不会告诉我我需要将数据库列添加到表Person 但是,在C#中,由于我必须首先更新接口,编译器会告诉我必须修复哪些对象,包括数据库。

显然,我希望F#中的两个世界都是最好的。 虽然这可以通过接口实现,但它感觉不到F#方式。 毕竟,DI / IOC的模拟在F#中完全不同,它通常通过传递函数而不是接口来实现。

所以,这里有两个问题。

  1. 如何在F#world中轻松管理数据库上/下迁移? 而且,首先,当涉及到许多开发人员时,在F#世界中实际进行数据库迁移的正确方法是什么?

  2. 如上所述,实现“最好的C#世界”的F#方式是什么:当我更新F#类型Person然后修复我需要添加/删除属性到记录的所有地方时,最合适的F#方式是什么?在编译时或至少在我没有更新数据库以匹配业务对象的测试时“失败”?

如何在F#world中轻松管理数据库上/下迁移? 而且,首先,当涉及到许多开发人员时,在F#世界中实际进行数据库迁移的正确方法是什么?

管理Db迁移的最自然方式是使用db本机工具,即纯SQL。 在我们的团队中,我们使用dbup包,为每个解决方案我们创建一个小型控制台项目,以便在开发和部署期间汇总数据库迁移。 消费者应用程序都在F#(类型提供程序)和C#(EF)中,有时使用相同的数据库。 奇迹般有效。

你提到了EF Code First。 F#SQL提供程序本质上都是“Db First”,因为它们基于外部数据源(数据库)生成类型,而不是相反。 我不认为混合两种方法是个好主意。 事实上,我不会向任何人推荐EF Code First来管理迁移:普通的SQL更简单,不需要“广泛的萨满舞”,更多的人可以更灵活和更容易理解。 如果您对手动SQL脚本感到不舒服,并考虑将EF Code First用于自动生成迁移脚本,那么即使MS SQL Server Management Studio设计器也可以为您生成迁移脚本

如上所述,实现“最好的C#世界”的F#方式是什么:当我更新F#类型Person然后修复我需要添加/删除属性到记录的所有地方时,最合适的F#方式是什么?在编译时或至少在我没有更新数据库以匹配业务对象的测试时“失败”?

我的食谱如下:

  • 不要使用接口。 如你所说 :)

接口,它只是感觉不到F#方式

  • 不要让类型提供程序中的自动生成类型泄漏到精简db访问层之外。 它们不是业务对象,并且EF实体都不是事实。
  • 而是将F#记录和/或有区别的联合声明为您的域对象。 根据需要对它们进行建模,不要受db模式的限制。
  • 在db访问层中,从自动生成的db类型映射到域F#类型。 Type Provider自动生成的每种类型的使用都在此处开始和结束。 是的,这意味着您必须手动编写映射并在此处引入人为因素,例如,您可能会意外地将FirstName映射到LastName。 在实践中,这是一个微小的开销,并且解耦的好处超过它的幅度。
  • 如何确保你不要忘记映射一些属性? 这是不可能的,如果记录未完全初始化,F#编译器将发出错误。
  • 如何添加新属性而不是忘记初始化它? 从F#代码开始:向域记录/记录添加新属性,F#编译器将引导您进入所有记录实例(通常只有一个)并强制您使用某些内容对其进行初始化(您必须相应地添加迁移脚本/升级数据库架构) )。
  • 如何删除属性,不要忘记清理数据库模式的所有内容。 从另一端开始:从数据库中删除列。 类型提供程序类型和域F#记录之间的所有映射都会破坏并突出显示多余的属性(更重要的是,它会强制您仔细检查它们是否真的是多余的并重新考虑您的决定)。
  • 事实上,在某些情况下,您可能希望保留数据库列(例如,用于历史/审计目的),并仅从F#代码中删除属性。 当域模型与数据库模式分离时,它只是众多场景中的一种(而且相当罕见)。

简而言之

  • 通过纯SQL迁移
  • 域类型是手动声明的F#记录
  • 从类型提供程序到F#域类型的手动映射

更短

坚持单一责任原则,享受好处。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM