提示37. 怎樣進行按條件包含(Conditional Include)
問題
幾天前有人在StackOverflow上詢問怎樣進行按條件包含。
他們打算查詢一些實體(比方說Movies),並且希望積極式載入一個相關項目(比方說,Reviews),但又僅要那些匹配一些條件的reviews(如,Review.Stars==5)。
不幸的是EF的積極式載入對此沒有完整的支援,如,對於ObjectQuery<Movie>.Include(…)方法,Include或者是全部載入或者是不載入任何東西。
解決方案
但也有一種變通方法。
下面是使這個解決方案“成真”的一個樣本情境:
public class Movie
{
public int ID {get;set;}
public string Name {get;set;}
public string Genre {get;set;}
public List<Review> Reviews {get;set;}
}
public class Review
{
public int ID {get;set;}
public int Stars {get;set;}
public string Summary {get;set;}
public Movie Movie {get;set;}
public User User {get;set;}
}
想象你想檢索所有“Horror”電影以及它們所有的5星評論。
你可以這樣做:
var dbquery =
from movie in ctx.Movies
where movie.Genre == “Horror”
select new {
movie,
reviews = from review in movie.Reviews
where review.Stars == 5
select review
};
var movies = dbquery
.AsEnumerable()
.Select(m => m.movie);
現在的問題是這是如何工作的呢?
第一個查詢建立了一個匿名型別的新執行個體,其中包含了每個Horror電影及它的5星評論。
由於調用了AsEnumerable()方法,第二個查詢使用LINQ to Objects運行於記憶體中,僅是有匿名型別封裝的對象中取出movie。
並且有趣的是每個電影都包含已經載入的其5星評論。
所以這段代碼:
foreach(var movie in movies)
{
foreach(var review in movie.Reviews)
Assert(review.Rating == 5);
}
會順利通過!
這可以工作因為EF實現了一些稱作關聯組合(relationship fix-up)的東西。
Relationship fix-up確保當第二個實體進入ObjectContext時相關的對象自動被連結。
並且因為我們同時載入Movie與一個過濾後的前者的評論列表,這兩者都進入ObjectContext後,EF會確保它們自動連結,這意味著在Movie.Reviews集合中會出現匹配評論。
例如,條件包含。
在這個主題上有一些不同的合成方法:
執行兩個獨立的查詢:一個查詢Movie,一個查詢Reviews,然後讓關聯組合完成剩下的工作。
執行一個如這所示的選擇多個類型的查詢。
排序關聯 – 見提示1
一旦你理解關聯組合如何工作,你可以真正充分利用它。
Enjoy。
提示38. 怎樣在資料服務架構(.NET Data Service,開發代號Astoria)中使用Code Only
通常建立一個ADO.NET資料服務(又稱Astoria)的服務的方法是建立一個繼承自DataService<T>的類。
public class BloggingService : DataService<BloggingEntities>
如果你要在底層使用Entity Framework,你提供的T的類型必須繼承自ObjectContext。
大部分時間這可以很好的工作,但是使用CodeOnly時不可以,而上面所述就是原因。
在DataService架構內部構建了一個BloggingEntities的執行個體,並由其MetadataWorkspace得到模型。
問題是如果你使用Code-Only來配置模型,構造BloggingEntities的唯一方法是通過Code-Only ContextBuilder,而Astoria對此一無所知。
嗯…
謝天謝地這有一個很簡單的變通方法,你只需像這樣重寫DataServie<T>的CreateDataSource()方法:
protected override BloggingService CreateDataSource()
{
//Code-Only code goes here:
var contextBuilder = GetConfiguredContextBuilder();
var connection = GetSqlConnection();
return contextBuilder.Create(connection);
}
正如你所見,這相當簡單。
要點
由於效能原因,避免每次重新設定ContextBuilder花費的開銷很重要,所以GetConfiguredContextBuilder()方法應該僅建立並配置builder一次,並緩衝這個builder用於接下來的調用。
警告
這條提示僅適用於.NET 4.0 Beta2及以上版本。
Code-Only僅工作於.NET 4.0並作為一個單獨的下載(本文寫作時)。ADO.NET資料服務(又稱Astoria)*將*作為.NET 4.0的一部分發行,但並不在Beta1中,所以暫時不能在Astoria中使用CodeOnly,你不得不等待Astoria出現在.NET 4.0 Beta2中,可能你也等待Code-Only的另一個發布。
這意味著在可以嘗試這個提示前,你還需的呢古代一小段時間。
提示39. 怎樣設定重疊的關聯 – 僅EF 4.0
情境:
在EF 4中有了外部索引鍵關聯(FK Relationship),首次出現在.NET 4.0 Beta2中,所以現在有可能有一個像這樣的模型:
public class Division
{
public int DivisionID {get;set} // Primary Key
public string Name {get;set;}
public virtual List<Lawyer> Lawyers {get;set;}
public virtual List<Unit> Units {get;set;}
}
public class Lawyer
{
public int LawyerID {get;set;} // Primary Key
public int DivisionID {get;set;} // Primary Key + FK to Division
public string Name {get;set;}
public virtual Division Division {get;set;}
public virtual List<Unit> Units {get;set;}
}
public class ProductTeam
{
public int ProductID {get;set;} // Primary Key
public int? DivisionID {get;set;} // FK to Division & Lawyer
public int? LawyerID {get;set;} // FK to Lawyer
public string Name {get;set;}
public virtual Division Division {get;set;}
public virtual Lawyer Lawyer {get;set;}
}
注意Lawyer有一個由LawyerID與DivisionID組合成的複合主鍵。
當你開始操作ProductTeam類的時候有趣的事情就出現了,這個類中同時存在Lawyer與Division兩個引用及必要的FK屬性。
如果你進行這樣的操作:
var team = (from t in ctx.ProductTeams
where t.Lawyer.Name == “Fred Bloggs”
select t).FirstOrDefault();
team.Lawyer = null;
ctx.SaveChanges();
這到底做了什麼呢?
是否意味著清空team.LawyerID與team.DivisionID或僅是team.LawyerID呢?
由關係的角度看,清空任何FK屬性都足以讓關聯斷開。
嗯…
很難正確的得到使用者想要,所以EF使用了一個你可以依賴的一致的規則,而不是引入一些基於命名規範等的奇妙規則:
當使用者將一個參考關聯性設定為null時,EF會清空所有支援關聯的可空類型的FK屬性,而不管這個FK是否參與了其它關聯。
問題:
所以在這種情況下EF清空了DivisionID與LawyerID,因為它們都支援著Lawyer導覽屬性。
這意味著將Lawyer置空同時*也會*置空Division。
然而你真的要那麼做嗎?
可能是,可能不是。
解決方案:
如果你只想將Lawyer置空,你有兩個選擇:
更改模型將DivisionID這個FK變為非可空類型,這樣EF就只能將LawyerID置空,這樣到Division的關聯就被完整的保留下來。
但是一個改變模型的解決方案並不總是合適,如果Division真的也需要為可空呢?
更好的選擇是直接通過FK屬性操作關聯:
var team = (from t in ctx.ProductTeams
where t.Lawyer.Name == “Fred Bloggs”
select t).FirstOrDefault();
team.LawyerID = null;
ctx.SaveChanges();
正如期望的,DivisionID與Division會不受影響的保留下來。
提示40. 怎樣通過L2E得到展現層模型
問題:
想象你有這些實體:
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public int ID { get; set; }
public string Name { get; set; }
public virtual List<Product> Products { get; set; }
}
但在你的UI中,你想要顯示產品 id,產品名稱與產品的類別名稱。
你可能嘗試將這個查詢傳遞到展現層。
var displayData = from product in ctx.Products.Include("Category")
select product;
但是這樣你傳了一些不需要的東西,並且把UI綁定到概念性模型上產生緊耦合,所以這不是一個好注意。
你可能再嘗試一個這樣的查詢:
var displayData = from product in ctx.Products
select new {
ID = product.ID,
Name = product.Name,
CategoryName = product.Category.Name
};
但是你將很快發現你不可以將匿名型別對象傳遞給另一個方法,至少在不使用這個不友好的方法下。
解決方案:
大部分人認為LINQ to Entities只可以查詢得到Entity與匿名型別。
但是事實上它可以查詢得到任何有一個預設建構函式的非泛型型別的對象。
總之這就意味著你可以建立一個這樣的視圖類:
public class ProductView
{
public int ID { get; set; }
public string Name { get; set; }
public string CategoryName { get; set; }
}
然後這樣編寫:
var displayData = from product in ctx.Products
select new ProductView {
ID = product.ID,
Name = product.Name,
CategoryName = product.Category.Name
};
這樣將這個對象傳遞到視圖中就沒有問題了。
我自己也是剛剛發現這個方法,我曾總是假定這會失敗。
所以這是一個很好的驚喜。
提示41. 怎樣直接對資料庫執行T-SQL
有時候你會發現你需要執行一個Entity Framework不支援的查詢或命令。事實上這個問題對於大部分ORM很普遍,這也是其中大部分ORM給資料庫留了一個後門的原因。
Entity Framework同樣有一個後門…
.NET 3.5 SP1
在.NET 3.5 SP1中,你可以通過ObjectContext得到到底層資料的串連。
調用ObjectContext.Connection返回一個IdbConnection對象,但這不是我們需要的那一個,這是一個EntityConnection。而EntityConnection有一個StoreConnection屬性可以返回我們需要的對象:
var entityConn = ctx.Connection as EntityConnection;
var dbConn = entityConn.StoreConnection as SqlConnection;
一旦你有了這個connection,你就可以自由的以一般ADO.NET的方式執行一個查詢或命令:
dbConn.Open();
var cmd = new SqlCommand("SELECT * FROM PRODUCTS", dbConn );
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine("Product: ID:{0} Name:{1} CategoryID:{2}",
reader[0].ToString(),
reader[1].ToString(),
reader[2].ToString()
);
}
}
dbConn.Close();
很容易吧?
.NET 4.0
在.NET 4.0中甚至更好。有2個新方法被直接加入到OjbectContext中。
ExecuteStoreCommand(..)用於執行命令
ExecuteStoreQuery<T>(..)用於執行查詢
使用ExecuteStoreQuery<T>(..)
如果你使用ExecuteStoreQuery<T>(..),EF會為你建立T的執行個體並填充。所以你可以這樣寫:
foreach(var product in ctx.ExecuteStoreQuery<Product>(sql))
{
Console.WriteLine("Product: ID:{0} Name:{1} CategoryID:{2}",
product.Id,
product.Name,
product.CategoryId
);
}
要使這段代碼工作,查詢返回的列名必須必須與類中的屬性名稱相匹配,並且類必須有一個預設的建構函式。但是類甚至不需要為一個Entity。
所以如你有一個這樣的類:
public class ProductView
{
public int ID { get; set; }
public string Name { get; set; }
public string CategoryName { get; set; }
}
要查詢得到這個類的執行個體,你只需編寫返回ID,Name與CategoryName列的SQL即可。
如,像這樣:
string SQL = @"SELECT P.ID, P.Name, C.Name AS CategoryName
FROM Products P
JOIN Categories C
ON P.CategoryID = C.ID";
foreach (var pv in ctx.ExecuteStoreQuery<ProductView>(SQL))
{
Console.WriteLine("{0} {1} {2}",
pv.ID,
pv.Name,
pv.CategoryName
);
}
當然,這個例子只是出於示範目的,一般情況下查詢會更複雜,如,一些LINQ to Entities原生不能處理的工作。
對於這個特殊的例子,你可以很容易的使用標準LINQ to Entities代碼來完成,見提示40你將知道怎樣做。
編輯ExcuteStoreQuery<T>(..)返回的實體
如果建立的類確實為一個實體,並且你想要編輯它們,你需要多提供一些資訊:
var productToEdit = ctx.ExecuteStoreQuery<Product>(sql,
"Products",
MergeOption.PreserveChanges
).Single();
productToEdit.CategoryId = 6;
ctx.SaveChanges();
第二個參數是Product所屬的EntitySet的名稱,第三個參數告訴EF怎樣將這些Entity與可能已存在於ObjectContext中的副本合并。
如果你正確的完成了這些,調用SaveChanges()時會將對productToEdit做的更改提交回資料庫。
使用ExecuteStoreCommand():
這個非常簡單,你執行一些命令,例如一個批次更新方法,你將得到提供者內部返回的任何資料,具有代表性的是受影響的行數。
// 10% inflation day!
ctx.ExecuteStoreCommand(
"UPDATE Products SET Price = Price * 1.1"
);
這是那樣簡單。
Enjoy。