本文来源于博客园,转载请注明出处
这篇学习笔记是2015年写的,之前在CSDN里,花了不少时间整理,不忍丢弃,便手工搬到了这里。
前言
本文是我对照英文官网的学习记录,官方的网址在里。可以看到内容分为这么几大块:
Get Started
Past & Future Versions
API Documentation
Entity Framework 6.x
Learn More About Setting Up Your Model
Learn More About Using Your Model
Using EF With Other Technologies
下面开始:
Get Started
I just want to write code (使用代码)
I am creating a new database (创建新数据库)
新建项目
这一节讲的是用Code First的方式来整个小项目。为了简便,作者直接搞了个Console项目。
第一步,创建项目。
第二步,创建实体类。
第三步,创建一个Context:我们要派生出一个Context类来,它代表了与数据库的一个会话(Session),具体来说是从System.Data.Entity.DbContext派生。
经过上面三步,整完后差不多是这个样子:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Entity;
namespace CodeFirstNewDatabaseSample
{
class Program
{
static void Main(string[] args)
{
}
}
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
}
然后:
class Program
{
static void Main(string[] args)
{
using (var db = new BloggingContext())
{
// Create and save a new Blog
Console.Write("Enter a name for a new Blog: ");
var name = Console.ReadLine();
var blog = new Blog { Name = name };
db.Blogs.Add(blog);
db.SaveChanges();
// Display all Blogs from the database
var query = from b in db.Blogs
orderby b.Name
select b;
Console.WriteLine("All blogs in the database:");
foreach (var item in query)
{
Console.WriteLine(item.Name);
}
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}
默认情况下DbContext会创建一个数据库:
- If a local SQL Express instance is available (installed by default with Visual Studio 2010) then Code First has created the database on that instance
- If SQL Express isn’t available then Code First will try and use LocalDb (installed by default with Visual Studio 2012)
- The database is named after the fully qualified name of the derived context, in our case that is CodeFirstNewDatabaseSample.BloggingContext
注:通过“SQL SERVER 对象资源管理器”=(localdb)\V11.0
简单说来上面的意思就是:如果你机器上有SQLExpress的数据库实例,Code First方式就会在该实例上创建数据库,否则就会试图使用LocalDb(可以看成一个改进的SQLExpress实例,详细的介绍)。数据库的名称会是Context的完全限定名:CodeFirstNewDatabaseSample.BloggingContext。
处理模型改变(Code Migration)
我们需要用到一种叫做Code First Migrations的特性。
1、打开程序包控制台
2、运行Enable-Migrations命令,会为当前项目生成一个Migrations文件夹,里面有两个文件,一个Configuration.cs ,这里可以为Context指定Seed。_InitialCreate.cs我觉得它就代表一次Mirgration。
现在给Blog类增加一个Url属性:
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public virtual List<Post> Posts { get; set; }
}
3、在控制台中输入Add-Migration AddUrl
Add-Migration命令会对比上一次migration检查改变,如果有改变则会列出一个新的migration,这里我们给新的migration命名为AddUrl——看到这里,我觉得这很像SVN。
每个migration都会有一个时间戳前缀。当我们运行Update-database命令,就会把所有的migration都应用到数据库中(我的猜测是:按时间顺序)。
namespace CodeFirstNewDatabaseSample.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class AddUrl : DbMigration
{
public override void Up()
{
AddColumn("dbo.Blogs", "Url", c => c.String());
}
public override void Down()
{
DropColumn("dbo.Blogs", "Url");
}
}
}
要注意,要想Migration生效——哪怕最开始的那个Migration——一定先把代码跑起来,让数据生成一下,然后再去为我们Context开启Migration,再去改变模型,再去应用Migration……如果数据库里压根儿没有库和表,则这些都是不能完成的
数据迁移的使用心得
之前我们在使用EF开发的过程中,一直有一个比较头疼的问题:一旦实体类发生了改变,就需要手动改变数据库,还需要告知其他的开发人员作相应改变。而Code First Migration带来的好处就是,数据库的变化记录到了项目代码中,小组内的其他开发人员把项目更新后,直接运行Update-database即可更新自己本地的数据库。
不过,既然是小组合作,就有可能冲突,处理冲突的方法是:
拉取最新的代码,并作了合并后,得到最新的实体,这时候要根据项目需求处理好实体的冲突,然后我们需要重新生成一个新的Add-Migration,然后Update-Database
针对已有数据库
之前我们使用EF,但都没有使用Migration特性,实体变化了,要么手动删除数据库重新跑程序,要么手动更改相应字段。现在想要针对这些程序使用上Migration特性,怎么办呢:
1、首先,在实体所在的项目中的Package-Console中,运行:Enable-Migrations
2、然后运行 Add-Migration InitialCreate –IgnoreChanges (要确保没有更改任何实体)
3、再运行Update-Database
之后就可正常进行Migrate啦:更改实体,Add-Migration,Update-Database
在实际使用中失败了,然后在Entity项目中重新安装了最新的EF,成功了。
Data Annotations & Code First Fluent API(注解和流畅API):
大部分的模型配置可以通过注解完成,而Fluent API也可以做到,并且还可以完成一些高级配置,二者可以同时使用。EF支持的注解列表:
- Index[IsUnique=true] 注意,只有6.1及以上支持
- KeyAttribute
- StringLengthAttribute
- MaxLengthAttribute
- ConcurrencyCheckAttribute
- RequiredAttribute
- TimestampAttribute
- ComplexTypeAttribute
- ColumnAttribute
- TableAttribute
- InversePropertyAttribute
- ForeignKeyAttribute
- DatabaseGeneratedAttribute
- NotMappedAttribute
要使用Fluent API,需要覆写DbContext的OnModelCreating方法。
我们先用注解:
首先添加一个User模型,并用注解指定用户名为主键,代码如下:
public class User
{
public string Username { get; set; }
public string DisplayName { get; set; }
}
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
public DbSet<User> Users { get; set; }
}
public class User
{
[Key]
public string Username { get; set; }
public string DisplayName { get; set; }
}
然后我们增加一个migration,并应用这个migration:
Add-Migration AddUser
Update-Database
两条命令都执行完后,我们就能看到最新的数据库。
我们再用Fluent API,更改在数据库表中User的一个DisplayName列:
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Property(u => u.DisplayName)
.HasColumnName("display_name");
}
}
同样,也需要两条命令才能更新数据库:
Add-Migration ChangeDisplayName
Update-Database
Learn More About Setting Up Your Model
这一大部分其实相当于把Get Started的内容往细了讲,主要分两块,一块是Code First,一块是EF designer。
Connections and Models
这节涉及的内容包括:EF是如何找到一个可用的数据库连接的,我们如何才能改变默认的行为。
通常一个EF应用程序会使用一个DbContext的派生类,该派生类会调用DbContext基类的某个构造来控制:
a、context如何连接到一个数据库,比如,如何找到并使用一个连接字符串
b、context是以code first形式生成model还是直接加载EF designer创建的model
c、额外的高级选项
如下的一些代码片段展示了DbContext构造的一些使用方法
Use Code First with connection by convention
namespace Demo.EF
{
public class BloggingContext : DbContext
{
public BloggingContext()
// C# will call base class parameterless constructor by default
{
}
}
}
在没有为程序作任何配置(尤其是数据库、数据源相关的配置)的情况下,像上面一样调用的是无参的默认构造,这样便会按照默认的Code First的方式来创建并使用一个数据库及数据库连接,在这个例子中,DbContext使用派生类的完全限定名Demo.EF.BlogginContext来作为生成的数据库的名称,并针对该数据库创建一个连接字符串(使用SQL Express或者LocalDb),SQL Express优先。默认情况下VS2010会安装一个SQL Express实例,而VS2012则是LocalDb。
Use Code First with connection by convention and specified database name
public class BloggingContext : DbContext
{
public BloggingContext()
: base("BloggingDatabase")
{
}
}
在没有给程序作任何配置的情况下,像上面一样调用了父类带参的构造函数,这会使得DbContext以Code First的模式运行,默认情况下,会创建一个连接到参数所指定的数据库的连接。在该例中,数据库的名称是BloggingDatabase。
Use Code First with connection string in app.config/web.config file
<configuration>
<connectionStrings>
<add name="BloggingCompactDatabase"
providerName="System.Data.SqlServerCe.4.0"
connectionString="Data Source=Blogging.sdf"/>
</connectionStrings>
</configuration>
上面这种方式就在告诉DbContext使用一个SQL Express 和 LocalDb之外的database server:Compact Edition database。
假设使用的是DbContext的无参构造,如果连接字符串的名称和派生类名(带不带空间前缀都行)相同,DbContext便会找到该配置中的连接字符串,如果和派生类名不同,你可以将配置中的连接字符串传给DbContext的参数:
public class BloggingContext : DbContext
{
public BloggingContext()
: base("BloggingCompactDatabase")
{
}
}
更加严格的写法是这样:
public class BloggingContext : DbContext
{
public BloggingContext()
: base("name=BloggingCompactDatabase")
{
}
}
这时,如果在配置中找不到名为BloggingCompactDatabase的连接字符串,则会抛异常。我个人喜欢这种方式。
Handling of Transaction Commit Failures(处理事务提交失败)
适用于+EF6.1。
如何处理连接失败对事务提交的影响,这篇文章介绍的很详细: SQL Database Connectivity and the Idempotency Issue。总而言之是两种情况:
1、事务提交在Server端失败了。
2、事务在Server端提交成功了但是由于连接中断客户端没有得到通过。
发生第1种情况,程序或者用户可以重新提交,发生第二种情况,就应该避免重新提交。EF的新特性可以帮助我们自动处理这些问题。
Using the feature
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.SqlServer;
public class MyConfiguration : DbConfiguration
{
public MyConfiguration()
{
SetTransactionHandler(SqlProviderServices.ProviderInvariantName, () => new CommitFailureHandler());
SetExecutionStrategy(SqlProviderServices.ProviderInvariantName, () => new SqlAzureExecutionStrategy());
}
}
关于DbConfiguration,后面会说到。
How transactions are tracked
EF是怎么做到的呢,简单来说启用了这个特性,EF会生成一个 __Transactions表,每当有新的事务,就会添加一行。需要注意的是,该表的内容可能会越来越多,需要手动不定期清理。
Code-Based Configuration (EF6 onwards)
EF程序的配置文件可以写在app.config/web.config文件中,也可以写在代码中。如果两处重复配置,则文件里的配置最终生效。
代码中的配置主要通过System.Data.Entity.Config.DbConfiguration
完成:
- 在整个程序中只创建一个DbConfiguration
- 将DbConfiguration和DbContext放在相同的程序集中
- 让DbConfiguration有一个公有无参默认构造
- 配置通过在构造中调用相关方法完成
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.SqlServer;
namespace MyNamespace
{
public class MyConfiguration : DbConfiguration
{
public MyConfiguration()
{
SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
SetDefaultConnectionFactory(new LocalDbConnectionFactory("v11.0"));
}
}
}
Creating a Model with Code First
Conventions
Type Discovery:
我们先定义好自己的 conceptual (domain) model ,通过DbContext和DbSet可以对应到表中,如果model涉及到继承呢,直接上代码:
public class SchoolEntities : DbContext
{
public DbSet<Department> Departments { get; set; }
//下面这行和上面这行,只留一行即可,也可两行都留,EF会智能地创建出相关的表
//public DbSet<Course> Courses { get; set; }
}
public class Department
{
// Primary key
public int DepartmentID { get; set; }
public string Name { get; set; }
// Navigation property
public virtual ICollection<Course> Courses { get; set; }
}
public class Course
{
// Primary key
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
// Foreign key
public int DepartmentID { get; set; }
// Navigation properties
public virtual Department Department { get; set; }
}
public partial class OnlineCourse : Course
{
public string URL { get; set; }
}
[NotMapped]
public partial class OnsiteCourse : Course
{
public string Location { get; set; }
public string Days { get; set; }
public System.DateTime Time { get; set; }
}
对于继承而言,子类的属性会添加到表中。若子类加了NotMapped则子类的属性不会添加到表中。
Primary Key Convention:
字段“ID"或者ClassName+"ID"会被看作主键。(忽略大小写)
Relationship Convention:
除了导航属性,从对象(对应的是从表)上最好还包含外键属性,如下面代码中的Course:
public class Department
{
// Primary key
public int DepartmentID { get; set; }
public string Name { get; set; }
// Navigation property
public virtual ICollection<Course> Courses { get; set; }
}
public class Course
{
// Primary key
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
// Foreign key
public int DepartmentID { get; set; }
// Navigation properties
public virtual Department Department { get; set; }
}
从类中的属性,只要类型和主类中的主键属性类型一致,且属性名满足以下三种格式:
navigation property name+principal primary key property name:如DepartmentDepartmentID
principal class name+primary key property name:如DepartmentDepartmentID,注意,这里导航属性恰好和主类名相同强调内容,倘若导航属性是Dpt,则外键名称应该是DptDepartmentID
principal primary key property name:如DepartmentID
就会自动作为从表中的外键。
倘若从对象没有显示定义外键属性,则就会以"导航属性名+主对象主键属性名"为格式生成一个外键,如:Dpt_DepartmentID(假设Course中的导航属性是Dpt)
另外,外键名称的匹配检测都是大小写无关的。
- 如果从类中的外键属性是不可空的,则Code First会在两表的关系上设置上级联删除,并且这种主从关系也是强制的,比如Course中的外键属性必须有值,否则就不设置级联删除,主从关系也不是强制的,如:course.DepartmentID = null,这就要求DepartmentID是可空的,如下面代码所示:
class Program
{
static void Main(string[] args)
{
using (var db = new TestContext())
{
db.Customer.Add(new Customer() { Name = "ddd" });
db.SaveChanges();
}
}
}
public class TestContext : DbContext
{
public TestContext():base("name=Test")
{
}
public DbSet<Customer> Customer { get; set; }
public DbSet<User> User { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
//由于这里的外键属性是可空的,所以可以直接向数据库中添加一个UserId为null的Customer
public int? UserId { get; set; }
public virtual User User { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Customer> Cusomters { get; set; }
}
- 如果在同类型对象上有多个关系,则主外键关系需要使用注解或者Fluent API来配置了。在下面的Data Annotatios的InverseProperty提到了通过注解的方法来完成配置。
Removing Conventions:
下面的代码移除了复数表名的约定
public class SchoolEntities : DbContext
{
. . .
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Configure Code First to ignore PluralizingTableName convention
// If you keep this convention, the generated tables
// will have pluralized names.
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
Data Annotations
Key:
默认情况下,EF把"
Id"或者"classNameId"来作为主键。而Key注解可以改变这一点。
Required:
对于非空类型来说,生成的数据库的对应字段是not null的,不管有没有Required注解。
NotMapped:
不言自明。
ConcurrencyCheck:
[ConcurrencyCheck, MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
public string BloggerName { get; set; }
当调用saveChanges保存对BloggerName的更新时,Id和旧值都会经你Server,Server如果发现与此同时BloggerName已经更新了,客户端就会引发一个DbUpdateConcurrencyException异常。注意上面其实一次性给BloggerName加了三个注解,这里我们关注的仅是ConcurrencyCheck。通过处理异常可以提示进行相关其他操作。
InverseProperty :
假设一篇文章有两个作者,一个作者可发表多篇文章,则这种外键关系可以这样建立:
public class Post
{
public int Id { get; set; }
public Person CreatedBy { get; set; }
public Person UpdatedBy { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
[InverseProperty("CreatedBy")]
public List<Post> PostsWritten { get; set; }
[InverseProperty("CreatedBy")]
public List<Post> PostsUpdated { get; set; }
}
Fluent API - Configuring Relationships
特殊的一对一
有一对一、一对多、多对多三种关系,对于一对一来说,主键也起到了外键的作用,所以没有另外的外键。对于一对一来说下面的代码依赖convention是不能正常工作的:
public class Member
{
public int Id { get; set; }
public String MbrName { get; set; }
public virtual Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public virtual Member Member { get; set; }
public String Addr { get; set; }
}
运行会抛异常:无法确定类型“Address”与“Member”之间的关联的主体端。必须使用关系 Fluent API 或数据注释显式配置此关联的主体端。
正确的代码:
public class SchoolDb : DbContext
{
public DbSet<Member> Members { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder
.Entity<Address>()
.HasKey(t => t.MemberId);
modelBuilder
.Entity<Member>()
.HasRequired(t => t.Address)
.WithRequiredPrincipal();
}
}
public class Member
{
public int Id { get; set; }
public String MbrName { get; set; }
public virtual Address Address { get; set; }
}
public class Address
{
public int MemberId { get; set; }
public String Addr { get; set; }
}
上面的代码确定了一对一中的主体表是Member,从表中不要显示定义主键,否则抛异常,EF会将外键MemberId作为主键。
概述
用fluent API配置relationship的时候,就是在EntityTypeConfiguration上调用HasRequired, HasOptional, 或者HasMany方法来指定相应Entity的relationship类型:HasRequired和HasOptional指定的是reference navigation 属性,HasMany指定的是collection navigation 属性,随后便可以使用WithRequired, WithOptional, 和WithMany 来配置反向的导航属性。这些函数都有无参重载,可以用来指定单向导航属性的基数。最后可以用HasForeignKey方法来指定外键。
Configuring a Required-to-Optional Relationship (One-to–Zero-or-One)
// Configure the primary key for the OfficeAssignment
modelBuilder.Entity<OfficeAssignment>()
.HasKey(t => t.InstructorID);
// Map one-to-zero or one relationship
modelBuilder.Entity<OfficeAssignment>()
.HasRequired(t => t.Instructor)
.WithOptional(t => t.OfficeAssignment);
由于OfficeAssignment的主键属性名未遵从约定,所以第一句先配置主键。第二句是在配置关系。
Configuring a Relationship Where Both Ends Are Required (One-to-One)
大部分情况下EF可以推断出关系中的主实体和从实体,不过,当两个实体都是必须的时候,或者两个实体都是可选的时候,EF就无法进行主从判定了,对于前者,在HasRequired之后使用WithRequiredPrinciple或者WithRequiredDependent,对于后者,在HasOptional方法后使用WithOptionalPrincipal或者WithOptionalDependent。
// Configure the primary key for the OfficeAssignment
modelBuilder.Entity<OfficeAssignment>()
.HasKey(t => t.InstructorID);
modelBuilder.Entity<Instructor>()
.HasRequired(t => t.OfficeAssignment)
.WithRequiredPrincipal(t => t.Instructor);
Configuring a Many-to-Many Relationship
下面的代码在Course和Instructor之间配置了多对多的属性,需要注意的是,中间表是依赖于约定创建的,表名是CourseInstructor,两个列名分别为Course_CourseID和Instructor_InstructorID。
modelBuilder.Entity<Course>()
.HasMany(t => t.Instructors)
.WithMany(t => t.Courses)
如果要指定中间表名和列名,则需要使用Map方法:
modelBuilder.Entity<Course>()
.HasMany(t => t.Instructors)
.WithMany(t => t.Courses)
.Map(m =>
{
m.ToTable("CourseInstructor");
m.MapLeftKey("CourseID");
m.MapRightKey("InstructorID");
});
Configuring a Relationship with One Navigation Property
单向导航指的是只能从一方导到另一方。默认情况下,EF将单向导航看成一对多。比如,你想让Instructor和OfficeAssignment有一对一的关系,只有Instructor定义了导航属性,那么fluent API的配置则是:
// Configure the primary Key for the OfficeAssignment
modelBuilder.Entity<OfficeAssignment>()
.HasKey(t => t.InstructorID);
modelBuilder.Entity<Instructor>()
.HasRequired(t => t.OfficeAssignment)
.WithRequiredPrincipal();
Configuring a Foreign Key Name That Does Not Follow the Code First Convention
modelBuilder.Entity<Course>()
.HasRequired(c => c.Department)
.WithMany(d => d.Courses)
.HasForeignKey(c => c.SomeDepartmentID);
Enum Support
using LearnEasyUIEntity;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
namespace Console
{
class Program
{
static void Main(string[] args)
{
using (var context = new EnumTestContext())
{
context.Departments.Add(new Department { Name = DepartmentNames.English });
context.SaveChanges();
var department = (from d in context.Departments
where d.Name == DepartmentNames.English
select d).FirstOrDefault();
Console.WriteLine(
"DepartmentID: {0} Name: {1}",
department.DepartmentID,
department.Name);
}
}
}
public partial class EnumTestContext : DbContext
{
public DbSet<Department> Departments { get; set; }
}
public enum DepartmentNames
{
English,
Math,
Economics
}
public partial class Department
{
public int DepartmentID { get; set; }
public DepartmentNames Name { get; set; }
public decimal Budget { get; set; }
}
}
Learn More About Using Your Model
Working with DbContext
使用建议:
- 使用using,尽量缩短DbContext的生命周期
- 在Web应用中,为每个请求new一个DbContext中
Querying/Finding Entities
使用Query:
using (var context = new BloggingContext())
{
// Query for all blogs with names starting with B
var blogs = from b in context.Blogs
where b.Name.StartsWith("B")
select b;
// Query for the Blog named ADO.NET Blog
var blog = context.Blogs
.Where(b => b.Name == "ADO.NET Blog")
.FirstOrDefault();
}
使用Find方法:
using (var context = new BloggingContext())
{
// Will hit the database
var blog = context.Blogs.Find(3);
// Will return the same instance without hitting the database
var blogAgain = context.Blogs.Find(3);
context.Blogs.Add(new Blog { Id = -1 });
// Will find the new blog even though it does not exist in the database
var newBlog = context.Blogs.Find(-1);
// Will find a User which has a string primary key
var user = context.Users.Find("johndoe1987");
}
使用Query和Find两种方式作查询,有如下不同:
使用Query每次查询都会操作数据库,就算查出来的实体们在Context中都存在。从数据库查询出来的结果集会和Context中的对象们对比,如果对象不存在则添加到Context中,如果存在就以存在的对象作为最终结果,但是不会覆写Context的对象,而且添加到Context中但是没有保存到数据库的对象不会被Query。
使用Find方法,Find以主键查询,如果Context已经有该对象,则不会发生数据库操作,而且Find会查询添加到Context但未保存到数据库的对象。
Async Query & Save
依赖于.NET4.5,在.NET4.5中,异步编程的语法简单许多:Task.Wait、await。
Relationships & Navigation Properties
在多的一方删除关系时
.NET4.0:
context.Entry(course).Reference(c => c.Department).Load();
course.Department = null;
.NET4.5:
context.Entry(course).Reference(c => c.Department).CurrentValue = null;
原生sql语句
大部分操作也可以用原生的sql语句代替,比如执行一个存储过程:
using (var context = new BloggingContext())
{
var blogs = context.Blogs.SqlQuery("dbo.GetBlogs").ToList();
}
或者作更新操作:
using (var context = new BloggingContext())
{
context.Database.SqlCommand(
"UPDATE dbo.Blogs SET Name = 'Another Name' WHERE BlogId = 1");
}
Optimistic Concurrency Patterns(乐观并发)
对EF来说,乐观并发的意思是当试图把更新保存到数据库的时候,EF总是“乐观”地认为数据并没有被别人改变过。倘若已经改变了,则会抛出异常:DbUpdateConcurrencyException。注意:只有当更新带有ConcurrencyCheck标记的属性(下例的Name属性)的时候,才会抛异常 。
捕获异常后一般来说有两种处理方式:
1、要么让数据库赢
using (var context = new BloggingContext())
{
var blog = context.Blogs.Find(1);
blog.Name = "The New ADO.NET Blog";
bool saveFailed;
do
{
saveFailed = false;
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
// Update the values of the entity that failed to save from the store
ex.Entries.Single().Reload();
}
} while (saveFailed);
}
2、 要么让客户端赢
using (var context = new BloggingContext())
{
var blog = context.Blogs.Find(1);
blog.Name = "The New ADO.NET Blog";
bool saveFailed;
do
{
saveFailed = false;
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
// Update original values from the database
var entry = ex.Entries.Single();
entry.OriginalValues.SetValues(entry.GetDatabaseValues());
}
} while (saveFailed);
}
在第二段代码中,倘若不把原始值设置为数据库最新值,则依然会抛异常,由此可以猜测EF并发控制的一些机制:把更新保存到数据库之前,先查询最新的值,若最新的值可原始值一样,则说明数据并未发生改变,那么就把此次更改存入数据库,否则就认为有其他人已经更改过,从而抛出DbUpdateConcurrencyException 异常。下面的代码的可以验证我们的猜测:
try
{
entry.Name = "suaaaack";
//只要旧值设置的和数据库中不一样就会抛异常
logDb.Entry<Fake>(entry).Property(a => a.Name).OriginalValue = "xxx";
logDb.SaveChanges();
saveFailed = false;
}
catch (DbUpdateConcurrencyException ex)
{
}
Entity Framework Automatic Detect Changes
面几个方法调用时,会检测改变:
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbChangeTracker.Entries
当在一循环中大量调用上述方法,跟踪大量实体时,检测改变会浪费性能,为了提高性能可以这样做:
using (var context = new BloggingContext())
{
try
{
context.Configuration.AutoDetectChangesEnabled = false;
// Make many calls in a loop
foreach (var blog in aLotOfBlogs)
{
context.Blogs.Add(blog);
}
}
finally
{
context.Configuration.AutoDetectChangesEnabled = true;
}
}
No-Tracking Queries
在只读的环境下,查询大量实体时,以下代码可以提高性能:
using (var context = new BloggingContext())
{
// Query for all blogs without tracking them
var blogs1 = context.Blogs.AsNoTracking();
// Query for some blogs without tracking them
var blogs2 = context.Blogs
.Where(b => b.Name.Contains(".NET"))
.AsNoTracking()
.ToList();
}
Add/Attach and Entity States
实体的状态通过EntitySate枚举来标识,共5种:
- Added:没被context 维护,但是数据库中没有。
- Unchanged:被context维护,且属性值和从数据库查询出时一样。
- Modified:和Unchanged不同的是,属性值和从数据库查询出来的时候,发生了变化。
- Deleted:和Unchanged不同的是,该实体被标记为删除,SaveChanges的时候,会从数据库中删除掉。
- Detached:从context中分离出来,context不再追踪它了。
三种添加新Entity的方式:
using (var context = new BloggingContext())
{
var blog = new Blog { Name = "ADO.NET Blog" };
context.Blogs.Add(blog);
context.SaveChanges();
}
using (var context = new BloggingContext())
{
var blog = new Blog { Name = "ADO.NET Blog" };
context.Entry(blog).State = EntityState.Added;
context.SaveChanges();
}
using (var context = new BloggingContext())
{
// Add a new User by setting a reference from a tracked Blog
var blog = context.Blogs.Find(1);
blog.Owner = new User { UserName = "johndoe1987" };
// Add a new Post by adding to the collection of a tracked Blog
var blog = context.Blogs.Find(2);
blog.Posts.Add(new Post { Name = "How to Add Entities" });
context.SaveChanges();
}
Insert/Update模式:
public void InsertOrUpdate(Blog blog)
{
using (var context = new BloggingContext())
{
context.Entry(blog).State = blog.BlogId == 0 ?
EntityState.Added :
EntityState.Modified;
context.SaveChanges();
}
}
Working with Transactions
EF对事务的默认处理:
如果没有事务,SaveChanges()会开启事务,执行完毕之后就关闭了。如果显示使用了事务,则SaveChanges执行后不会直接入库,会使用该事务。类似的ExecuteSqlCommand和EntityFramework.Extended的批量更新和删除也是如此。
事务的隔离级别则是 database provider 默认的隔离级别,比如,在SQL Server上,隔离级别是:Read Committed,意思是:一个事务内,可以读到另一个事务已经提交的数据,这是否成为一个问题,和项目需求有关,一般来说,这不会是问题,而且EF对此也提供了并发处理机制ConcurrencyCheck。
对于查询,EF是不开启事务的。
EF的这些默认处理足以应付大多数开发需求。
显式使用事务:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void StartOwnTransactionWithinContext()
{
using (var context = new BloggingContext())
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
context.Database.ExecuteSqlCommand(
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'"
);
var query = context.Posts.Where(p => p.Blog.Rating >= 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
dbContextTransaction.Commit();
}
catch (Exception)
{
dbContextTransaction.Rollback();
}
}
}
}
}
}
BeginTransaction()方法会按需打开数据库链接。
跨库的事务:
上面所说的事务,都是针对同一个数据库,如果操作多个数据库该怎么使用事务呢?
这便是所谓的分布式事务,它并不是EF的一部分。待以后研究一下ADO.NET再说。
本文来源于博客园,转载请注明出处