简体   繁体   English

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

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

The question is very technical, and it sits deeply between F# / C# differences. 问题非常技术性,它深深地介于F#/ C#差异之间。 It is quite likely that I might've missed something. 我很可能错过了一些东西。 If you find a conceptual error, please, comment and I will update the question. 如果您发现概念错误,请发表评论,我将更新问题。

Let's start from C# world. 让我们从C#世界开始吧。 Suppose that I have a simple business object, call it Person (but, please, keep in mind that there are 100+ objects far more complicated than that in the business domain that we work with): 假设我有一个简单的业务对象,称之为Person (但是,请记住,有100多个对象远比我们使用的业务领域中的对象复杂得多):

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

and I use DI / IOC and so that I never actually pass a Person around. 我使用DI / IOC,所以我从来没有真正传递过一个Person Rather, I would always use an interface (mentioned above), call it IPerson : 相反,我总是使用一个接口(如上所述),称之为IPerson

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

The business requirement is that the person can be serialized to / deserialized from the database. 业务要求是可以从数据库序列化/反序列化该人员。 Let's say that I choose to use Entity Framework for that, but the actual implementation seems irrelevant to the question. 假设我选择使用Entity Framework,但实际的实现似乎与问题无关。 At this point I have an option to introduce “database” related class(es), eg EFPerson : 此时我可以选择引入“数据库”相关类,例如EFPerson

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

along with the relevant database related attributes and code, which I will skip for brevity, and then use Reflection to copy properties of IPerson interface between Person and EFPerson OR just use EFPerson (passed as IPerson ) directly OR do something else. 以及相关的数据库相关属性和代码,为简洁起见,我将跳过,然后使用Reflection复制PersonEFPerson之间的IPerson接口的属性,或者直接使用EFPerson (作为IPerson传递)或执行其他操作。 This is fairly irrelevant, as the consumers will always see IPerson and so the implementation can be changed at any time without the consumers knowing anything about it. 这是相当无关紧要的,因为消费者总是会看到IPerson ,因此可以随时更改实施,而消费者对此一无所知。

If I need to add a property, then I would update the interface IPerson first (let's say I add a property DateTime DateOfBirth { get; set; } ) and then the compiler will tell me what to fix. 如果我需要添加一个属性,那么我会首先更新接口IPerson (假设我添加一个属性DateTime DateOfBirth { get; set; } )然后编译器将告诉我要修复什么。 However, if I remove the property from the interface (let's say that I no longer need LastName ), then the compiler won't help me. 但是,如果我从界面中删除该属性(假设我不再需要LastName ),那么编译器将无法帮助我。 However, I can write a Reflection-based test, which would ensure that the properties of IPerson , Person , EFPerson , etc. are identical. 但是,我可以编写一个基于反射的测试,它将确保IPersonPersonEFPerson等的属性相同。 This is not really needed, but it can be done and then it will work like magic (and yes, we do have such tests and they do work like magic). 这不是真的需要,但它可以完成然后它将像魔术一样工作(是的,我们确实有这样的测试,他们确实像魔术一样工作)。

Now, let's get to F# world. 现在,让我们来到F#世界。 Here we have the type providers, which completely remove the need to create database objects in the code: they are created automatically by the type providers! 这里我们有类型提供程序,它完全消除了在代码中创建数据库对象的需要:它们由类型提供程序自动创建!

Cool! 凉! But is it? 但是吗?

First, somebody has to create / update the database objects and if there is more than one developer involved, then it is natural that the database may and will be upgraded / downgraded in different branches. 首先,有人必须创建/更新数据库对象,如果涉及多个开发人员,那么数据库可能并将在不同分支中升级/降级是很自然的。 So far, from my experience, this is an extreme pain on the neck when F# type providers are involved. 到目前为止,根据我的经验,当涉及到F#类型的提供者时,这是一个极度痛苦的问题。 Even if C# EF Code First is used to handle migrations, some “extensive shaman dancing” is required to make F# type providers “happy”. 即使使用C#EF Code First来处理迁移,也需要一些“广泛的萨满舞蹈”来使F#类型的提供者“满意”。

Second, everything is immutable in F# world by default (unless we make it mutable), so we clearly don't want to pass mutable database objects upstream. 其次,默认情况下,F#世界中的所有内容都是不可变的(除非我们使其变为可变),因此我们显然不希望向上游传递可变数据库对象。 Which means that once we load a mutable row from the database, we want to convert it into a “native” F# immutable structure as soon as possible so that to work only with pure functions upstream. 这意味着一旦我们从数据库加载一个可变行,我们希望尽快将其转换为“本机”F#不可变结构,以便仅使用上游的纯函数。 After all, using pure functions decreases the number of required tests in, I guess, 5 – 50 times, depending on the domain. 毕竟,使用纯函数可以减少所需测试的数量,我猜,5到50次,具体取决于域。

Let's get back to our Person . 让我们回到我们的Person I will skip any possible re-mapping for now (eg database integer into F# DU case and similar stuff). 我现在将跳过任何可能的重新映射(例如数据库整数到F#DU情况和类似的东西)。 So, our F# Person would look like that: 所以,我们的F# Person看起来像那样:

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

So, if “tomorrow” I need to add dateOfBirth : DateTime to this type, then the compiler will tell me about all places where this needs to be fixed. 因此,如果“明天”我需要将dateOfBirth : DateTime添加到此类型,那么编译器将告诉我所有需要修复的地方。 This is great because C# compiler will not tell me where I need to add that date of birth, … except the database. 这很好,因为C#编译器不会告诉我在哪里需要添加出生日期,...除了数据库。 The F# compiler will not tell me that I need to go and add a database column to the table Person . F#编译器不会告诉我我需要将数据库列添加到表Person However, in C#, since I would have to update the interface first, the compiler will tell me which objects must be fixed, including the database one(s). 但是,在C#中,由于我必须首先更新接口,编译器会告诉我必须修复哪些对象,包括数据库。

Apparently, I want the best from both worlds in F#. 显然,我希望F#中的两个世界都是最好的。 And while this can be achieved using interfaces, it just does not feel the F# way. 虽然这可以通过接口实现,但它感觉不到F#方式。 After all, the analog of DI / IOC is done very differently in F# and it is usually achieved by passing functions rather than interfaces. 毕竟,DI / IOC的模拟在F#中完全不同,它通常通过传递函数而不是接口来实现。

So, here are two questions. 所以,这里有两个问题。

  1. How can I easily manage database up / down migrations in F# world? 如何在F#world中轻松管理数据库上/下迁移? And, to start from, what is the proper way to actually do the database migrations in F# world when many developers are involved? 而且,首先,当涉及到许多开发人员时,在F#世界中实际进行数据库迁移的正确方法是什么?

  2. What is the F# way to achieve “the best of C# world” as described above: when I update F# type Person and then fix all places where I need to add / remove properties to the record, what would be the most appropriate F# way to “fail” either at compile time or at least at test time when I have not updated the database to match the business object(s)? 如上所述,实现“最好的C#世界”的F#方式是什么:当我更新F#类型Person然后修复我需要添加/删除属性到记录的所有地方时,最合适的F#方式是什么?在编译时或至少在我没有更新数据库以匹配业务对象的测试时“失败”?

How can I easily manage database up / down migrations in F# world? 如何在F#world中轻松管理数据库上/下迁移? And, to start from, what is the proper way to actually do the database migrations in F# world when many developers are involved? 而且,首先,当涉及到许多开发人员时,在F#世界中实际进行数据库迁移的正确方法是什么?

Most natural way to manage Db migrations is to use tools native to db ie plain SQL. 管理Db迁移的最自然方式是使用db本机工具,即纯SQL。 At our team we use dbup package, for every solution we create a small console project to roll up db migrations in dev and during deployment. 在我们的团队中,我们使用dbup包,为每个解决方案我们创建一个小型控制台项目,以便在开发和部署期间汇总数据库迁移。 Consumer apps are both in F# (type providers) and C# (EF), sometimes with the same database. 消费者应用程序都在F#(类型提供程序)和C#(EF)中,有时使用相同的数据库。 Works like a charm. 奇迹般有效。

You mentioned EF Code First. 你提到了EF Code First。 F# SQL providers are all inherently "Db First" because they generate types based on external data source (database) and not the other way around. F#SQL提供程序本质上都是“Db First”,因为它们基于外部数据源(数据库)生成类型,而不是相反。 I don't think that mixing two approaches is a good idea. 我不认为混合两种方法是个好主意。 In fact I wouldn't recommend EF Code First to anyone to manage migrations: plain SQL is simpler, doesn't require "extensive shaman dancing", infinitely more flexible and understood by far more people. 事实上,我不会向任何人推荐EF Code First来管理迁移:普通的SQL更简单,不需要“广泛的萨满舞”,更多的人可以更灵活和更容易理解。 If you are uncomfortable with manual SQL scripting and consider EF Code First just for automatic generation of migration script then even MS SQL Server Management Studio designer can generate migration scripts for you 如果您对手动SQL脚本感到不舒服,并考虑将EF Code First用于自动生成迁移脚本,那么即使MS SQL Server Management Studio设计器也可以为您生成迁移脚本

What is the F# way to achieve “the best of C# world” as described above: when I update F# type Person and then fix all places where I need to add / remove properties to the record, what would be the most appropriate F# way to “fail” either at compile time or at least at test time when I have not updated the database to match the business object(s)? 如上所述,实现“最好的C#世界”的F#方式是什么:当我更新F#类型Person然后修复我需要添加/删除属性到记录的所有地方时,最合适的F#方式是什么?在编译时或至少在我没有更新数据库以匹配业务对象的测试时“失败”?

My recipe is as follows: 我的食谱如下:

  • Don't use the interfaces. 不要使用接口。 as you said :) 如你所说 :)

interfaces, it just does not feel the F# way 接口,它只是感觉不到F#方式

  • Don't let autogenerated types from type provider to leak outside thin db access layer. 不要让类型提供程序中的自动生成类型泄漏到精简db访问层之外。 They are not business objects, and neither EF entities are as a matter of fact. 它们不是业务对象,并且EF实体都不是事实。
  • Instead declare F# records and/or discriminated unions as your domain objects. 而是将F#记录和/或有区别的联合声明为您的域对象。 Model them as you please and don't feel constrained by db schema. 根据需要对它们进行建模,不要受db模式的限制。
  • In db access layer, map from autogenerated db types to your domain F# types. 在db访问层中,从自动生成的db类型映射到域F#类型。 Every usage of types autogenerated by Type Provider begins and ends here. Type Provider自动生成的每种类型的使用都在此处开始和结束。 Yes, it means you have to write mappings manually and introduce human factor here eg you can accidentally map FirstName to LastName. 是的,这意味着您必须手动编写映射并在此处引入人为因素,例如,您可能会意外地将FirstName映射到LastName。 In practice it's a tiny overhead and benefits of decoupling outweigh it by a magnitude. 在实践中,这是一个微小的开销,并且解耦的好处超过它的幅度。
  • How to make sure you don't forget to map some property? 如何确保你不要忘记映射一些属性? It's impossible, F# compiler will emit error if record not fully initialized. 这是不可能的,如果记录未完全初始化,F#编译器将发出错误。
  • How to add new property and not forget to initialize it? 如何添加新属性而不是忘记初始化它? Start with F# code: add new property to domain record/records, F# compiler will guide you to all record instantiations (usually just one) and force you to initialize it with something (you will have to add a migration script / upgrade database schema accordingly). 从F#代码开始:向域记录/记录添加新属性,F#编译器将引导您进入所有记录实例(通常只有一个)并强制您使用某些内容对其进行初始化(您必须相应地添加迁移脚本/升级数据库架构) )。
  • How to remove a property and don't forget to clean up everything up to db schema. 如何删除属性,不要忘记清理数据库模式的所有内容。 Start from the other end: delete column from database. 从另一端开始:从数据库中删除列。 All mappings between type provider types and domain F# records will break and highlight properties that became redundant (more importantly, it will force you to double check that they are really redundant and reconsider your decision). 类型提供程序类型和域F#记录之间的所有映射都会破坏并突出显示多余的属性(更重要的是,它会强制您仔细检查它们是否真的是多余的并重新考虑您的决定)。
  • In fact in some scenarios you may want to preserve database column (eg for historical/audit purposes) and only remove property from F# code. 事实上,在某些情况下,您可能希望保留数据库列(例如,用于历史/审计目的),并仅从F#代码中删除属性。 It's just one (and rather rare) of multitude of scenarios when it's convenient to have domain model decoupled from db schema. 当域模型与数据库模式分离时,它只是众多场景中的一种(而且相当罕见)。

In Short 简而言之

  • migrations via plain SQL 通过纯SQL迁移
  • domain types are manually declared F# records 域类型是手动声明的F#记录
  • manual mapping from Type Providers to F# domain types 从类型提供程序到F#域类型的手动映射

Even Shorter 更短

Stick with Single Responsibility Principle and enjoy the benefits. 坚持单一责任原则,享受好处。

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

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