这是较早的REST API using C# .NET 7 with InMemory Store后期的延续。在本教程中,我将扩展服务以将数据存储在MySQL Database中。我将使用Docker运行mySQL并使用相同的数据库迁移。
设置数据库服务器
我将使用Docker-Compose在Docker容器中运行MySQL。这将使我们添加更多的服务,例如,我们的REST API在例如REDIS服务器用于分布式缓存。
让我们从右键单击Visual Studio中的解决方案名称并添加新文件开始添加新文件。我喜欢将文件命名为docker-compose.dev-env.yml
,请随时按照您的要求命名。添加以下内容以添加电影REST API的数据库实例。
version: '3.7'
services:
movies.db:
image: mysql:5.7
environment:
- MYSQL_DATABASE=defaultdb
- MYSQL_ROOT_PASSWORD=Password123
volumes:
- moviesdbdata:/var/lib/postgresql/data/
ports:
- "33060:3306"
volumes:
moviesdbdata:
在docker-compose文件为位置并执行以下命令以启动数据库服务器的解决方案的根部打开一个终端。
docker-compose -f docker-compose.dev-env.yml up -d
数据库迁移
在开始使用Postgres之前,我们需要创建一个表以存储数据。我将使用出色的roundhouse数据库部署系统执行数据库迁移。
i通常会创建一个具有所有数据库迁移和执行这些迁移的工具的容器。我将迁移命名为[yyyymmdd-hhmm-migration-name.sql],但请随时使用任何命名方案,请记住该工具如何订购多个文件以运行这些迁移。我还添加了一个wait-for-db.csx
文件,我将用作数据库迁移容器的入口点。这是一个dotnet-script
文件,将使用dotnet-script运行。我已经固定了与.NET SDK 3.1兼容的版本,因为此版本roundhouse
在编写时构建了。
dockerfile运行数据库迁移
FROM mcr.microsoft.com/dotnet/sdk:3.1-alpine
ENV PATH="$PATH:/root/.dotnet/tools"
RUN dotnet tool install -g dotnet-script --version 1.1.0
RUN dotnet tool install -g dotnet-roundhouse --version 1.3.1
WORKDIR /db
# Copy all db files
COPY . .
ENTRYPOINT ["dotnet-script", "wait-for-db.csx", "--", "rh", "--silent", "--dt", "postgres", "-cs"]
CMD ["Host=movies.db;Username=postgres;Password=Password123;Database=moviesdb;Integrated Security=false;"]
对于迁移,我在db\up
文件夹下添加了以下内容。
-
20230523_1800_schema_create.sql
CREATE SCHEMA IF NOT EXISTS Movies;
-
20230523_1801_table_movies_create.sql
USE Movies;
CREATE TABLE IF NOT EXISTS Movies (
Id CHAR(36) NOT NULL UNIQUE,
Title VARCHAR(100) NOT NULL,
Director VARCHAR(100) NOT NULL,
ReleaseDate DATETIME NOT NULL,
TicketPrice DECIMAL(12, 4) NOT NULL,
CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL,
PRIMARY KEY (Id)
) ENGINE=INNODB;
在docker-compose.dev-env.yml
文件中添加以下内容以添加迁移容器并在启动时运行迁移。请记住,如果添加新迁移,则需要删除容器和movies.db.migrations
映像以在容器中添加新的迁移文件。
movies.db.migrations:
depends_on:
- movies.db
image: movies.db.migrations
build:
context: ./db/
dockerfile: Dockerfile
command: '"server=movies.db;database=defaultdb;uid=root;password=Password123;SslMode=None;"'
在docker-compose文件为位置的解决方案的根部打开一个终端,并执行以下命令以启动数据库服务器并应用迁移以创建schema和movies
表。
docker-compose -f docker-compose.dev-env.yml up -d
MySQL电影商店
我将使用Dapper-与MySqlConnector一起使用的简单对象映射器。
设置
- 让我们首先添加Nuget软件包
dotnet add package MySqlConnector --version 2.2.6
dotnet add package Dapper --version 2.0.123
- 更新
IMovieStore
并制作所有方法async
。 - 更新
Controller
以制作方法async
和await
呼叫存储方法 - 更新
InMemoryMoviesStore
以制作方法async
11
sqlhelper
我在名为SqlHelper
的Store
文件夹下添加了一个助手类。它在Sql
文件夹下加载了嵌入式资源,并带有扩展名.sql
,其中包含助手实例的类。原因是我喜欢在自己的文件中将每个SQL
查询。随意将查询直接放在方法中。
班级和构造函数
在Store
下添加一个新文件夹,我将其命名为MySql
,并添加一个名为MySqlMoviesStore.cs
的文件。此类将接受IConfiguration
作为参数,我们将使用.NET配置加载MySQL连接字符串。我们将在构造函数中初始化connectionString
和sqlHelper
成员变量。
public MySqlMoviesStore(IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("MoviesDb");
if (connectionString == null)
{
throw new InvalidOperationException("Missing [MoviesDb] connection string.");
}
this.connectionString = connectionString;
sqlHelper = new SqlHelper<MySqlMoviesStore>();
}
我已经在appsettings.json
配置文件中指定了这一点。这对于开发是可以接受的,但切勿将生产/雄鹿连接字符串放在配置文件中。这可以放在安全的金库中,例如AWS参数存储或Azure KeyVault,可以从应用程序访问。 CD管道也可以配置为从安全位置加载此值,并将运行应用程序的容器设置为环境变量。
创造
我们创建了MySqlConnection
的新实例,用于创建和执行查询的设置参数,使用Dapper
插入新记录,我们正在处理NpgsqlException
并抛出我们的自定义DuplicateKeyException
如果exjection的ErrorCode
是DuplicateKeyEntry
。
创建功能看起来像
public async Task Create(CreateMovieParams createMovieParams)
{
await using var connection = new MySqlConnection(this.connectionString);
{
var parameters = new
{
createMovieParams.Id,
createMovieParams.Title,
createMovieParams.Director,
createMovieParams.ReleaseDate,
createMovieParams.TicketPrice,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
};
try
{
await connection.ExecuteAsync(
this.sqlHelper.GetSqlFromEmbeddedResource("Create"),
parameters,
commandType: CommandType.Text);
}
catch (MySqlException ex)
{
if (ex.ErrorCode == MySqlErrorCode.DuplicateKeyEntry)
{
throw new DuplicateKeyException();
}
throw;
}
}
}
和对应的SQL查询来自Create.sql
文件
INSERT INTO Movies(
Id,
Title,
Director,
ReleaseDate,
TicketPrice,
CreatedAt,
UpdatedAt
)
VALUES (
@Id,
@Title,
@Director,
@ReleaseDate,
@TicketPrice,
@CreatedAt,
@UpdatedAt
)
请注意,列的名称和参数名称应匹配数据库up
脚本中定义的外壳。
得到所有
我们创建了MySqlConnection
的新实例,使用Dapper
执行查询,Dapper将列将列映射到属性。
public async Task<IEnumerable<Movie>> GetAll()
{
await using var connection = new MySqlConnection(this.connectionString);
return await connection.QueryAsync<Movie>(
sqlHelper.GetSqlFromEmbeddedResource("GetAll"),
commandType: CommandType.Text
);
}
和对应的SQL查询来自GetAll.sql
文件
SELECT
Id,
Title,
Director,
TicketPrice,
ReleaseDate,
CreatedAt,
UpdatedAt
FROM Movies
GetByid
我们创建了MySqlConnection
的新实例,使用Dapper
通过传递ID来执行查询,Dapper将列将列映射到属性。
public async Task<Movie?> GetById(Guid id)
{
await using var connection = new MySqlConnection(this.connectionString);
return await connection.QueryFirstOrDefaultAsync<Movie?>(
sqlHelper.GetSqlFromEmbeddedResource("GetById"),
new { id },
commandType: System.Data.CommandType.Text
);
}
和来自GetById.sql
文件的SQL
SELECT
Id,
Title,
Director,
TicketPrice,
ReleaseDate,
CreatedAt,
UpdatedAt
FROM Movies
WHERE Id = @Id
更新
我们创建了MySqlConnection
的新实例,用于查询的设置参数,并使用Dapper
执行查询以更新现有记录。
更新功能看起来像
public async Task Update(Guid id, UpdateMovieParams updateMovieParams)
{
await using var connection = new MySqlConnection(this.connectionString);
{
var parameters = new
{
Id = id,
updateMovieParams.Title,
updateMovieParams.Director,
updateMovieParams.ReleaseDate,
updateMovieParams.TicketPrice,
UpdatedAt = DateTime.UtcNow,
};
await connection.ExecuteAsync(
this.sqlHelper.GetSqlFromEmbeddedResource("Update"),
parameters,
commandType: CommandType.Text);
}
}
和对应的SQL查询来自Update.sql
文件
UPDATE Movies
SET
Title = @Title,
Director = @Director,
ReleaseDate = @ReleaseDate,
TicketPrice = @TicketPrice,
UpdatedAt = @UpdatedAt
WHERE id = @id
删除
我们创建了MySqlConnection
的新实例,使用Dapper
通过传递ID来执行查询。
public async Task Delete(Guid id)
{
await using var connection = new MySqlConnection(this.connectionString);
await connection.ExecuteAsync(
sqlHelper.GetSqlFromEmbeddedResource("Delete"),
new { id },
commandType: CommandType.Text
);
}
和对应的SQL查询来自Delete.sql
文件
DELETE
FROM Movies
WHERE Id = @Id
请注意,我们不会像我们在InMemoryMoviesStore
中所做的那样抛出RecordNotFoundException
异常,原因是试图删除具有非存在密钥的记录,这在Postgres中并不是错误。
设置依赖注入
最后一步是设置依赖项注入容器以连接新创建的商店。更新Program.cs
,如下所示
// builder.Services.AddSingleton<IMoviesStore, InMemoryMoviesStore>();
builder.Services.AddScoped<IMoviesStore, MySqlMoviesStore>();
为了简单起见,我已禁用InMemoryMoviesStore
,我们可以添加配置,并基于该决定在运行时使用哪种服务。这可能是一个很好的练习,但是我们实际上不这样做。但是,对于流量重型服务,使用中音或分布式缓存用于缓存结果以提高性能。
测试
我没有为本教程添加任何单元或集成测试,也许是以下教程。但是所有端点可以通过运行应用程序或使用Postman进行测试。
来源
演示应用程序的源代码托管在movies-api-cs存储库的GitHub上。
参考
没有特定顺序