这将主要适用于无法通过soa访问数据的asp.net应用程序。这意味着尽管某些建议仍然适用,但您可以访问从框架加载的对象,而不是传输对象。
这是一个社区帖子,因此请根据需要添加。
适用于:Visual Studio 2008 sp1附带的实体框架1.0。
为什么首先选择EF?
考虑到这是一个存在很多问题的年轻技术(请参阅下文),因此要想在项目中使用EF潮流可能是一件很难的事。但是,这是Microsoft推动的技术(以EF的子集Linq2Sql为代价)。此外,您可能对NHibernate或其他解决方案不满意。无论出于何种原因,都有很多人(包括我在内)与EF合作,而且生活还不错。
EF和继承
第一个大主题是继承。EF确实支持以两种方式持久化的继承类的映射:每个类表和表层次结构。建模很容易,并且该部分没有编程问题。
(以下内容适用于每个类模型的表,因为我没有使用每个层次结构的表的经验,这是有限的。)真正的问题是当您尝试运行包含一个或多个对象的查询时出现的继承树:生成的sql令人难以置信,它需要很长时间才能被EF解析,并且也需要很长时间执行。这是一个真正的表演塞子。足够的EF可能不应与继承一起使用,或尽可能少地使用。
这是一个多么糟糕的例子。我的EF模型有大约30个类,其中大约10个是继承树的一部分。运行查询以从Base类中获取一项(类似于Base.Get(id)一样简单)时,生成的SQL超过50,000个字符。然后,当您尝试返回一些关联时,它甚至会退化更多,甚至会引发SQL异常,即无法一次查询超过256个表。
好的,这很不好,EF概念是允许您创建对象结构而无需(或尽可能少地)考虑表的实际数据库实现。这完全失败了。
那么,建议呢?如果可以的话,避免继承,性能会好得多。尽量在需要的地方使用它。在我看来,这使EF成为了美化的sql生成查询工具,但是使用它仍然有很多优势。和实现机制的方式类似于继承。
用接口绕过继承
尝试获得某种与EF的继承关系时要了解的第一件事是,您不能为非EF建模的类分配基类。甚至不要尝试,建模者会覆盖它。那么该怎么办?
您可以使用接口来强制类实现某些功能。例如,这里有一个IEntity接口,可让您定义EF实体之间的关联,而在设计时您不知道实体的类型。
public enum EntityTypes{ Unknown = -1, Dog = 0, Cat }
public interface IEntity
{
int EntityID { get; }
string Name { get; }
Type EntityType { get; }
}
public partial class Dog : IEntity
{
// implement EntityID and Name which could actually be fields
// from your EF model
Type EntityType{ get{ return EntityTypes.Dog; } }
}
使用此IEntity,然后可以在其他类中使用未定义的关联
// lets take a class that you defined in your model.
// that class has a mapping to the columns: PetID, PetType
public partial class Person
{
public IEntity GetPet()
{
return IEntityController.Get(PetID,PetType);
}
}
它利用了一些扩展功能:
public class IEntityController
{
static public IEntity Get(int id, EntityTypes type)
{
switch (type)
{
case EntityTypes.Dog: return Dog.Get(id);
case EntityTypes.Cat: return Cat.Get(id);
default: throw new Exception("Invalid EntityType");
}
}
}
不像普通继承那样整洁,特别是考虑到必须将PetType存储在额外的数据库字段中,但是考虑到性能的提高,我不会回头。
它也不能建立一对多,多对多关系的模型,但是可以创造性地使用“联盟”来使它起作用。最后,它会在对象的属性/功能中产生将数据加载到侧面的副作用,您需要注意这一点。在这方面,使用诸如GetXYZ()这样的明确命名约定很有帮助。
编译查询
实体框架的性能不如使用ADO(显然)或Linq2SQL进行直接数据库访问好。但是有多种方法可以改进它,其中之一就是编译查询。编译查询的性能类似于Linq2Sql。
什么是编译查询?这只是一个查询,您可以告诉框架将已解析的树保留在内存中,因此下次运行它时无需重新生成它。因此,下次运行时,您将节省解析树所需的时间。不要小看它,因为这是一个非常昂贵的操作,在更复杂的查询中会变得更糟。
有两种编译查询的方法:使用EntitySQL创建ObjectQuery和使用CompiledQuery.Compile()函数。(请注意,通过在页面中使用EntityDataSource,实际上您将在EntitySQL中使用ObjectQuery,以便对其进行编译和缓存)。
如果您不知道EntitySQL是什么,请放在此处。这是针对EF编写查询的基于字符串的方法。这是一个示例:“从Entities.DogSet中选择值dog作为dog,其中dog.ID = @ID”。该语法与SQL语法非常相似。您还可以执行非常复杂的对象操作,这在[here] [1]中有很好的解释。
好的,这是使用ObjectQuery <>的方法
string query = "select value dog " +
"from Entities.DogSet as dog " +
"where dog.ID = @ID";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance));
oQuery.Parameters.Add(new ObjectParameter("ID", id));
oQuery.EnablePlanCaching = true;
return oQuery.FirstOrDefault();
首次运行此查询时,框架将生成表达式树并将其保存在内存中。因此,下次执行该代码时,您将节省该昂贵的步骤。在该示例中,EnablePlanCaching = true,这是默认选项,因此不必要。
编译查询以供以后使用的另一种方法是CompiledQuery.Compile方法。这使用一个委托:
static readonly Func<Entities, int, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
ctx.DogSet.FirstOrDefault(it => it.ID == id));
或使用linq
static readonly Func<Entities, int, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
(from dog in ctx.DogSet where dog.ID == id select dog).FirstOrDefault());
调用查询:
query_GetDog.Invoke( YourContext, id );
CompiledQuery的优点是查询的语法在编译时检查,而EntitySQL则没有。但是,还有其他考虑...
包括
假设您希望查询返回狗主人的数据,以避免对数据库进行两次调用。容易做到,对吧?
实体SQL
string query = "select value dog " +
"from Entities.DogSet as dog " +
"where dog.ID = @ID";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance)).Include("Owner");
oQuery.Parameters.Add(new ObjectParameter("ID", id));
oQuery.EnablePlanCaching = true;
return oQuery.FirstOrDefault();
编译查询
static readonly Func<Entities, int, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, Dog>((ctx, id) =>
(from dog in ctx.DogSet.Include("Owner") where dog.ID == id select dog).FirstOrDefault());
现在,如果要对包含参数进行参数化怎么办?我的意思是,您希望有一个从不同页面调用的Get()函数,该函数关心狗的不同关系。一个在乎所有者,另一个在乎他的最爱食品,另一个在乎他的FavotireToy,等等。基本上,您想告诉查询要加载哪些关联。
使用EntitySQL很容易
public Dog Get(int id, string include)
{
string query = "select value dog " +
"from Entities.DogSet as dog " +
"where dog.ID = @ID";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>(query, EntityContext.Instance))
.IncludeMany(include);
oQuery.Parameters.Add(new ObjectParameter("ID", id));
oQuery.EnablePlanCaching = true;
return oQuery.FirstOrDefault();
}
包含仅使用传递的字符串。很简单。请注意,可以使用IncludeMany(string)改进Include(string)函数(仅接受单个路径),该函数可以让您传递以逗号分隔的关联字符串以进行加载。在扩展部分中进一步寻找该功能。
但是,如果尝试使用CompiledQuery进行操作,则会遇到许多问题:
明显的
static readonly Func<Entities, int, string, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, string, Dog>((ctx, id, include) =>
(from dog in ctx.DogSet.Include(include) where dog.ID == id select dog).FirstOrDefault());
调用时将窒息:
query_GetDog.Invoke( YourContext, id, "Owner,FavoriteFood" );
因为,如上所述,Include()只希望看到字符串中的单个路径,所以在这里给它2:“ Owner”和“ FavoriteFood”(不要与“ Owner.FavoriteFood”相混淆!)。
然后,让我们使用IncludeMany(),它是一个扩展函数
static readonly Func<Entities, int, string, Dog> query_GetDog =
CompiledQuery.Compile<Entities, int, string, Dog>((ctx, id, include) =>
(from dog in ctx.DogSet.IncludeMany(include) where dog.ID == id select dog).FirstOrDefault());
再次出错,这是因为EF无法解析IncludeMany,因为它不是可识别的函数的一部分:它是扩展。
好的,因此您想向函数传递任意数量的路径,而Includes()只需要一个路径。该怎么办?您可以决定永远不需要超过20个“包含”,并将结构中的每个单独的字符串传递给CompiledQuery。但是现在查询看起来像这样:
from dog in ctx.DogSet.Include(include1).Include(include2).Include(include3)
.Include(include4).Include(include5).Include(include6)
.[...].Include(include19).Include(include20) where dog.ID == id select dog
这也很糟糕。好的,那等一下。我们不能用CompiledQuery返回ObjectQuery <>吗?然后在其中设置包含?好吧,我也会这么想:
static readonly Func<Entities, int, ObjectQuery<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, int, string, ObjectQuery<Dog>>((ctx, id) =>
(ObjectQuery<Dog>)(from dog in ctx.DogSet where dog.ID == id select dog));
public Dog GetDog( int id, string include )
{
ObjectQuery<Dog> oQuery = query_GetDog(id);
oQuery = oQuery.IncludeMany(include);
return oQuery.FirstOrDefault;
}
那应该行得通,只是当您调用IncludeMany(或Include,Where,OrderBy ...)时,会使缓存的已编译查询无效,因为它现在是一个全新的查询!因此,表达式树需要重新分析,您的性能再次受到打击。
那么解决方案是什么?您根本无法将CompiledQueries与参数化的Includes一起使用。请改用EntitySQL。这并不意味着CompiledQueries没有使用。这对于始终在相同上下文中调用的本地化查询非常有用。理想情况下,应始终使用CompiledQuery,因为语法是在编译时检查的,但由于限制,这是不可能的。
一个使用示例是:您可能希望有一个页面来查询哪些两只狗有相同的喜爱食物,对于BusinessLayer函数来说这有点狭窄,因此您将其放在页面中并确切知道包含的类型是什么需要。
将3个以上的参数传递给CompiledQuery
Func限于5个参数,其中最后一个是返回类型,而第一个是模型中的Entities对象。这样就剩下3个参数了。一点点,但可以很容易地加以改进。
public struct MyParams
{
public string param1;
public int param2;
public DateTime param3;
}
static readonly Func<Entities, MyParams, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, MyParams, IEnumerable<Dog>>((ctx, myParams) =>
from dog in ctx.DogSet where dog.Age == myParams.param2 && dog.Name == myParams.param1 and dog.BirthDate > myParams.param3 select dog);
public List<Dog> GetSomeDogs( int age, string Name, DateTime birthDate )
{
MyParams myParams = new MyParams();
myParams.param1 = name;
myParams.param2 = age;
myParams.param3 = birthDate;
return query_GetDog(YourContext,myParams).ToList();
}
返回类型(这不适用于EntitySQL查询,因为它们在执行期间没有与CompiledQuery方法同时编译)
与Linq一起使用,通常直到最后一刻才强制执行查询,以防下游的某些其他函数希望以某种方式更改查询:
static readonly Func<Entities, int, string, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, int, string, IEnumerable<Dog>>((ctx, age, name) =>
from dog in ctx.DogSet where dog.Age == age && dog.Name == name select dog);
public IEnumerable<Dog> GetSomeDogs( int age, string name )
{
return query_GetDog(YourContext,age,name);
}
public void DataBindStuff()
{
IEnumerable<Dog> dogs = GetSomeDogs(4,"Bud");
// but I want the dogs ordered by BirthDate
gridView.DataSource = dogs.OrderBy( it => it.BirthDate );
}
这里会发生什么?通过仍然玩原始的ObjectQuery(这是实现IEnumerable的Linq语句的实际返回类型),它将使编译后的查询无效并被强制重新解析。因此,经验法则是返回对象的List <>。
static readonly Func<Entities, int, string, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, int, string, IEnumerable<Dog>>((ctx, age, name) =>
from dog in ctx.DogSet where dog.Age == age && dog.Name == name select dog);
public List<Dog> GetSomeDogs( int age, string name )
{
return query_GetDog(YourContext,age,name).ToList(); //<== change here
}
public void DataBindStuff()
{
List<Dog> dogs = GetSomeDogs(4,"Bud");
// but I want the dogs ordered by BirthDate
gridView.DataSource = dogs.OrderBy( it => it.BirthDate );
}
当您调用ToList()时,查询将按照编译后的查询执行,然后,稍后对内存中的对象执行OrderBy。它可能会慢一点,但我什至不确定。可以肯定的一点是,您不必担心错误处理ObjectQuery并使编译后的查询计划无效。
再一次,这不是一个笼统的声明。ToList()是一种防御性编程技巧,但是如果您有充分的理由不使用ToList(),请继续。在许多情况下,您需要在执行查询之前对其进行优化。
性能
编译查询对性能有何影响?它实际上可能很大。一条经验法则是,对查询进行编译和缓存以进行重用至少需要简单地执行而不进行缓存的时间的两倍。对于复杂的查询(请阅读inherirante),我看到的时间最多为10秒。
因此,第一次调用预编译查询时,您会遇到性能下降的情况。在第一次命中之后,性能明显优于相同的非预编译查询。几乎与Linq2Sql相同
当您第一次加载带有预编译查询的页面时,您将获得成功。它将在5到15秒钟内加载(显然,将调用多个预编译查询),而随后的加载将花费不到300毫秒。巨大的差异,由您决定第一个用户点击是否可以,或者您希望脚本调用您的页面以强制查询的编译。
可以缓存该查询吗?
{
Dog dog = from dog in YourContext.DogSet where dog.ID == id select dog;
}
不,临时的Linq查询不会被缓存,并且每次调用它都会产生生成树的开销。
参数化查询
大多数搜索功能都包含大量参数化的查询。甚至还有可用的库,这些库可让您根据lamba表达式构建参数化查询。问题是您不能将预编译查询与这些查询一起使用。一种解决方法是在查询中映射所有可能的条件并标记要使用的条件:
public struct MyParams
{
public string name;
public bool checkName;
public int age;
public bool checkAge;
}
static readonly Func<Entities, MyParams, IEnumerable<Dog>> query_GetDog =
CompiledQuery.Compile<Entities, MyParams, IEnumerable<Dog>>((ctx, myParams) =>
from dog in ctx.DogSet
where (myParams.checkAge == true && dog.Age == myParams.age)
&& (myParams.checkName == true && dog.Name == myParams.name )
select dog);
protected List<Dog> GetSomeDogs()
{
MyParams myParams = new MyParams();
myParams.name = "Bud";
myParams.checkName = true;
myParams.age = 0;
myParams.checkAge = false;
return query_GetDog(YourContext,myParams).ToList();
}
这样做的好处是,您可以获得预编译的quert的所有好处。缺点是您很可能会以很难维护的where子句结尾,对预编译查询将招致更大的损失,并且您运行的每个查询的效率都不如预期(特别是与加入的连接)。
另一种方法是逐步构建EntitySQL查询,就像我们对SQL所做的一样。
protected List<Dod> GetSomeDogs( string name, int age)
{
string query = "select value dog from Entities.DogSet where 1 = 1 ";
if( !String.IsNullOrEmpty(name) )
query = query + " and dog.Name == @Name ";
if( age > 0 )
query = query + " and dog.Age == @Age ";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>( query, YourContext );
if( !String.IsNullOrEmpty(name) )
oQuery.Parameters.Add( new ObjectParameter( "Name", name ) );
if( age > 0 )
oQuery.Parameters.Add( new ObjectParameter( "Age", age ) );
return oQuery.ToList();
}
这里的问题是:-编译期间没有语法检查-参数的每种不同组合都会生成不同的查询,首次运行时需要对其进行预编译。在这种情况下,只有4种不同的可能查询(无参数,仅年龄,仅名称和两个参数),但是您可以看到,正常的世界搜索可以提供更多的方法。-没有人喜欢连接字符串!
另一种选择是查询大量数据子集,然后将其缩小到内存中。如果您要处理数据的确定子集(例如城市中的所有狗),则此功能特别有用。您知道有很多,但也没有很多...因此,您的CityDog搜索页可以在内存中加载该城市的所有狗,这是一个预编译的查询,然后优化结果
protected List<Dod> GetSomeDogs( string name, int age, string city)
{
string query = "select value dog from Entities.DogSet where dog.Owner.Address.City == @City ";
ObjectQuery<Dog> oQuery = new ObjectQuery<Dog>( query, YourContext );
oQuery.Parameters.Add( new ObjectParameter( "City", city ) );
List<Dog> dogs = oQuery.ToList();
if( !String.IsNullOrEmpty(name) )
dogs = dogs.Where( it => it.Name == name );
if( age > 0 )
dogs = dogs.Where( it => it.Age == age );
return dogs;
}
当您开始显示所有数据然后进行过滤时,此功能特别有用。
问题:-如果您对子集不小心,可能会导致严重的数据传输。-您只能过滤返回的数据。这意味着,如果您不返回Dog.Owner关联,则将无法对Dog.Owner.Name进行过滤那么最好的解决方案是什么?没有 您需要选择最适合您和问题的解决方案:-当您不关心预编译查询时,请使用基于lambda的查询构建。-当对象结构不太复杂时,请使用完全定义的预编译Linq查询。-当结构可能很复杂并且不同的结果查询的可能数量较小(这意味着较少的预编译命中)时,请使用EntitySQL /字符串串联。
单例访问
处理您所有页面上的上下文和实体的最佳方法是使用单例模式:
public sealed class YourContext
{
private const string instanceKey = "On3GoModelKey";
YourContext(){}
public static YourEntities Instance
{
get
{
HttpContext context = HttpContext.Current;
if( context == null )
return Nested.instance;
if (context.Items[instanceKey] == null)
{
On3GoEntities entity = new On3GoEntities();
context.Items[instanceKey] = entity;
}
return (YourEntities)context.Items[instanceKey];
}
}
class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}
internal static readonly YourEntities instance = new YourEntities();
}
}
NoTracking,值得吗?
执行查询时,您可以告诉框架跟踪其将返回或不返回的对象。这是什么意思?启用跟踪(默认选项)后,框架将跟踪对象的状况(是否已对其进行了修改?已创建?已删除?),并且还将在从数据库中进行进一步查询时将对象链接在一起。在这里很有趣。
例如,假设ID == 2的Dog拥有一个ID == 10的所有者。
Dog dog = (from dog in YourContext.DogSet where dog.ID == 2 select dog).FirstOrDefault();
//dog.OwnerReference.IsLoaded == false;
Person owner = (from o in YourContext.PersonSet where o.ID == 10 select dog).FirstOrDefault();
//dog.OwnerReference.IsLoaded == true;
如果我们在没有跟踪的情况下进行相同的操作,结果将有所不同。
ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)
(from dog in YourContext.DogSet where dog.ID == 2 select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
Dog dog = oDogQuery.FirstOrDefault();
//dog.OwnerReference.IsLoaded == false;
ObjectQuery<Person> oPersonQuery = (ObjectQuery<Person>)
(from o in YourContext.PersonSet where o.ID == 10 select o);
oPersonQuery.MergeOption = MergeOption.NoTracking;
Owner owner = oPersonQuery.FirstOrDefault();
//dog.OwnerReference.IsLoaded == false;
跟踪非常有用,在一个完美的世界中,如果不存在性能问题,跟踪就会一直有效。但是在这个世界上,就性能而言,它是有代价的。因此,您应该使用NoTracking来加快速度吗?这取决于您打算将数据用于什么目的。
使用NoTracking查询的数据是否有可能用于在数据库中进行更新/插入/删除?如果是这样,请不要使用NoTracking,因为不会跟踪关联,这会引发异常。
在绝对没有数据库更新的页面中,可以使用NoTracking。
可以混合使用跟踪和NoTracking,但是在更新/插入/删除时需要格外小心。问题是,如果混合使用,则可能会冒着使框架尝试将NoTracking对象Attach()附加到存在相同对象的另一个副本且跟踪打开的上下文的风险。基本上,我的意思是
Dog dog1 = (from dog in YourContext.DogSet where dog.ID == 2).FirstOrDefault();
ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)
(from dog in YourContext.DogSet where dog.ID == 2 select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
Dog dog2 = oDogQuery.FirstOrDefault();
dog1和dog2是2个不同的对象,一个是被跟踪的,另一个不是。在更新/插入中使用分离的对象将强制执行Attach(),该消息将显示“请稍等,我这里已经有一个具有相同数据库密钥的对象。失败”。当您对一个对象执行Attach()操作时,它的所有层次结构也会被附加,从而在所有地方引起问题。要格外小心。
使用NoTracking可以更快
这取决于查询。有些比其他更易于跟踪。我对此没有一个简单的规则,但这会有所帮助。
那我应该在所有地方都使用NoTracking吗?
不完全是。跟踪对象有一些优势。第一个是对象已缓存,因此对该对象的后续调用将不会访问数据库。该缓存仅在YourEntities对象的生命周期内有效,如果使用上面的单例代码,则该生命周期与页面生命周期相同。一页请求==一个YourEntity对象。因此,对于同一对象的多次调用,每个页面请求将仅加载一次。(其他缓存机制可以扩展该范围)。
当您使用NoTracking并尝试多次加载同一对象时会发生什么?每次都会查询数据库,因此会产生影响。在单个页面请求中,您应该/应该多久调用一次相同的对象?当然要尽可能少,但是确实会发生。
还记得上面关于为您的协会自动连接的文章吗?NoTracking没有这些功能,因此,如果您分批加载数据,则它们之间将没有链接:
ObjectQuery<Dog> oDogQuery = (ObjectQuery<Dog>)(from dog in YourContext.DogSet select dog);
oDogQuery.MergeOption = MergeOption.NoTracking;
List<Dog> dogs = oDogQuery.ToList();
ObjectQuery<Person> oPersonQuery = (ObjectQuery<Person>)(from o in YourContext.PersonSet select o);
oPersonQuery.MergeOption = MergeOption.NoTracking;
List<Person> owners = oPersonQuery.ToList();
在这种情况下,没有狗将设置其.Owner属性。
尝试优化性能时,请记住一些注意事项。
没有延迟加载,我该怎么办?
这可以看作是变相的祝福。当然,手动加载所有内容很烦人。但是,它减少了对数据库的调用次数,并迫使您考虑何时应该加载数据。您可以在一个数据库调用中加载的负载越多越好。这始终是正确的,但现在通过EF的“功能”得以实施。
当然,您可以调用if(!ObjectReference.IsLoaded)ObjectReference.Load();。如果愿意,但是更好的做法是强制框架一次加载您需要的对象。在这里,有关参数化包含的讨论开始变得有意义。
假设您有Dog对象
public class Dog
{
public Dog Get(int id)
{
return YourContext.DogSet.FirstOrDefault(it => it.ID == id );
}
}
这是您一直使用的功能类型。它从各处调用,一旦有了Dog对象,您将在不同的功能上对它执行完全不同的操作。首先,它应该预先编译,因为您会经常调用它。其次,每个不同的页面都希望能够访问Dog数据的不同子集。有些会想要所有者,有些会喜欢收藏玩具,等等。
当然,您可以在需要时随时为每个引用调用Load()。但这每次都会生成对数据库的调用。馊主意。因此,相反,每个页面都会在首次请求Dog对象时请求其想要查看的数据:
static public Dog Get(int id) { return GetDog(entity,"");}
static public Dog Get(int id, string includePath)
{
string query = "select value o " +
" from YourEntities.DogSet as o " +