提示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。