F#类型提供程序与C#接口+实体框架 [英] F# type providers vs C# interfaces + Entity Framework

查看:87
本文介绍了F#类型提供程序与C#接口+实体框架的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

The question is very technical, and it sits deeply between F# / C# differences. It is quite likely that I might’ve missed something. If you find a conceptual error, please, comment and I will update the question.

让我们从C#世界开始。假设我有一个简单的业务对象,将其称为 Person (但是请记住,有100多个对象远比我们在业务领域中要复杂的多

Let’s start from C# world. 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):

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

我使用DI / IOC,因此我从未真正通过。相反,我将始终使用接口(如上所述),将其称为 IPerson

and I use DI / IOC and so that I never actually pass a Person around. Rather, I would always use an interface (mentioned above), call it IPerson:

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

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

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. At this point I have an option to introduce "database" related class(es), e.g. EFPerson:

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

以及与数据库相关的相关属性和代码,我将跳过为简便起见,然后使用反射在 Person EFPerson 之间复制 IPerson 接口的属性code>或直接使用 EFPerson (作为 IPerson 传递)或执行其他操作。这是完全无关紧要的,因为消费者始终会看到 IPerson ,因此可以在任何时候更改实现,而无需消费者对此有所了解。

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. 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 (假设我添加了一个属性 DateTime DateOfBirth {get; set;} ),然后编译器会告诉我要解决的问题。但是,如果我从界面中删除了该属性(假设我不再需要 LastName ),那么编译器将无济于事。但是,我可以编写一个基于反射的测试,该测试将确保 IPerson Person EFPerson 等相同。确实不是必需的,但是可以做到这一点,然后它将像魔术一样工作(是的,我们确实有这样的测试,它们的确像魔术一样工作。)

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. 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. However, I can write a Reflection-based test, which would ensure that the properties of IPerson, Person, EFPerson, etc. are identical. 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).

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

Now, let’s get to F# world. 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!

酷!

首先,有人必须创建/更新数据库对象,如果涉及的开发人员不止一个,那么很自然,数据库可能并且会在不同的分支机构中升级/降级。到目前为止,根据我的经验,当涉及到F#类型提供程序时,这是一个极大的痛苦。即使使用C#EF Code First来处理迁移,也需要一些大量的萨满舞才能使F#类型的提供者高兴。

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. Even if C# EF Code First is used to handle migrations, some "extensive shaman dancing" is required to make F# type providers "happy".

第二,一切在默认情况下,F#世界(除非我们使其可变),因此我们显然不希望将可变数据库对象传递给上游。这意味着一旦我们从数据库加载了可变行,就希望尽快将其转换为本地 F#不可变结构,以便仅在上游使用纯函数。毕竟,使用纯函数会减少所需测试的次数,具体视域而定,为5至50倍。

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. 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. After all, using pure functions decreases the number of required tests in, I guess, 5 – 50 times, depending on the domain.

让我们回到我们的 Person 。我现在将跳过任何可能的重新映射(例如,将数据库整数转换为F#DU大小写和类似内容)。因此,我们的F# Person 看起来像这样:

Let’s get back to our Person. I will skip any possible re-mapping for now (e.g. database integer into F# DU case and similar stuff). So, our F# Person would look like that:

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

因此,如果明天我需要在此类型中添加 dateOfBirth:DateTime ,那么编译器会告诉我所有位置这需要解决。这很棒,因为C#编译器不会告诉我除数据库之外我需要在哪里添加该出生日期。 F#编译器不会告诉我我需要去到数据库 Person 表中。但是,在C#中,由于必须首先更新接口,因此编译器会告诉我必须修复哪些对象,包括数据库。

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. This is great because C# compiler will not tell me where I need to add that date of birth, … except the database. The F# compiler will not tell me that I need to go and add a database column to the table 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).

显然,我希望从F#中获得两全其美。尽管可以使用接口来实现这一点,但是感觉不到F#的方式。毕竟,DI / IOC的模拟在F#中的实现方式非常不同,通常是通过传递函数而不是接口来实现的。

Apparently, I want the best from both worlds in F#. And while this can be achieved using interfaces, it just does not feel the F# way. After all, the analog of DI / IOC is done very differently in F# and it is usually achieved by passing functions rather than interfaces.

因此,这里有两个问题。 / p>

So, here are two questions.


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

  1. How can I easily manage database up / down migrations in 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#的实现方法是什么?如上所述,实现 C#最好的世界:当我更新F#时,键入 Person ,然后修复所有需要在记录中添加/删除属性的地方,那会是什么?是在我尚未更新数据库以匹配业务对象时在编译时或至少在测试时失败的最合适的F#方法?

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)?


推荐答案


如何在F#世界中轻松管理数据库上/下迁移?
首先,当有很多开发人员参与时,在F#世界中实际进行数据库
迁移的正确方法是什么?

How can I easily manage database up / down migrations in 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?

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

Most natural way to manage Db migrations is to use tools native to db i.e. plain 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. Consumer apps are both in F# (type providers) and C# (EF), sometimes with the same database. Works like a charm.

您首先提到了EF代码。 F#SQL提供程序本质上都是 Db First,因为它们基于外部数据源(数据库)生成类型,而不是基于其他方式。我认为混合两种方法不是一个好主意。实际上,我不会向任何人推荐EF Code First来管理迁移:纯SQL更简单,不需要广泛的萨满舞,它无限灵活并且为更多的人所了解。
如果您对手动SQL脚本不满意,并考虑将EF Code First仅仅用于自动生成迁移脚本,那么甚至可以 MS SQL Server Management Studio设计器可以为您生成迁移脚本

You mentioned 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. 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. 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


实现上述 $ C $世界的F#方法是什么:如上​​所述
:当我更新F#时键入Person,然后修复所有我
需要向记录中添加/删除属性,在我未更新数据库时,在编译时或至少在
测试时,最失败的
合适的F#方法是什么?匹配业务
对象?

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)?

我的食谱如下:


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


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

interfaces, it just does not feel the F# way




  • 不要让自动生成的类型从类型提供程序泄漏到瘦数据库访问层之外。它们不是业务对象,并且事实上,EF实体都不是

  • 改为声明F#记录和/或已区分的联合作为您的域对象。可以根据需要对它们进行建模,而不会受到数据库架构的约束。

  • 在数据库访问层中,将自动生成的数据库类型映射到您的域F#类型。 由类型提供程序自动生成的每种类型的使用都在此处开始和结束。是的,这意味着您必须手动编写映射并在此处引入人为因素,例如您可能不小心将FirstName映射到LastName。实际上,这是很小的开销,去耦的好处要大得多。

  • 如何确保您不会忘记映射某些属性?不可能,如果记录未完全初始化,F#编译器将发出错误。

  • 如何添加新属性而不忘记初始化它?从F#代码开始:向域记录/记录添加新属性,F#编译器将指导您进行所有记录实例化(通常只有一个实例),并强制您用某种方式对其进行初始化(您将必须相应地添加迁移脚本/升级数据库架构) )。

  • 如何删除属性,别忘了清理数据库结构中的所有内容。从另一端开始:从数据库中删除列。类型提供程序类型和域F#记录之间的所有映射都将中断并突出显示已变得多余的属性(更重要的是,它将迫使您再次检查它们是否确实是冗余的并重新考虑您的决定)。

  • 实际上,在某些情况下,您可能想保留数据库列(例如,出于历史/审计目的),而仅从F#代码中删除属性。将域模型与数据库模式分离很方便时,这只是多种情况中的一种(而且很少)。

    • Don't let autogenerated types from type provider to leak outside thin db access layer. They are not business objects, and neither EF entities are as a matter of fact.
    • Instead declare F# records and/or discriminated unions as your domain objects. Model them as you please and don't feel constrained by db schema.
    • In db access layer, map from autogenerated db types to your domain F# types. Every usage of types autogenerated by Type Provider begins and ends here. Yes, it means you have to write mappings manually and introduce human factor here e.g. you can accidentally map FirstName to 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.
    • 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).
    • 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).
    • In fact in some scenarios you may want to preserve database column (e.g. for historical/audit purposes) and only remove property from F# code. It's just one (and rather rare) of multitude of scenarios when it's convenient to have domain model decoupled from db schema.
    • 总之


      • 通过普通SQL迁移

      • 域类型是手动声明的F#记录

      • 从类型提供程序到F#域类型的手动映射

      更短 strong>

      坚持单一责任原则并享受其中的好处。

      Stick with Single Responsibility Principle and enjoy the benefits.

      这篇关于F#类型提供程序与C#接口+实体框架的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆