参考资料:
微软MVP杨旭教程:https://www.bilibili.com/video/BV1xa4y1v7rR?p=10
在EF Core项目中,如果想在数据库中添加视图或者存储过程或者类似的东西,不可以直接操作数据库,而应该把生成视图或生成存储过程的脚本放在一个Migration里面,让它来执行生成视图或者创建存储过程。
创建视图和存储过程
直接Add一个空的Migration,然后再修改Migration的代码。空的Migration:
public partial class AddView : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
然后使用migrationBuilder.Sql()
,把SQL脚本用@"xxxx"
包裹起来当作参数放进去。创建视图和存储过程的脚本都应该放在Up方法中,先创建视图再创建存储过程:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"CREATE VIEW [dbo].[PlayerClubView]
AS SELECT p.Id as PlayerId, p.Name as PlayerName, c.Name as ClubName
FROM [dbo].[Players] as p
INNER JOIN [dbo].[Clubs] as c
ON p.ClubId = c.Id");
migrationBuilder.Sql(
@"CREATE PROCEDURE [dbo].[RemoveGamePlayersProcedure] @playerId int = 0
AS
DELETE FROM [dbo].[GamePlayers] WHERE [PlayerId] = @playerId
RETURN 0");
}
同时Down方法中应该包含回滚的操作,如果更新失败就回滚,回滚时应该先删除存储过程再删除视图:
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"DROP PROCEDURE [dbo].[RemoveGamePlayersProcedure]");
migrationBuilder.Sql(
@"DROP VIEW [dbo].[PlayerClubView]");
}
Update-Database之后,看一下数据库:
可以看到视图和存储过程全都有了。
无主键的Entity
我们建立的实体Model基本都有主键,而且基本都是Id,但:
- .NET Core 3.1 允许无主键的Entity
- 它们不会被追踪
- 映射到没有主键的Table或者View
我们根据刚才建立的视图建立一个没有主键的模型:
public class PlayerClub
{
public int PlayerId { get; set; }
public string PlayerName { get; set; }
public string ClubName { get; set; }
}
然后把这个类添加到Context里的DbSet属性。但DbSet不识别没有主键的类,我们需要再OnModelCreating中设置一下,用HasNoKey()
方法设置PlayerClub这个Entity。但只这样设置还不足够,如果后面再添加迁移,它会认为我们想要创建PlayerClubs这样一个Table,所以要用ToView()
方法把它映射到我们前面创建的视图[dbo].[PlayerClubView]
上。需要稍微修改一下视图名,去掉中括号和dbo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.Entity<PlayerClub>()
.HasNoKey()
.ToView("PlayerClubView");
}
针对这种没有主键的Entity,查出来的结果都是无法追踪的。
然后我们像查表一样查一下视图:
var playerClubs = context.PlayerClubs.ToList();
这种查询也可以加Where查询条件:
var playerClubs = context.PlayerClubs
.Where(x => x.PlayerId > 1)
.ToList();
但如果使用DbSet的Find()
方法,在编译的时候不会报错,在运行的时候会报错。因为Find后面跟的是主键,而这个视图没有主键。
原生SQL查询
一共有两种方法,还有它们各自的异步版本:
- FromSQLRaw("SELECT" * ...")
- FromSQLRawAsync("SELECT" * ...")
- FromSQLInterpolated($"SELECT * WHERE x={var}")
- FromSQLInterpolatedAsync($"SELECT * WHERE x={var}")
第二种方法支持C#6出现的字符串插值。这几种方法都是DbSet的方法,所以只能针对DbSet来执行这个方法。最后还要调用ToList()
等方法,否则查询不会执行。
var leagues = context.Leagues
.FromSqlRaw("SELECT * FROM dbo.Leagues")
.ToList();
也可以加查询条件和Include()
等。过滤条件可以在SQL语句里面写,在外面写没有太大意义:
var clubs = context.Clubs
.FromSqlRaw("SELECT * FROM dbo.Clubs")
.Include(x => x.League)
.Include(x => x.Players)
.ThenInclude(x => x.GamePlayers)
.ToList();
原生SQL查询的要求
- 必须返回Entity类型的所有(标量)属性
也就是SELECT *,如果写列名必须一个不多一个不少,而且不包含导航属性
- 字段名和Entity的属性名匹配
- 无法包含关联的数据
指在SQL语句中无法包含关联的数据
- 只能查询已知的Entity
字符串插值
插值的部分在生成的SQL语句中也是SQL参数。
var id = 0;
var clubs = context.Clubs
.FromSqlInterpolated($"SELECT * FROM dbo.Clubs WHERE Id > {id}")
.ToList();
如果数据库中有对应的Club类的存储过程的话,Clubs.FromSqlInterpolated()
和Clubs.FromSqlRaw()
也可以执行存储过程。前提是要求存储过程返回的字段必须与Club类匹配。
执行非查询类SQL
执行非查询类SQL,包括执行非查询类的存储过程,不能使用DbSet的方法,应当使用Context的Database属性,它有下面两种方法,各自还有一个异步方法。。
- Context.Database.ExecuteSQLRaw()
- Context.Database.ExecuteSQLRawAsync()
- Context.Database.ExecuteSQLInterpolated()
- Context.Database.ExecuteSQLInterpolatedAsync()
- 无法用于查询
- 只能返回影响的行数
var count = context.Database
.ExecuteSqlRaw("EXEC dbo.RemoveGamePlayersProcedure {0}", 2);
count = context.Database
.ExecuteSqlInterpolated($"EXEC dbo.RemoveGamePlayersProcedure {2}");
可以看到无论那种方法,都使用了参数形式,因为这是非查询类SQL语句,不使用参数的话很容易被SQL注入。