我们有多个开发人员在处理一个使用Entity Framework 5.0的项目。每个开发人员都使用自己的本地SQL 2012数据库,这样他就可以在不妨碍他人的情况下进行开发和测试。
起初,我们使用了自动迁移和基于代码的迁移的混合。这根本不起作用,所以我们决定禁用自动迁移,只允许基于代码的迁移。我应该补充一点,我们从一个干净的数据库开始,没有来自所有自动迁移的"损坏"的_MigrationsHistory
。
所以现在的工作流程是:
- 开发人员更改数据模型
- 执行
add-migration <Name>
并使用update-database
将其应用于他的数据库 - 签入数据模型更改和迁移到Git
- 另一个开发人员提取、接收更改并将其应用于他的数据库
到目前为止,这一切都很顺利。然而,在今天之前,通常只有我进行迁移,其他人应用它们。但今天有三个开发人员进行了迁移。我刚刚完成了这些迁移,做了一个update-database
,结果很好。
然而,我也对自己的数据模型进行了更改,所以在update-database
结束时,它给了我一个警告,我仍然没有更新,所以我做了add-migration <my migration>
。然而,当它构建迁移时,它给了我已经应用到数据库的所有迁移的更改。所以:它试图删除已经删除的列,试图创建一个已经存在的表,等等
怎么可能呢?我的假设是,EF只需检查_MigrationsHistory
表,找出表中还不存在的迁移,并按照名称中的时间戳顺序逐个应用这些迁移。但显然不是,因为即使我撤消了自己的更改,并且有了一个干净的环境,它仍然会抱怨我的数据库与模型不同步。但我只是提取了这些更改并将它们应用到我的数据库中。它是同步的。我也可以在_MigrationsHistory
表中看到我刚刚应用的迁移。
我唯一能想到的是,我在数据模型中添加了一个不会导致数据库更改的属性(我在数据模式Y中添加了List<X>
,其中X是一对多关系中的多。这不会导致数据库的更改,因为X已经有了Y的外键)。会是这样吗?如果是这样的话,那真的很脆弱,因为没有办法为此添加迁移,因为没有数据库更改,我也不知道如何解决这个问题。
我不知道该如何处理,因为我当然可以编辑它搭建的内容,并删除已经应用到我的数据库中的所有内容。但后来呢?我签入了它,然后其他一些开发人员收到了同样的消息,即即使在应用了我的新更改后,他的数据库也不是最新的,构建了他自己的更改,得到了同样的无意义的构建,编辑了它,签入了,然后下一个开发人员得到了它。它变成了一个恶性循环,与我们使用自动迁移时的情况类似,我认为我们已经通过切换到仅基于代码来解决了这个问题。我现在不能相信它会做正确的事情,这样的工作简直是一场噩梦。
我还尝试用update-database -t:201211091112102_<migrationname>
一个接一个地添加我从同事那里获得的迁移,但没有成功。它仍然给了我错误的脚手架。
那么,我们在这里做错了什么,或者EF根本不是为这样的合作而建的吗?
更新
我创建了一个可复制的测试用例,尽管为了模拟这个多用户/多数据库的场景,这有点冗长。
https://github.com/JulianR/EfMigrationsTest/
当你有上述项目时,复制的步骤(这些步骤也出现在代码中):
- 添加迁移初始化
- 更新数据库(在数据库"TestDb"上)
- 将连接字符串更改为指向TestDb1
- 更新TestDb1上的数据库
- 对类Test取消注释属性Foo
- 添加迁移M1以将属性Foo添加到TestDb1
- 评论测试。再次Foo
- 将连接字符串更改为指向TestDb2
- 从项目中排除迁移M1,这样它就不会应用于TestDb2
- 类Test上的取消注释属性Bar
- 更新数据库以将Init迁移应用于TestDb2
- 添加迁移M2以将属性Bar添加到TestDb2
- 更改连接字符串以再次指向原始TestDb
- 将迁移M1再次包含到项目中
- 对类Test取消注释属性Foo
- 对类Test取消注释属性SomeInt
- 更新数据库
- 添加迁移M3
- 更新数据库时,由于M3试图将列Foo添加到刚刚由迁移M1添加的数据库TestDb中,因此出现错误
以上是模拟三个用户,其中用户1初始化他的数据库,另外两个也使用他的初始化来创建他们的数据库。然后,用户2和用户3都对数据模型进行了自己的更改,并将其与应用更改所需的迁移一起添加到源代码管理中。然后,用户1拉取用户2和3的更改,而用户1自己也对数据库进行了更改。然后用户1调用CCD_ 11来应用用户2和3的改变。然后,他构建自己的迁移,然后错误地将用户2或3的更改添加到构建的迁移中,这在应用于用户1的数据库时会导致错误。
您需要添加一个空白的"合并"迁移,该迁移将重置.resx文件中最新迁移的快照。使用IgnoreChanges开关执行此操作:
Add-Migration <migration name> -IgnoreChanges
有关的解释,请参阅此处
您需要像编码冲突一样手动解决迁移冲突。如果您进行了更新并且有新的迁移,则需要确保上次迁移后的元数据与当前模型匹配。要更新迁移的元数据,请为其重新发出"添加迁移"命令。
例如,在您的场景中的步骤17(更新数据库)之前,您应该发出以下命令
Add-Migration M2
这将更新元数据,使其与当前模型同步。现在,当您尝试添加M3时,它应该是空白的,因为您还没有进行任何进一步的模型更改。
选项1:添加一个空白的"合并"迁移
- 确保本地代码库中的任何挂起的模型更改都已写入迁移。这一步确保您不会错过任何生成空白时的合法更改迁移
- 与源代码管理同步
- 运行更新数据库以应用其他开发人员已签入的任何新迁移**注意:***如果您没有从更新数据库中收到任何警告命令,然后其他开发人员没有进行新的迁移不需要执行任何进一步的合并
- 运行添加迁移–忽略更改(例如,添加迁移合并–忽略更改)。这将生成一个包含所有元数据的迁移(包括当前模型的快照),但将忽略任何将当前模型与快照进行比较时检测到的更改在上一次迁移中(这意味着您得到一个空白的Up和Down方法)
- 继续开发,或提交给源代码管理(在运行您的单元测试)
选项2:更新上次迁移中的模型快照
- 确保本地代码库中的任何挂起的模型更改都已写入迁移。这一步确保您不会错过任何生成空白时的合法更改迁移
- 与源代码管理同步
- 运行更新数据库到应用其他开发人员已签入的任何新迁移**注意:***如果您没有从更新数据库中收到任何警告命令,然后其他开发人员没有进行新的迁移不需要执行任何进一步的合并
- 运行更新数据库–TargetMigration(在我们的示例中接下来是更新数据库–TargetMigration AddRating)。这将使数据库恢复到倒数第二的状态迁移–有效地"取消应用"数据库。**注意:***此步骤是确保编辑安全所必需的迁移的元数据,因为元数据也存储在数据库的__MigrationsHistoryTable。这就是为什么你应该仅当上次迁移仅在您的本地代码库。如果其他数据库应用了上次迁移还必须回滚它们,并将上次迁移重新应用到更新元数据
- 运行添加迁移(在示例中我们一直在关注这将类似于添加迁移201311062215252_AddReaders)。**注意:***您需要包括时间戳,以便迁移知道您要编辑现有的迁移而不是搭建一个新的迁移。这将更新上次迁移的元数据以匹配当前模型。你会命令完成后会得到以下警告,但正是你想要的。"仅用于迁移的设计器代码"201311062215252_AddReaders"重新搭建脚手架。重新搭建脚手架整个迁移,请使用-Force参数。">
- 运行更新数据库到使用更新的元数据重新应用最新的迁移
- 继续开发或提交给源代码管理(在运行您的单元之后当然是测试)
MSDN有一篇关于这方面的精彩文章。请检查一下。
团队环境中的实体框架代码优先迁移
我们的环境中也存在类似的问题,以下是我们迄今为止发现的问题以及我们如何解决的问题:
当您有已应用但未签入的更改(更新数据库),然后您收到另一个没有您的更改的开发人员的更改时,这就是事情似乎不同步的地方。根据我们的经验,在更新数据库过程中,为您自己的更改保存的元数据似乎被其他开发人员的元数据所覆盖。其他开发人员没有您的更改,因此保存的元数据不再是您数据库的真实反映。当EF在那之后进行比较时,它会"认为"由于元数据的变化,您的更改实际上又是新的。
一个简单但公认丑陋的解决方法是进行另一次迁移,并清除其中的内容,这样就有了空的up()和空的down()方法。应用该迁移并将其检查到源代码管理中,让每个人都同步到该迁移。这只是同步所有的元数据,这样每个人都可以考虑到所有的更改。
我在codeplex上添加了一个问题,这个问题也让我们团队中的许多人感到头疼。
链接是https://entityframework.codeplex.com/workitem/1670
我已经对此进行了一些思考,我希望我能为这里提出的不同意见和实践做出贡献。
考虑一下您的本地迁移实际代表了什么。在本地使用dev数据库时,当向表中添加列等、添加新实体等时,我使用迁移以最方便的方式更新数据库。
因此,AddMigration将我当前的模型(我们称之为模型b)与我以前的模型(模型a)进行比较,并生成一个从数据库中的a=>b开始的迁移。
对我来说,如果每个人都有自己的数据库,并且组织中存在某种stage/test/dev/production数据库服务器,那么尝试将我的迁移与其他任何人的迁移合并是没有意义的。这完全取决于团队是如何建立的,但如果你想真正以分布式的方式工作,那么将彼此与其他人所做的改变隔离开来是有意义的。
好吧,如果你是分布式工作的,并且有一些实体,例如Person,你正在处理它。出于某种原因,很多其他人也在处理它。所以,你在sprint中根据你的特定故事的需要添加和删除Person的属性(我们在这里都很敏捷,不是吗?),比如社会保障号码,你首先把它变成一个整数,因为你没有那么聪明,然后变成一个字符串等等。
您添加了名字和姓氏。
然后你完成了,你有十个奇怪的上下迁移(你可能在工作时删除了其中一些,因为它们只是垃圾),你从中央Git回购中获取一些更改。哇!你的同事鲍勃也需要一些名字,也许你们应该互相谈谈?
无论如何,我想他添加了NameFirst和NameLast。。。那你是做什么的?好吧,你可以合并、重构、更改,这样它就有了更合理的名称。。。像FirstName和LastName一样,你运行测试并检查他的代码,然后你推到中心。
但是迁移呢?好吧,现在是时候进行迁移了——移动中央回购,或者更具体地说,分支"测试",包含一个从其模型a=>模型b的漂亮的小迁移。这个迁移将是一个而且只有一个迁移,而不是十个奇怪的迁移。
你明白我在说什么吗?我们正在与漂亮的小pocos合作,它们之间的比较构成了实际的迁移。所以,我们根本不应该合并迁移,在我看来,我们应该为每个分支或类似的分支进行迁移。
事实上,我们甚至需要在合并后的分支中创建迁移吗?是的,如果这个数据库是自动更新的,我们需要
另一件需要考虑的事情是,在从中央回购中撤出之前,永远不要真正创建迁移。这意味着在创建迁移之前,您将获得其他团队成员的迁移代码和他们对模型的更改。
我还需要做更多的工作,至少这是我的想法。
我能够想出的解决方案(至少对于2个用户,还没有测试3个)是:
- 合并迁移以同步元数据运行更新数据库(这应该会失败),然后
- 添加数据库,然后
- 删除
up()
和down()
方法中生成的所有代码
这仍将由更新数据库运行,但不会执行任何操作,只是使元数据同步。
我同意@LavaEater的观点。问题的核心似乎是移民脚手架应该集中起来。也许每次推送都是作为自动化/集成构建过程的一部分?之后,团队成员可以从服务器中提取所产生的迁移。
这意味着他们自己的迁移脚本不应该被推送到服务器。
有一种简单的方法可以让在迁移时不发生合并冲突/错误。
- 像在任何时候一样处理分支
- 如果合并到master并出现合并错误,则:
- 从
migrations
文件夹中删除所有*.cs文件 - 在
migrations
文件夹中执行git checkout master ./*
- 重新创建迁移
- 您的快照是通过更新或更新获得的,不存在合并冲突
- 同样,在将拉请求合并到master之前,您需要与master合并,并始终执行步骤3-6
下面是执行步骤3-6的简单Powershell脚本:
function Write-Info($text)
{
Write-Color "$pwd", "> ", "$text" -Colour "Yellow", "Blue", "White"
}
function Create-Migration($project, $migrationName, $referenceBranch)
{
Set-Location "$SolutionPath$project"
Write-Info "Going to migrations"
Set-Location "Migrations"
Write-Info "Removing ./*.cs"
Remove-Item ./*.cs
Write-Info "git fetch --all"
git fetch --all
Write-Info "git checkout origin/$referenceBranch ./*"
git checkout origin/$referenceBranch ./*
Set-Location ..
Write-Info "Creating migration $migrationName "
dotnet ef migrations add "$migrationName"
}
在过去的半年里,我一直在使用这种方法。迁移时需要解决0个合并冲突8)。