数据库迁移和历史数据生成
This commit is contained in:
parent
bfd1ee0547
commit
0b1c871c7e
137
notes/helloshop/migration-dataseeding.md
Normal file
137
notes/helloshop/migration-dataseeding.md
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# 自动迁移和数据库初始化
|
||||||
|
|
||||||
|
## 生成迁移脚本
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ef migrations add InitialCreate -o Infrastructure/Migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
## 手动将最新迁移脚本应用到数据库
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用代码创建数据库
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await dbContext.Database.EnsureCreatedAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
`EnsureCreatedAsync` 会创建数据库,并会创建所有表,但不会应用迁移脚本,适用于开发环境。
|
||||||
|
|
||||||
|
## 使用代码将最新迁移脚本应用到数据库
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await dbContext.Database.MigrateAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
`MigrateAsync` 会应用迁移脚本,如果数据库不存在,会先创建数据库,适用于生产环境。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
如果先前已经使用 `EnsureCreatedAsync` 创建了数据库,再使用 `MigrateAsync` 会抛出异常,因为数据库和所有表已经存在,因为迁移中有创建表的操作脚本。
|
||||||
|
|
||||||
|
如果数据库不存在,使用 `MigrateAsync` 会先创建数据库,再创建迁移历史表,然后再应用迁移脚本,但这个过程中会打印一些错误警告信息,虽然可以忽略,但不够优雅。
|
||||||
|
|
||||||
|
## 优雅的做法
|
||||||
|
|
||||||
|
这里使用到一些 EF Core 的内部服务,注意他们在实现细节上的区别。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static async ValueTask RunMigrationAsync(TDbContext dbContext, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||||
|
var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
|
||||||
|
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||||
|
await strategy.ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
if (!await dbCreator.ExistsAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
await dbCreator.CreateAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
await historyRepository.CreateIfNotExistsAsync();
|
||||||
|
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1、使用 `dbCreator.ExistsAsync` 先检查数据库是否存在,不存在则仅仅创建数据库(它不会创建表),这个时候得到一个空的数据库。
|
||||||
|
|
||||||
|
2、再使用 `historyRepository.CreateIfNotExistsAsync` 在空数据库中创建迁移历史表,用于保存迁移记录。
|
||||||
|
|
||||||
|
3、最后使用 `dbContext.Database.MigrateAsync` 应用迁移脚本,因为迁移脚本中的操作会在空数据库中执行,包括创建表等等操作。
|
||||||
|
|
||||||
|
4、迁移完成后,数据库中会有所有表和迁移历史表,迁移历史表中登记了最新的迁移记录。
|
||||||
|
|
||||||
|
5、后续再使用 `dbContext.Database.MigrateAsync` 会检查迁移历史表,如果有新的迁移脚本,会应用新的迁移脚本。
|
||||||
|
|
||||||
|
6、这样就实现了自动迁移和数据库初始化。
|
||||||
|
|
||||||
|
为了以上操作都在一个事务中执行,我们使用 `dbContext.Database.CreateExecutionStrategy` 创建一个执行策略, 在策略中执行上述操作。
|
||||||
|
|
||||||
|
|
||||||
|
## 数据库初始化的方式
|
||||||
|
|
||||||
|
在最新的 EF 9.0 建议使用 UseSeeding 和 UseAsyncSeeding 并利用初始数据对数据库进行种子设定,这些代码会在`迁移`时执行。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)=> optionsBuilder
|
||||||
|
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
|
||||||
|
.UseSeeding((context, _) =>
|
||||||
|
{
|
||||||
|
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
|
||||||
|
if (testBlog == null)
|
||||||
|
{
|
||||||
|
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
|
||||||
|
if (testBlog == null)
|
||||||
|
{
|
||||||
|
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
不推荐传统的种子数据方式,因为它会在每次迁移时都会执行,而且不支持异步操作。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<Blog>().HasData(
|
||||||
|
new Blog { BlogId = 1, Url = "http://sample.com" },
|
||||||
|
new Blog { BlogId = 2, Url = "http://sample2.com" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 零度框架的数据库初始化
|
||||||
|
|
||||||
|
在某些场景下数据库初始化可能是一个耗时的操作,我们使用 IHostedService 或 BackgroundService 来异步执行数据库初始化。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DbInitializer : IHostedService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public DbInitializer(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
// The code to initialize the database
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user