第 5 章 创建数据服务
选择一种数据存储
由于我坚持要尽可能的跨平台,所以我决定选用 Postgres,而不用 SQL Server 以照顾 Linux 或 Mac 电脑的读者
构建 Postgres 仓储
在本节,我们要升级位置服务让它使用 Postgres
为了完成这一过程,需要创建一个新的仓储实现,以封装 PostgreSQL 的客户端通信
回顾一下位置仓库的接口
public interface ILocationRecordRepository {LocationRecord Add(LocationRecord locationRecord);LocationRecord Update(LocationRecord locationRecord);LocationRecord Get(Guid memberId, Guid recordId);LocationRecord Delete(Guid memberId, Guid recordId);LocationRecord GetLatestForMember(Guid memberId);ICollection<LocationRecord> AllForMember(Guid memberId);
}
接下来要做的就是创建一个数据库上下文
数据库上下文的使用方式是创建与特定模型相关的类型,并从数据库上下文继承
由于与位置数据打交道,所以要创建一个 LocationDbContext 类
using Microsoft.EntityFrameworkCore;
using StatlerWaldorfCorp.LocationService.Models;
using Npgsql.EntityFrameworkCore.PostgreSQL;namespace StatlerWaldorfCorp.LocationService.Persistence
{public class LocationDbContext : DbContext{public LocationDbContext(DbContextOptions<LocationDbContext> options) :base(options){}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.HasPostgresExtension("uuid-ossp");}public DbSet<LocationRecord> LocationRecords {get; set;}}
}
实现位置记录仓储接口
// using Microsoft.EntityFrameworkCore;using System;
using System.Linq;
using System.Collections.Generic;
using StatlerWaldorfCorp.LocationService.Models;
using Microsoft.EntityFrameworkCore;namespace StatlerWaldorfCorp.LocationService.Persistence
{public class LocationRecordRepository : ILocationRecordRepository{private LocationDbContext context;public LocationRecordRepository(LocationDbContext context){this.context = context;}public LocationRecord Add(LocationRecord locationRecord){this.context.Add(locationRecord);this.context.SaveChanges();return locationRecord;}public LocationRecord Update(LocationRecord locationRecord){this.context.Entry(locationRecord).State = EntityState.Modified;this.context.SaveChanges();return locationRecord;}public LocationRecord Get(Guid memberId, Guid recordId){return this.context.LocationRecords.FirstOrDefault(lr => lr.MemberID == memberId && lr.ID == recordId);}public LocationRecord Delete(Guid memberId, Guid recordId){LocationRecord locationRecord = this.Get(memberId, recordId);this.context.Remove(locationRecord);this.context.SaveChanges();return locationRecord;}public LocationRecord GetLatestForMember(Guid memberId){LocationRecord locationRecord = this.context.LocationRecords.Where(lr => lr.MemberID == memberId).OrderBy(lr => lr.Timestamp).Last();return locationRecord;}public ICollection<LocationRecord> AllForMember(Guid memberId){return this.context.LocationRecords.Where(lr => lr.MemberID == memberId).OrderBy(lr => lr.Timestamp).ToList();}}
}
为了实现以注入的方式获取 Postgres 数据库上下文,需要在 Startup 类的 ConfigureServices 方法里把仓储添加到依赖注入系统
public void ConfigureServices(IServiceCollection services)
{services.AddEntityFrameworkNpgsql().AddDbContext<LocationDbContext>(options =>options.UseNpgsql(Configuration));services.AddScoped<ILocationRecordRepository, LocationRecordRepository>();services.AddMvc();
}
数据库是一种后端服务
在本例中,我们准备用环境变量来覆盖由配置文件提供的默认配置
appsettings.json
{"transient": false,"postgres": {"cstr": "Host=localhost;Port=5432;Database=locationservice;Username=integrator;Password=inteword"}
}
前面实现的仓储需要一种数据库上下文才能运作,为了给位置模型创建数据库上下文,只需要创建一个类,并从 DbContext 继承
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using StatlerWaldorfCorp.LocationService.Models;
using Npgsql.EntityFrameworkCore.PostgreSQL;namespace StatlerWaldorfCorp.LocationService.Persistence
{public class LocationDbContext : DbContext{public LocationDbContext(DbContextOptions<LocationDbContext> options) :base(options){}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.HasPostgresExtension("uuid-ossp");}public DbSet<LocationRecord> LocationRecords {get; set;}}public class LocationDbContextFactory : IDbContextFactory<LocationDbContext>{public LocationDbContext Create(DbContextFactoryOptions options){var optionsBuilder = new DbContextOptionsBuilder<LocationDbContext>();var connectionString = Startup.Configuration.GetSection("postgres:cstr").Value;optionsBuilder.UseNpgsql(connectionString);return new LocationDbContext(optionsBuilder.Options);}}
}
创建了新的数据库上下文后,需要让它在依赖注入中可用,这样位置仓储才能使用它
public void ConfigureServices(IServiceCollection services)
{//var transient = Boolean.Parse(Configuration.GetSection("transient").Value);var transient = true;if (Configuration.GetSection("transient") != null) {transient = Boolean.Parse(Configuration.GetSection("transient").Value);}if (transient) {logger.LogInformation("Using transient location record repository.");services.AddScoped<ILocationRecordRepository, InMemoryLocationRecordRepository>();} else {var connectionString = Configuration.GetSection("postgres:cstr").Value;services.AddEntityFrameworkNpgsql().AddDbContext<LocationDbContext>(options =>options.UseNpgsql(connectionString));logger.LogInformation("Using '{0}' for DB connection string.", connectionString);services.AddScoped<ILocationRecordRepository, LocationRecordRepository>();}services.AddMvc();
}
让这些功能最终生效的奇妙之处在于对 AddEntityFrameworkNpgsql 以及 AddDbContext 两个方法的调用
对真实仓储进行集成测试
我们想要利用自动的构建流水线,每次运行构建时都启动一个新的、空白的 Postgres 实例
然后,让集成测试在这个新实例上运行,执行迁移以配置数据库结构
每次提交代码时,整个过程既要能在本地、团队成员的机器上运行,又要能在云上自动运行
这就是我喜欢搭配使用 Wercker 和 Docker 的原因
试运行数据服务
使用特定参数启动 Postgres
$ docker run -p 5432:5432 --name some-postgres \
-e POSTGRES_PASSWORD=inteword -e POSTGRES_USER=integrator \
-e POSTGRES_DB=locationservice -d postgres
这样就以 some-postgres 为名称启动一个 Postgres 的 Docker 镜像
为验证能够成功连接到 Postgres,可运行下面的 Docker 命令来启动 psql
$ docker run -it --rm --link some-postgres:postgres postgres \
psql -h postgres -U integrator -d locationservice
数据库启动后,还需要表结构,顺便设置了很快会用到的环境变量
$ exprot TRANSIENT=false
$ export POSTGRES__CSTR=“Host=localhost;Username=integrator; \
Password=inteword;Database=locationservice;Port=5432"
$ dotnet ef database update
我们期望位置服务能够访问到自己的容器之外,并进入 Postgres 容器之内
容器链接能够实现这项能力,不过需要在启动 Docker 镜像之前就完成环境变量的修改
$ export POSTGRES__CSTR=“Host=localhost;Username=integrator; \
Password=inteword;Database=locationservice;Port=5432"
$ docker run -p 5000:5000 --link some-postgres:psotgres \
-e TRANSIENT=false -e PORT=5000 \
-e POSTGRES__CSTR dotnetcoreservices/locationservice:latest
使用 psotgres 作为主机名链接 Postgres 容器后,位置服务就应该能够正确连接到数据库了
为亲自验证结果,可以提交一个位置记录
$ curl -H "Content-Type:application/json" -X POST -d \
'{"id":"64c3e69f-1580-4b2f-a9ff-2c5f3b8f0elf","latitude":12.0, \
"longitude":10.0,"altitude":5.0,"timestamp":0, \
"memberId":"63e7acf8-8fae-42ec-9349-3c8593ac8292"}' \
http://localhost:5000/locations/63e7acf8-8fae-42ec-9349-3c8593ac8292
通过服务查询我们虚构的团队成员历史位置
$ curl http://localhost:5000/locations/63e7acf8-8fae-42ec-9349-3c8593ac8292
为了再次确认,查询 latest 端点并确保仍能获取到期望的输出
$ curl http://localhost:5000/locations/63e7acf8-8fae-42ec-9349-3c8593ac8292/latest
最后,为了证实确实在使用真实的数据库实例,可以使用 docker ps 以及 docker kill 找到位置服务所在的 Docker 进程并终止它
然后通过之前用过的命令重新启动服务