收藏 分享(赏)

使用Entity. Framework编程(3).doc

上传人:精品资料 文档编号:11127830 上传时间:2020-02-08 格式:DOC 页数:25 大小:534KB
下载 相关 举报
使用Entity. Framework编程(3).doc_第1页
第1页 / 共25页
使用Entity. Framework编程(3).doc_第2页
第2页 / 共25页
使用Entity. Framework编程(3).doc_第3页
第3页 / 共25页
使用Entity. Framework编程(3).doc_第4页
第4页 / 共25页
使用Entity. Framework编程(3).doc_第5页
第5页 / 共25页
点击查看更多>>
资源描述

1、使用 Entity. Framework 编程(3)第三章对属性使用约定和配置在第 2 章,对 Code First 的约定以及如何通过配置覆写默认约定行为进行了大致的介绍。学习了如何使用 Data Annotations 进行配置,也学习了如何使用 Fluent API 作出相同的配置,并对两者进行了对比。在本章乃至以后几章里,将深入各种用于配置模型的领域。对每个主题会看到 Code First 如何通过默认规则进行工作,也会学到如何通过 Data Annotations 和 Fluent API 来覆写这些规则。前已指出,在 Fluent API 中可以实现的很多配置在 Data Anno

2、tations 无法实现。我们会在适当的时机指出这些差异。本章专注于对类中属性的配置,以观察默认规则和配置对数据库列的影响。你将会学习到诸如如何控制字符串长度,byte 数组,数值的精度等方面的知识。你也可以学到键属性以及所谓的“开放式并发属性” 。最后,您还可以学到有关 Code First 检测一个属性是否是复杂类型(aka 值类型),如果 Code First 无法从您的域类中推断出复杂类型时,我们将都会您如何对 Code First 提供帮助以识别复杂类型。在 Code First 中使用属性在第 2 章里,您已经看到一些应用于字符串属性的规则和配置选项,在进入新的选择我们快速回顾一下

3、。Length字长Convention默认规则max (type specified by database)max(类型由数据库指定)Data AnnotationMinLength(nn)MaxLength(nn)StringLength(nn) Fluent Entity.Property(t=t.PropertyName).HasMaxLength(nn) 字长用于描述数组的长度。包括对字符串和 byte 数组。 Code First 的默认规则 string 或者 byte 数组的长度应为最大。根据不同的数据库类型确定在数据库最终的类型。对 SQL Server 而言,string

4、会生成 nvarchar(max),而 byte 数组会生成 varbinary(max).你可以覆写默认长度来设置在数据库中的实际字长。长度的最大值会在 EF 框架将更新数据存入数据库之前进行验证。如果使用 Data Annotation 来配置,还可以为数组配置MinLength(最小长度)特性。最小长度特性也会得到 EF 验证 API 的验证,但不会影响数据库。数据类型Convention默认规则The default column data type is determined by the database provider you are using. For SQL Server

5、 some example default data types are:默认的列数据类型由数据库决定,对 SQL Server 而言如下:String : nvarchar(max)Integer:intByte Array:varbinary(max)Boolen:bitData Annotation Column(TypeName=“XXX“)Fluent Entity.Property(t=t.PropertyName).HasColumnType(“XXX“)第 2 章,您已经看到了几个如何映射.Net 类型到数据库数据类型的例子。Destination 和Lodging 类包含有整

6、型,字符串,Byte 数组,布尔型变量。Code First 通知数据库选择合适的数据类型匹配每一列。由于使用的是 SQL Server 数据库,因此分别映射到 nvarchar(max),int, varbinary(max)和 bit 类型。根据您选择的配置当然也可以指定到基他类型。例如,将字符串映射到数据库的 int 数据类型,运行时 DbModelBuilder 就会抛出一个错误告知映射非法,然后会给出如何进行纠正的细节指示。可空性和必需项配置 Convention Key Properties : not null in database默认规则 键属性:在数据库中为非空Refere

7、nce Types (String, arrays): null in the database引用类型(String,数组):在数据库中可空Value Types (all numeric types, DateTime, bool, char) : not null in database值类型(所有数字类型,日期,布尔,字符):在数据库为非空Nullable Value Types : null in databaseNullable值类型(可空类型):在数据库可空Data Annotation RequiredFluent Entity.Property(t=t.PropertyNam

8、e).IsRequired默认规则约定确保非可空的.Net 类型要映射到数据库的非可空字段,除此以外,任何键属性都只能映射到非可空数据库字段。 如果你使用.Net 的泛型 Nullable指定一个值类型(如 int)为可空,将会映射到数据库的一个可空字段。在第 2 章您已看到如何使用配置指定一个属性为必须项。使用 Data Annotation 的 Required标记和 Fluent 的 IsRequired 属性都可强制 Lodging.Name 属性为必须项。在保存数据到数据库之前,EF 运行时会对必须属性进行验证;如果属性没有赋值就会抛出一个异常。另一个效果是,数据库相应字段为非空。映

9、射键Convention默认规则Properties named Id属性名为 IdProperties named TypeName + Id属性名为类型名+IdData Annotation KeyFluent Entity.HasKey(t=t.PropertyNameEF 框架要求每个实体都有一个键。这个键用于上下文以保持每个独立对象的跟踪。键是唯一的而且经常由数据库生成。Code First 默认规则作出了同样的预设。回忆一下由 Destination 和 Lodging 类生成的数据库,DestinationId 和 LodgingId 的整型字段都被标记为主键和非空字段。如果进一

10、步观察二者的列属性,你会发现这些字段是自增长的标识字段,如图 3-1 所示,这是默认规则将整型量作为主键来管理。大多数情况下,数据库中的主键不是 int 就是 GUID 类型,尽管任意类型都可以作为键属性。数据库中的主键会是多个表的组成字段,类似地,一个实体的键也是某个类中的多个属性之一。在本节结束的时候,你会看到如何配置复合键。Code First 默认规则对不合规键属性的响应如果在我们的类中我们意指的键碰巧满足 Code First 默认规则,那么一切顺利。但是如果不满足规则呢?我们向模型添加一个新类,Trip ,见代码 3-1.Trip 类没有任何满足实体键默认规则的属性,但我们的意图是

11、 Identifier 属性应该作为键。Example 3-1. The Trip class without an obvious key propertypublic class Trippublic Guid Identifier get; set; public DateTime StartDate get; set; public DateTime EndDate get; set; public decimal CostUSD get; set; 伴随这个新类,我们需要在 BrakAwayContext 中添加一个 DbSet数据集:public DbSet Trips get;

12、set; 我们再次运行程序,在尝试从类中创建模型时 DbModel Builder 抛出一个异常:在模型生成过程中检测到一个或多个验证错误: System.Data.Edm.EdmEntityType: : 实体类型“Trip“还没有定义 key。请为这个实体类型定义 Key. 由于没有找到期望的默认 Key 属性(Id 或 TripId),Code First 无法继续创建模型。需要明确的是,类型(GUID)与这个问题无关。如前所述,您可以使用任何的原始类型作为键。使用 Data Annotations 配置 KeyData Annotation 标识一个键只需要简单的一个 Key.Key

13、特性位于System.ComponentModel.DataAnnotations.dll,由于它已经被添加到了.Net4 之中,也被其他 API 所使用(如 ASP.Net MVC 使用的 Key).如果你的项目尚未包含此程序集的引用,你就不能添加它。对这个特定的特性不需要引用 EntityFramwork.dll。Keypublic Guid Identifier get; set; 在 Flurent API 中使用 HasKey 来配置 Key 属性使用 Fluent API 来配置 Key 属性与前面几个 Fluent 配置不同。这一配置直接添加到实体上。为了配置一个 key,你需要

14、使用 HasKey 方法,如代码 3-2。Example 3-2. The HasKey Fluent configuration in OnModelCreatingmodelBuilder.Entity().HasKey(t = t.Identifier)如果将代码配置进 EntityTypeConfiguration 类中,正如你在第 2 章学到的,应该开始于HasKey 或 This.HasKey(代码 3-3)Example 3-3. HasKey inside of an EntityTypeConfiguration classHasKey(t = t.Identifier)配置

15、数据库生成的属性Convention默认规则Integer keys:Identity整型键值:标识列Data Annotation DatabaseGenerated(DatabaseGeneratedOption)Fluent Entity.Property(t=t.PropertyName).HasDatabaseGeneratedOption(DatabaseGeneratedOption)在前面部分里,你已经看到默认情况整型键值会被 EF 框架生成标识字段,由数据库生成值。而我们自己创建的 Guid 型的键值怎么办?Guid 需要特殊的处置,包含在DatabaseGenerated

16、配置。为了展示,我们添加一个新方法,InsertTrip(代码 3-4)到控制台程序,然后在主模型进行调用。Example 3-4. The InsertTrip methodprivate static void InsertTrip()var trip = new TripCostUSD = 800,StartDate = new DateTime(2011, 9, 1),EndDate = new DateTime(2011, 9, 14);using (var context = new BreakAwayContext()context.Trips.Add(trip);context

17、.SaveChanges();运行程序会导致数据库卸载并增加新的 Trips 表后重新创建,如图 3-2.Identifier 是主键,唯一标识,非空列。回到本章前面的内容,你知道值类型默认是 required。在此也会看到同样的效果,StartDate,EndDtat 和 CostUSD 属性都是值类型,默认情况下,在数据库也都是非空字段。 然后在新行中我们看到 Guid 值被填充为很多个 0.如图 3-3数据库和 EF 框架都不知道我们想让他们之一为新添加的 Trips 生成一个新的 Guid。由于这个属性没有一个生成新 Guid 的逻辑方法,就会默认以 0 值填入。如果你尝试以同样的值插

18、入另一个记录,数据库会抛出一个错误,因为期待一个唯一值。当然可以配置数据库自动生成一个新的 Guid(通过设置默认值为 newid()。不管你在数据库中手动操作还希望 CodeFirst 插入此逻辑,你必须让 Code First 知道数据库将要处理 Guid.解决方案是让 Code First 知道数据库将要生成这个键值通过使用另一个annotation:DatabaseGenerated.这一配置有三个选项None,Identity 和 Computed.我们想要 Identifier 字段被标识为 Identity,才能确保数据库在加入新行时自动生成标识字段的值,正如整型类型的键值自动生

19、成一样。使用 Data Annotations 配置数据库- 生成选项修改类代码告诉 Code First 让数据库生成一个唯一的键值:Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)public Guid Identifier get; set; 当键字段为整数时,Code First 默认选择 DatabaseGeneratedOption.Identity。而对Guid,你需要显示进行配置。这是唯一一种可以通过 Identify 来配置 Code First 的数据类型。如果映射到一现有的数据库,任何在插入数据可生成值的列都可

20、以标识为 Identify.再次运行程序,如图 3-4,输出了新生成的标识。你可能对查看 SQL 语句感兴趣,代码 3-5 显示的 EF 框架发送给数据库的 INSERT 语句,其中要求数据库为 Idenfifier 属性生成 Guid 值Example 3-5. SQL for inserting a new Tripdeclare generated_keys table(Identifier uniqueidentifier)insert dbo.Trips(StartDate, EndDate, CostUSD)output inserted.Identifier into gener

21、ated_keysvalues (0, 1, 2)select t.Identifierfrom generated_keys as gjoin dbo.Trips as t on g.Identifier = t.Identifierwhere ROWCOUNT 0,N0 datetime2(7),1 datetime2(7),2 decimal(18,2),0=2011-09-01 00:00:00,1=2011-09-14 00:00:00,2=800.00DatabaseGeneratedOption 还有两个枚举值:None 和 Coumputed。下面就有一个示例证明 None 是

22、有用的。代码 3-6 显示了另一个新类,Person,SocialSecurityNumber 属性已经被配置为此类的键属性。Example 3-6. Person class with unconventional key propertyusing System.ComponentModel.DataAnnotations;namespace Modelpublic class PersonKeypublic int SocialSecurityNumber get; set; public string FirstName get; set; public string LastName

23、 get; set; 记得要在 BreakAwayContext 类中添加 DbSetpublic DbSet People get; set; 最后,将一个新方法,InsertPerson(见代码 3-7)添加到控制台程序中,在 Main 方法中调用这个方法,就会向数据库中添加一个新的 person。Example 3-7. InsertPerson methodprivate static void InsertPerson()var person = new PersonFirstName = “Rowan“,LastName = “Miller“,SocialSecurityNumbe

24、r = 12345678;using (var context = new BreakAwayContext()context.People.Add(person);context.SaveChanges();再次运行程序,让我们再看看数据库新添加的一行,如图 3-5.SocialSecurityNumber 的值是 1, 不是 12345678.为什么?由于 Code First 根据 key 是一个整型这个事实告知数据库这是一个标识字段,因此在 INSERT 语句中 EF 框架没有提供正确的 SocialSecurityNumber 的值,而是让数据库自行生成。从而在 SaveChange

25、s 完成后查看 person 实例中 SocialSecurityNumber 的值,此值已经被更新为数据库生成的值,1.为修正这一点,我们需要添加一些配置覆写默认标识规则,在这种情况下,DatabaseGeneratedOption.Identity 是不对的,应该用 None:Key, DatabaseGenerated(DatabaseGeneratedOption.None)public int SocialSecurityNumber get; set; 然后再运行程序,如图 3-6,数据库正确插入了有关数据。DatabaseGeneratedOption.Computed 用于指定

26、一个映射到数据库的字段是通过计算得到的。例如,如果有一个 FullName 字段在 People 表中,是用一个公式将 FirstName 和LastName 组合起来得到的,你就应该让 EF 框架知道以便其不会尝试存储数据到此列中。你不能指定一个公式用来计算 Code First 中列的值,因此当映射到一个现存的数据库中你只能使用 Computed。要不然,在试图创建数据库时如果遇到 Computed 配置,数据库引擎就会抛出运行时异常。使用 Fluent API 来配置数据库生成选项DatabaseGeneratedOption 可以配置为一种特殊的属性,你可以将配置附加 HasKey 后

27、面,如:modelBuilder.Entity().HasKey(t = t.Identifier).Property(t = t.Identifier).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); 或者创建一个独立的语句:modelBuilder.Entity().HasKey(p = t.SocialSecurityNumber);modelBuilder.Entity().Property(p = p.SocialSecurityNumber).HasDatabaseGeneratedOption(Datab

28、aseGeneratedOption.None); 你会注意到 DatabaseGeneratedOption 枚举位于System.ComponentModel.DataAnnotations 名称空间,在 EntityFramework.dll 中。需要在 context 类的文件头部添加 using 引用。为开放式并发环境配置时间戳或行版本字段Convention默认规则None无Data Annotation TimeStampFluent Entity.Property(t=t.PropertyName).IsRowVersion()EF 框架从第一版本开始就支持开放式并发环境。 P

29、rogramming Entity Framework 这本书的第二版在第 23 章深入探讨了开放式并发。在这里我我们教你如何配置类映射到RowVersion(或称作 TimeStamp,时间戳)字段,同时通知 EF 框架在进行更新或删除数据库操作时使用这些字段进行并发检查。使用 Code First 你需要指定一个字段使用开放式并发检查,与映射到数据库的类型无关,或者你可以进一步指定并发的字段映射到一个 TimeStamp 字段。一个类只能有一个属性可以配置为 TimeStamp 特性。RRowVersion 和 TimeStamp 是两个具有相同类型的项。Sql Server 使用Time

30、Stamp,而其他数据库使用更恰当的名称为 RowVersion.d SQL Server2008中,timestamp 数据类型也调整为 rowversion,但是大多数工具(如 Sql Server Management Studio,vs 等)仍然显示为 timestamp. Code First 的默认规则与 TimeStamp 字段默认情况下,Code First 并不识别时间戳属性,因此没有默认约定行为,获得此行为必须配置此属性。使用 Data Annotations 配置时间戳并非任何属性都可以映射到一个 timestamp 数据库类型。必须是 byte 数组才可以。配置过程很简

31、单,将 TimeStamp 特性加到 Trip 和 Personal 类中的下列属性中。Timestamppublic byte RowVersion get; set; 然后运行控制台程序,确保 InserTrip 和 InsertPerson 方法都在 Main 方法中进行调用。在数据库中你会看到新生成的 RowVersion 列(图 3-7),类型为非可空 timestamp 类型。任何时候行内数据被修改时数据库都会自动为此属性创建新值。但 TimeStamp 不仅影响数据库的映射,还会导致属性被 EF 框架视作并发的令牌。如果你使用 EDMX 文件,这就等同于设置了一个属性的 Conc

32、urrencyMode(并发模式)。EF 框架在执行插入、更新或删除数据库时,就会考虑并发字段,返回每个 INSERT 和 UPDATE 更新数据库的值,并传回到每个 UPDATE和 DELETE 的相关属性的原始位置。例 3-8 显示了当执行 InsertPerson 方法后保存设置时的 SQL 语句:Example 3-8. INSERT combined with SELECT to return new RowVersionexec sp_executesql Ninsert dbo.People(SocialSecurityNumber, FirstName, LastName)va

33、lues (0, 1, 2)select RowVersionfrom dbo.Peoplewhere ROWCOUNT 0 and SocialSecurityNumber = 0,N0 int,1 nvarchar(max) ,2 nvarchar(max) ,0=12345678,1=NRowan,2=NMiller EF 框架不仅通知数据库执行插入,而且还请求返回 RowVersion 的值。一旦属性被标记为并发,EF 就总会这样做,即使它并不是一个 timestamp 类型数据。对更新和删除语句更是如此,因为在这会有并发检查产生。我们添加一个新的方法,UpdatePerson 到程序

34、中,见代码 3-9Example 3-9. The UpdateTrip methodprivate static void UpdateTrip()using (var context = new BreakAwayContext()var trip = context.Trips.FirstOrDefault();trip.CostUSD = 750;context.SaveChanges(); 代码 3-10 显示了当调用 UpdatePerson 时的 SQL 语句:Example 3-10. UPDATE that filters on original RowVersion and

35、 returns new RowVersionexec sp_executesql Nupdate dbo.Tripsset CostUSD = 0where (Identifier = 1) and (RowVersion = 2)select RowVersionfrom dbo.Tripswhere ROWCOUNT 0 and Identifier = 1,N0 decimal(18,2),1 uniqueidentifier,2 binary(8),0=750.00,1=D1086EFE-5C5B-405D-9F09-688981BB5B41,2=0x0000000000001773

36、 注意谓词 Where 用于定位 trip 的语句被更新过滤器包括了 Identifier 和 Rowversion 两个参数。如果另外的人更改了行程就会被我们的方法检索到,由于 RowVersion 已经更改,将不会再有行匹配过滤器。更新就会失败,EF 框架会抛出 OptimisticConcurrencyException 的异常。使用 Fluent API 配置 TimeStamp/RowVersionFluent 使用 RowVersion 来配置,要指定一个 RowVersion 属性,需要将 IsRowVersion()方法附加到属性上。使用 DbModelBuilder,需要对属

37、性作如下配置:modelBuilder.Entity().Property(p = p.RowVersion).IsRowVersion();在 EnityTypeConfiguration类中配置如下:Property(p=p.RowVersion).IsRowVersion();配置并发非时间戳字段Convention默认规则None无Data AnnotationConcurrencyCheckFluent Entity.Property(t=t.PropertyName).IsConcurrencyToken()一个不太常见的方式是并发检查是通过字段为非行版本类型进行的。例如,许多数据

38、库可能并没有行版本数据类型。因此你不能指定一个行版本属性,但你仍需要对一个或多个数据库字段进行并发检查。Person 类当前使用属性 SocialSecurityNumber 作为其标识键。设想类使用了 PersionId 属性作为标识键而将 SocialSecurityNumber 简单地视作整型数据而不作为标识跟踪。在这种情况下,你可能想有一种方法避免在 SocialSecurityNum ber 进行改变时的冲突,因为在美国,每个公民的社会保险号码是唯一的。因此,如果一个一个用户编辑了一个人的记录,可能更改的 FirstName 的拼写,但同时,另外的人想更改此人的社会保险号码,前者在尝

39、试存储更改时就会遇到一个冲突。指定 SocialSecurityNumber 属性为一个并发检查字段将提供这种检查(避免这种事情发生)。使用 Data Annotations 配置开放式并发代码 3-11 显示了修改的类为 SocialSecurityNumber 配置并发检查Example 3-11. Modified Person class with a ConcurrencyCheckpublic class Personpublic int PersonId get; set; ConcurrencyCheckpublic int SocialSecurityNumber get;

40、set; public string FirstName get; set; public string LastName get; set; 例 3-12 显示了一个方法试图更新一个 Person.如果调用这个方法,就需要先调用InsertPerson 以确保数据库内存在一个 Person 数据。Example 3-12. The UpdatePerson methodprivate static void UpdatePerson()using (var context = new BreakAwayContext()var person = context.People.FirstOrD

41、efault();person.FirstName = “Rowena“;context.SaveChanges();正如您在 Trip.RowVersion 字段中看到的(代码 3-10),当一个更新或删除请求发送以数据库时,SLQ 语句(见代码 3-13)不仅查找匹配的 Key(PersonId),还要匹配原始并发字段值(SocialSecurityNumber).Example 3-13. SQL providing concurrency checking on SocialSecurityNumberexec sp_executesql Nupdate dbo.Peopleset F

42、irstName = 0where (PersonId = 1) and (SocialSecurityNumber = 2),N0 nvarchar(max) ,1 int,2 int,0=NRowena,1=1,2=12345678如果匹配没有发现(也就是说 SocialSecurityNumber 已经在数据库中变更了),更新失败抛出 OptimisticConcurrencyException 异常。使用 Fluent API 的开放式并发配置Fluent API 使用 IsConcurrencyToken 方法配置并发,并应用于属性。如代码 3-14 所示Example 3-14.

43、Configuring concurrency checking fluentlypublic class PersonConfiguration : EntityTypeConfigurationpublic PersonConfiguration()Property(p = p.SocialSecurityNumber).IsConcurrencyToken();我们为 Person 提供其自己的配置类,也就是这个新类。不要忘记在 OnModelCreating 方法中将 PersonConfiguration 添加到 modelBuilder.Configurations 集合里。映射到

44、非-Unicode 数据库类型Convention默认规则All strings map to Unicode-encoded database types所有的字符串都映射到 Unicode 数据库类型Data Annotation 不可用Fluent Entity.Property(t=t.PropertyName).IsUnicode(boolean)默认情况下,Code First 会将所有字符串都映射到数据库中的 Unicode 字符串类型。 你可以使用 IsUnicod 方法指定一个字符串是否映射到数据库 Unicode 字符串类型。下列代码添加到 LodgingConfiguat

45、ion 中告诉 Code First 不要将 Owner 属性作为 Unicode 类型:Property(l=l.Owner).IsUnicode(false);对 Decimal 固定有效位数和小数位数的影响Convention默认规则Decimals are 18, 2Data Annotation 不可用Fluent Entity.Property(t=t.PropertyName).HasPrecision(n,n)固定有效位数(一个数字中数的位数)和小数位数(小数点右侧的位数)可以使用 Fluent API进行配置,而不能用 Data Annotations 配置。为了观察其如何工

46、作,我们向 Lodging 类中添加一个新的 decmial 属性:MilesFromNearestAirport:public decimal MilesFromNearestAirport get; set; 默认设置默认情况下,固定有效位数为 18,小数位为 2,如图 3-8 所示。使用 Flurent API,可以对固定有效位和小数位进行配置,使用的是 HasPrecison 方法。即使默认值之一是想要设置的的,也需要将两个值都指定:Property(l = l.MilesFromNearestAirport).HasPrecision(8, 1);图 3-9 显示了 MilesFro

47、mNearestAirport 有效位和小数位的更改情况。在 Code First 使用复杂类型EF 框架从第一版开始就支持复杂类型。复杂类型也可视作值类型(?)可以作为附加属性添加到其他类。复杂类型与实体类型的区别在于复杂类型没有其自己的键。它是依赖于其“宿主“类型跟踪变化 和持久化。一个没有 Key 属性的类型,并且作为属性映射到一个或多个类型中, Code First 就会将其视作为复杂类型。Code First 将预设复杂类型的属性出现在宿主类型映射到数据库的表中。在 People 表中如何将 Person 中的 Address 包含进来,将 Address 的属性都映射到 Peopl

48、e表中?可以直接将所有相关属性都纳入 Person 类中,见代码 3-15:Example 3-15. Individual properties representing an address in Personpublic class Personpublic int PersonId get; set; public int SocialSecurityNumber get; set; public string FirstName get; set; public string LastName get; set; public string StreetAddress get; se

49、t; public string City get; set; public string State get; set; public string ZipCode get; set; 但在你的模型中如果使用 Address 类作为分割类,就可以简化 Person 类,如代码 3-16 所示:Example 3-16. Address type as a property of Personpublic class Addresspublic int AddressId get; set; public string StreetAddress get; set; public string City

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报