Entity Framework 复杂类型
为了说明什么是复杂属性,先举一个例子。
public class CompanyAddress { public int ID { get; set; } public string CompanyName { get; set; } public string StreetAddress { get; set; } public string City { get; set; } public string State { get; set; } public string ZipCode { get; set; } }
public class FamilyAddress
{
public int ID { get; set; }
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
} |
上面有两个类:公司地址和家庭地址,它们有四个相同的属性:StreetAddress、City、State、ZipCode。映射到数据库中的结构如图:
这里,我们可以将这四个属性集合成一个复杂属性Address,修改后的类为:
public class CompanyAddress { public int ID { get; set; } public string CompanyName { get; set; } public Address Address { get; set; } }
public class FamilyAddress
{
public int ID { get; set; }
public Address Address { get; set; }
}
[ComplexType]
public class Address
{
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
} |
此时,所生成的数据库如图:
可以看到,两张表中仍然具有相应的地址属性信息。代码中的Address类就是复杂属性,它并不会在数据库中映射成相应的表,但我们的代码确简洁了许多。
所以如果有几个属性在几个类中都有用到,那么就可以将这几个属性集合成一个复杂类型,并在相应的类中增加这个复杂类型的属性。
Entity Framework 并发处理
什么是并发?
并发分悲观并发和乐观并发。
悲观并发:比如有两个用户A,B,同时登录系统修改一个文档,如果A先进入修改,则系统会把该文档锁住,B就没办法打开了,只有等A修改完,完全退出的时候B才能进入修改。
乐观并发:同上面的例子,A,B两个用户同时登录,如果A先进入修改紧跟着B也进入了。A修改文档的同时B也在修改。如果在A保存之后B再保存他的修改,此时系统检测到数据库中文档记录与B刚进入时不一致,B保存时会抛出异常,修改失败。
EF中如何控制并发?
Entity Framework不支持悲观并发,只支持乐观并发。
如果要对某一个表做并发处理,就在该表中加一条Timestamp类型的字段。注意,一张表中只能有一个Timestamp的字段。
Data Annotations中用Timestamp来标识设置并发控制字段,标识为Timestamp的字段必需为byte[]类型。
public class Person { public int PersonId { get; set; } public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [Timestamp] public byte[] RowVersion { get; set; } } |
Fluent API用IsRowVersion方法
modelBuilder.Entity<Person>().Property(p => p.RowVersion).IsRowVersion(); |
我们看到生成的数据库中,RowVersion是timestamp类型。
下面我们写一段代码来测试一下:
static void Main(string[] args) { var person = new Person { FirstName = "Rowan", LastName = "Miller", SocialSecurityNumber = 12345678 }; //新增一条记录,保存到数据库中 using (var con = new BreakAwayContext()) { con.People.Add(person); con.SaveChanges(); }
var firContext = new BreakAwayContext();
//取第一条记录,并修改一个字段:这里是修改了FirstName
//先不保存
var p1 = firContext.People.FirstOrDefault();
p1.FirstName = "Steven";
//再创建一个Context,同样取第一条记录,修改LastName字段并保存
using (var secContext = new BreakAwayContext())
{
var p2 = secContext.People.FirstOrDefault();
p2.LastName = "Francis";
secContext.SaveChanges();
}
try
{
firContext.SaveChanges();
Console.WriteLine(" 保存成功");
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine(ex.Entries.First().Entity.GetType().Name
+ " 保存失败");
}
Console.Read();
} |
上面我们实例化了三个DbContext,第一个增加一条记录到数据库中,第二个修改刚增加的记录但不保存,然后第三个Context也取刚新增的记录并保存,最后再保存第二个Context,结果保存失败。
可以看到我们的并发控制取到了作用。
分析EF生成的SQL语句:
exec sp_executesql N'update [dbo].[People] set [LastName] = @0 where (([PersonId] = @1) and ([RowVersion] = @2)) select [RowVersion] from [dbo].[People] where @@ROWCOUNT > 0 and [PersonId] = @1',N'@0 nvarchar(max) ,
@1 int,@2 binary(8)',@0=N'Francis',@1=1,@2=0x00000000000007D1 |
可以看到,它在取对应记录的时候把RowVersion也作为筛选条件。上面例子中的secContext保存的时候,数据库中的RowVersion字段的值就变了,所以firContext保存的时候用原来的RowVersion取值,自然就取不到相应的记录而报错。
如果我们只是要对某个字段作并发控制呢?别着急,EF也有办法。
Data Annotations中用ConcurrencyCheck来标识
public class Person { public int PersonId { get; set; } [ConcurrencyCheck] public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public byte[] RowVersion { get; set; } } |
Fluent API用IsConcurrencyToken方法
modelBuilder.Entity<Person>().Property(p => p.SocialSecurityNumber).IsConcurrencyToken(); |
上面的实体中,我们将SocialSecurityNumber(社会保险号)标识为开放式并发,也写一个类似的代码测试一下:
static void Main(string[] args) { var person = new Person { FirstName = "Rowan", LastName = "Miller", SocialSecurityNumber = 12345678 }; //新增一条记录,保存到数据库中 using (var con = new BreakAwayContext()) { con.People.Add(person); con.SaveChanges(); }
var firContext = new BreakAwayContext();
//取第一条记录,并修改SocialSecurityNumber字段
//先不保存
var p1 = firContext.People.FirstOrDefault();
p1.SocialSecurityNumber = 123;
//再创建一个Context,同样取第一条记录,
//修改SocialSecurityNumber字段并保存
using (var secContext = new BreakAwayContext())
{
var p2 = secContext.People.FirstOrDefault();
p2.SocialSecurityNumber = 456;
secContext.SaveChanges();
}
try
{
firContext.SaveChanges();
Console.WriteLine(" 保存成功");
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine(ex.Entries.First().Entity.GetType().Name
+ " 保存失败");
}
Console.Read();
} |
运行结果同样是保存失败,说明我们的并发控制起作用了。
分析一下EF执行的SQL:
exec sp_executesql N'update [dbo].[People] set [SocialSecurityNumber] = @0 where (([PersonId] = @1) and ([SocialSecurityNumber] = @2)) ',N'@0 int,@1 int,@2 int',@0=123,@1=1,@2=12345678 |
可以看到,EF将我们要并发控制的列SocialSecurityNumber也作为一个筛选条件,这样firContext保存的时候也会因为的数据库中SocialSecurityNumber值变了,取不到对应的记录而更新失败。
补充一下:如果是EDMX如何将字段设置为Concurrency。很简单,在对应的字段上右键-属性。在打开的属性窗口中有一个并发模式,你将它选择为Fixed即可。
Entity Framework 数据生成选项DatabaseGenerated
在EF中,我们建立数据模型的时候,可以给属性配置数据生成选项DatabaseGenerated,它后有三个枚举值:Identity、None和Computed。
Identity:自增长
None:不处理
Computed:表示这一列是计算列。
在EF中,如果主键是int类型,Code First生成数据库的时候会自动设置该列为自增长。但如果主键是Guid类型,我们就要手动的去设置了。
对于下面的模型,如果我们没有设置自增长,数据库中会以0来填充
public class Person { [Key] public Guid SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } |
var person = new Person { FirstName = "Rowan", LastName = "Miller", }; using (var context = new BreakAwayContext()) { context.People.Add(person); context.SaveChanges(); } |
插入第二条记录的时候就会报错了。所以,下面的模型设置才是正确的。
public class Person { [Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } |
现在我们来看看有些情况配置为None也是有用的。修改一下上面的模型。
public class Person { [Key] public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } |
我们再插入一条记录看看
var person = new Person { FirstName = "Rowan", LastName = "Miller", SocialSecurityNumber = 12345678 };
using (var context = new BreakAwayContext())
{
context.People.Add(person);
context.SaveChanges();
} |
在数据库中存储的是1,并不是我们所想要的12345678.这是为什么呢?
因为主键是SocialSecurityNumber是Int类型,Code
First在数据库中对该列作自增长处理。这时,我们想再插入自定义的SocialSecurityNumber就不行了。
所以当我们想这样做时,就应该把SocialSecurityNumber的自增长配置为None.
public class Person { [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } |
如果对属性标识为Computed,EF会认为该列是通过其它列计算得出的,不会将其持久化到数据库中。
public class Person { [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public string Name { get; set; } } |
var person = new Person { FirstName = "Rowan", LastName = "Miller", SocialSecurityNumber = 1231478, Name = "Rowan Miller", };
using (var context = new BreakAwayContext())
{
context.People.Add(person);
context.SaveChanges();
} |
查看数据库,我们看到Name并没有存储任何值。
Entity Framework Code First级联删除
使用Data Annotations:
如果我们要到一对主从表增加级联删除,则要在主表中的引用属性上增加Required关键字,如:
public class Destination { public int DestinationId { get; set; } public string Name { get; set; } public string Country { get; set; } public string Description { get; set; } public byte[] Photo { get; set; } public List<Lodging> Lodgings { get; set; } }
public class Lodging
{
public int LodgingId { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
public bool IsResort { get; set; }
public decimal MilesFromNearestAirport { get;
set; }
[Required]
public Destination Destination { get; set; }
} |
可以看到,在生成的数据库中,外键应用了级联删除规则
使用Fluent API:
modelBuilder.Entity<Lodging>().HasRequired(l => l.Destination).WithMany(d => d.Lodgings).WillCascadeOnDelete(true); |
相对应的,如果要关闭级联功能则为:
modelBuilder.Entity<Lodging>().HasRequired(l => l.Destination).WithMany(d => d.Lodgings).WillCascadeOnDelete(false); |
Entity Framework Code First在Oracle下的伪实现
为什么要说是伪实现,因为还做不到类似MsSql中那样完全的功能。Oralce中的数据库还是要我们自己手动去创建的。这里,我们舍掉了Model
First中的EDMX文件,自己在代码里面写模型与映射关系,这又有点像是Code First模型了,所以我说它是一个伪实现。真正完全的Code
First应该是要通过Oracle开发驱动来支持了。
通过EF来连接Oracle数据库,前提是要下载ODP.NET驱动。Google一下就找得到了。
模型
public class Student { public Student() { this.Teachers = new HashSet<Teacher>(); } public Guid ID { get; set; } public string Name { get; set; } public int StudnetNumber { get; set; } public bool IsMale { get; set; }
public virtual ICollection<Teacher> Teachers
{ get; set; }
}
public class Teacher
{
public Teacher()
{
this.Students = new HashSet<Student>();
}
public Guid ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students
{ get; set; }
} |
模型很简单,这里写了两个示例模型,Student 与Teacher,并且它们之间是多对多的关系。
主键的配制
Oralce数据库中主键有两种方式:后台通过数据库自己去生,比如:trigger+sequence结合的方式来产生自增长的主键;还有一种就是通过前台显示的给ID赋值,如ID=1来插入到数据库库中。如果你想通过前台自己手动来插入的话,要配置主键的自增长为NONE。
HasKey(k => k.ID); Property(p => p.ID).HasColumnName("ID").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); |
数据类型的映射
ODP.NET能够支持大多数数据类型的自动映射,比如int,datetime,string等。但对于一些特别数据类型,比如boolean,guid,Oracle数据库中并没有对应的bit类型或是GUID类型。我们要在映射的时候显示指定。
//GUID类型的映射 Property(p => p.TypeCode).HasColumnName("TYPECODE").HasColumnType("GUID"); //boolean类型的映射 Property(p => p.IsSale).HasColumnName("ISSALE").HasColumnType("odp_internal_use_type"); |
关系的映射
ODP.NET能够很好的支持一对多的映射,一对多的映射就像我们在配置MsSql数据库中一样。
多对多关系的映射:因为两个多对多的表,会产生一个中间表,用来保存这两张表的主键,这就要求EF能够获得这两张表的主键。如果这两张表的主键是在数据库中通过Sequence来生成的,EF就无法获取。所以,如果要配置表之间的多对多关系,表的主键必须是通过前台显示插入的。
HasMany(t => t.Teachers).WithMany(s => s.Students).Map(m => { m.MapLeftKey("STUDENTID"); m.MapRightKey("TEACHERID"); m.ToTable("STUDENT_TEACHER", "GYOUNG"); }); |
总的映射如下:
public class StudentConfiguration : EntityTypeConfiguration<Student> { public StudentConfiguration() { HasKey(k => k.ID); Property(p => p.ID).HasColumnName("ID").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None) //GUID类型的映射 .HasColumnType("GUID"); Property(p => p.Name).HasColumnName("NAME"); //bool类型的映射 Property(p => p.IsMale).HasColumnName("ISMALE").HasColumnType("odp_internal_use_type"); Property(p => p.StudentNumber).HasColumnName("STUDENTNUMBER");
HasMany(t => t.Teachers).WithMany(s =>
s.Students).Map(m =>
{
//LeftKey 当前表的主键
m.MapLeftKey("STUDENTID");
//RightKey 另一张表的主键
m.MapRightKey("TEACHERID");
//必须指定数据库架构名称
m.ToTable("STUDENT_TEACHER", "GYOUNG");
});
//表名+数据库架构名
ToTable("STUDENT", "GYOUNG");
}
}
public class TeacherConfiguration : EntityTypeConfiguration<Teacher>
{
public TeacherConfiguration()
{
HasKey(k => k.ID);
Property(p => p.ID).HasColumnName("ID").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnType("GUID");
Property(p => p.Name).HasColumnName("NAME");
ToTable("TEACHER", "GYOUNG");
}
} |
DbContext
public class TestContext : DbContext { public DbSet<Student> Students { get; set; } public DbSet<Teacher> Teachers { get; set; }
protected override void OnModelCreating(DbModelBuilder
modelBuilder)
{
modelBuilder.Configurations.Add(new TeacherConfiguration()).Add(new
StudentConfiguration());
base.OnModelCreating(modelBuilder);
}
} |
附创建表的SQL:
-- Creating table 'TEACHERS' CREATE TABLE "GYOUNG"."TEACHERS" ( "ID" RAW(16) NOT NULL, "NAME" NVARCHAR2(200) NULL );
-- Creating table 'STUDENTS'
CREATE TABLE "GYOUNG"."STUDENTS"
(
"ID" RAW(16) NOT NULL,
"NAME" NVARCHAR2(200) NULL,
"ISMALE" NUMBER(10,0) NULL,
"STUDENTNUMBER" NUMBER(10,0) NULL
);
-- Creating table 'STUDENT_TEACHER'
CREATE TABLE "GYOUNG"."STUDENT_TEACHER"
(
"STUDENTID" RAW(16) NOT NULL,
"TEACHERID" RAW(16) NOT NULL
); |
忘记了配置,补贴一下:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,
EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <connectionStrings> <add name="MfTest" providerName="Oracle.DataAccess.Client"
connectionString="Data Source=Gyoung;user id=test;password=123456" /> </connectionStrings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" /> </entityFramework> <oracle.dataaccess.client> <settings> <add name="bool" value="edmmapping number(1,0)" /> <add name="byte" value="edmmapping number(3,0)" /> <add name="int16" value="edmmapping number(4,0)" /> <add name="int32" value="edmmapping number(9,0)" /> <add name="int64" value="edmmapping number(18,0)" /> </settings> </oracle.dataaccess.client> </configuration> |
MfTest就是连接字符串,和SQLSERVER差不多,并没有什么关键的设计。数据库名是Gyoung,你自己要在Oracle中tnsnames.ora文件中去配置监听地址。
让EF飞一会儿:如何用Entity Framework 6 连接Sqlite数据库
获取Sqlite
1.可以用NuGet程序包来获取,它也会自动下载EF6
2.在Sqlite官网上下载对应的版本:http://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki
注意这里面每个.net framework都有两个版本,一个带有bundle字眼,一个没有。一个安装的DLL里面包含SQLite.Interop.dll,而另一个没有。如果你运行代码的时候报
“无法加载SQLite.Interop.dll”的错误,则将安装文件中的SQLite.Interop.dll拷贝到Bin文件中即可。或是在NuGet下载的packages\System.Data.SQLite.Core.1.0.94.0\build中也有对应的程序。
示例代码
Model.cs
public class Person { public Int64 Id { get; set; } //注意要用Int64 public string FirstName { get; set; } public string LastName { get; set; } }
public class MyContext : DbContext
{
public DbSet<Person> Persons { get; set;
}
public MyContext()
: base("SqliteTest")
{
}
} |
Program.cs
static void Main(string[] args) { MyContext context = new MyContext(); var empList = context.Persons.OrderBy(c => c.FirstName).ToList(); Console.WriteLine(empList.Count);
Person people = new Person()
{
FirstName = "Hello",
LastName = "World"
};
context.Persons.Add(people);
context.SaveChanges();
Console.ReadLine();
} |
示例代码很简单,就是用EF对Person表进行新增与查看。
配置config文件
如果你是用NuGet获取Sqlite,会自动在config中配置一些相关的信息。
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,
EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <connectionStrings> <add name="SqliteTest" connectionString="data source=SqliteTest.db" providerName="System.Data.SQLite.EF6" /> </connectionStrings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <system.data> <DbProviderFactories> <add name="SQLite Data Provider" invariant="System.Data.SQLite"
description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" /> <remove invariant="System.Data.SQLite" /> <remove invariant="System.Data.SQLite.EF6" /> <add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6"
description=".NET Framework Data Provider for SQLite (Entity Framework 6)"
type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" /> </DbProviderFactories> </system.data> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v11.0" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient"
type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> <provider invariantName="System.Data.SQLite.EF6"
type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" /> </providers> </entityFramework> </configuration> |
其中数据连接串是:
<add name="SqliteTest" connectionString="data source=SqliteTest.db" providerName="System.Data.SQLite.EF6" /> |
注意提供程序集是System.Data.SQLite.EF6。
但是这个配仍然是错误的。
如果此时运行程序,会报错:
Unable to determine the provider name
for provider factory of type 'System.Data.SQLite.SQLiteFactory'.
Make sure that the ADO.NET provider is installed or
registered in the application config.
或中文错误信息:
未找到具有固定名称“System.Data.SQLite”的 ADO.NET
提供程序的实体框架提供程序。请确保在应用程序配置文件的“entityFramework”节中注册了该提供程序。
意思是EF没有找到提供System.Data.SQLite.SQLiteFactory的dll,我们看看现在config中的entityFramework节点:
<entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v11.0" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient"
type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> <provider invariantName="System.Data.SQLite.EF6"
type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" /> </providers> </entityFramework> |
有System.Data.SQLite.EF6与System.Data.SqlClient,确实没有名称为System.Data.SQLite的提供程序。这里我一直不明白为什么sqlite会去找名称为System.Data.SQLite的提供程序,因为我们在连接串中配置的provider也是System.Data.SQLite.EF6。
那我们就在EF的配置节点中增加一个名为System.Data.SQLite的provider,但type仍然是System.Data.SQLite.EF6。最终的配置如图:
红色部分是配置有变化的地方。
这里再运行程序就可以了。
注意:
1.连接串的配置。
数据连接串可以指定绝对地址,也可以指定相对地址。像我的data source=SqliteTest.db,则SqliteTest.db要在Bin文件夹中,如果是web程序可以通过Data
Source=|DataDirectory|\SqliteTest.db来配置在App_Data文件平中。
2.如果没有指定数据库中的表文件名,EF生成的SQL表都是用复数表示。就像我的程序中实体名是Person,但EF去查找的表名会是People。所以在数据库中定义的表名是People。
3.不支持CodeFirst模式,您需要自己先设计好Sqlite的表结构。
|