參考資料:
微軟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注入。