摘要
上一篇文章介绍了Fluent NHibernate基础知识。但是,Fluent NHibernate提供了一种更方便的Mapping方法称为Auto Mapping。只需在代码中定义一些Convention继承类,针对具体的属性、主键、关系、组件指定Mapping的规则,在实体类里定义简单的POCO对象就可以完成整个数据库的自动映射。Auto Mapping适合全新的系统开发,即是在系统设计时还没有数据库的时候。有点像Microsoft Entity Framework的Code First或是Model First的开发方式。
这篇文章介绍Fluent Mapping。本篇文章的代码可以到Fluent Auto Mapping下载。
1、Auto Mapping的优缺点
优点:
- 更少的Mapping代码,因为不需要大量显式定义映射关系。
- 数据库的Schema跟Model的定义更接近。
- 程序员可以把更多的精力放在特殊的映射关系上。因为定义的那些Convention已经帮你完成了大部分的工作了。
缺点:
- 因为大部分的映射都由Convention定义,不能方便地在细节上定义一些具体的映射关系。
- 对于已经存在的数据库系统,不太适合使用Auto Mapping。
2、程序演示
1)新建控制台应用程序工程Demo.Auto.Entities。
2)在新建的工程中,使用NuGet安装FluentNHibernate。
3)添加Enum、Domain文件夹和Mapping文件夹。
4)在Enum文件夹内添加文件Enums.cs。
1 namespace Demo.Auto.Entities.Enum 2 { 3 public enum CustomerCreditRating 4 { 5 Excellent, VeryVeryGood, VeryGood, Good, Neutral, Poor, Terrible 6 } 7 }
5)在Domain文件夹内添加文件Entity.cs。
1 namespace Demo.Auto.Entities.Domain 2 { 3 public abstract class Entity 4 { 5 public virtual int Id { get; private set; } 6 } 7 }
Entity类是所有实体类的基类,包含主键属性Id。
6)在Domain文件夹内添加Name.cs和Address.cs。
Name类
1 using System; 2 3 namespace Demo.Auto.Entities.Domain 4 { 5 public class Name 6 { 7 public string LastName { get; set; } 8 public string FirstName { get; set; } 9 10 public Name() { } 11 12 public Name(string firstName, string lastName) 13 { 14 if (string.IsNullOrWhiteSpace(firstName)) 15 { 16 throw new ArgumentException("First name must be defined."); 17 } 18 if (string.IsNullOrWhiteSpace(lastName)) 19 { 20 throw new ArgumentException("Last name must be defined."); 21 } 22 FirstName = firstName; 23 LastName = lastName; 24 } 25 26 public override int GetHashCode() 27 { 28 unchecked 29 { 30 var result = FirstName.GetHashCode(); 31 result = (result * 397) ^ LastName.GetHashCode(); 32 return result; 33 } 34 } 35 36 public bool Equals(Name other) 37 { 38 if (other == null) return false; 39 if (ReferenceEquals(this, other)) return true; 40 return Equals(other.FirstName, FirstName) && 41 Equals(other.LastName, LastName); 42 } 43 44 public override bool Equals(object other) 45 { 46 return Equals(other as Name); 47 } 48 } 49 }
Address类
1 namespace Demo.Auto.Entities.Domain 2 { 3 public class Address 4 { 5 public virtual string Street { get; set; } 6 public virtual string City { get; set; } 7 public virtual string Province { get; set; } 8 public virtual string Country { get; set; } 9 10 public bool Equals(Address other) 11 { 12 if (other == null) return false; 13 if (ReferenceEquals(this, other)) return true; 14 return Equals(other.Street, Street) && 15 Equals(other.City, City) && 16 Equals(other.Province, Province) && 17 Equals(other.Country, Country); 18 } 19 20 public override bool Equals(object obj) 21 { 22 return Equals(obj as Address); 23 } 24 25 public override int GetHashCode() 26 { 27 unchecked 28 { 29 var result = Street.GetHashCode(); 30 result = (result * 397) ^ (City != null ? City.GetHashCode() : 0); 31 result = (result * 397) ^ Province.GetHashCode(); 32 result = (result * 397) ^ Country.GetHashCode(); 33 return result; 34 } 35 } 36 } 37 }
Name类和Address类跟上一篇文章的Name类和Address类的代码一样,保持不变。
7)添加实体类Customer类、Product类和Order类。
Customer类
1 using Demo.Auto.Entities.Enum; 2 using System; 3 using System.Collections.Generic; 4 5 namespace Demo.Auto.Entities.Domain 6 { 7 public class Customer : Entity 8 { 9 public Customer() 10 { 11 MemberSince = DateTime.UtcNow; 12 } 13 14 public virtual Name Name { get; set; } 15 public virtual double AverageRating { get; set; } 16 public virtual int Points { get; set; } 17 public virtual bool HasGoldStatus { get; set; } 18 public virtual DateTime MemberSince { get; set; } 19 public virtual CustomerCreditRating CreditRating { get; set; } 20 public virtual Address Address { get; set; } 21 public virtual IList<Order> Orders { get; set; } 22 } 23 }
Customer类继承Entity类,继承主键属性Id。
集合属性用IList接口定义。
Product类
1 using System.Collections.Generic; 2 3 namespace Demo.Auto.Entities.Domain 4 { 5 public class Product : Entity 6 { 7 public virtual string ProductCode { get; set; } 8 9 public virtual string ProductName { get; set; } 10 11 public virtual string Description { get; set; } 12 13 public virtual IList<Order> Orders { get; set; } 14 } 15 }
Order类
1 using System; 2 using System.Collections.Generic; 3 4 namespace Demo.Auto.Entities.Domain 5 { 6 public class Order : Entity 7 { 8 public virtual DateTime Ordered { get; set; } 9 public virtual DateTime? Shipped { get; set; } 10 public virtual Address ShipTo { get; set; } 11 public virtual Customer Customer { get; set; } 12 public virtual IList<Product> Products { get; set; } 13 } 14 }
8)在Mapping文件夹下添加文件AutoMappingConfiguration。
1 using Demo.Auto.Entities.Domain; 2 using FluentNHibernate.Automapping; 3 using FluentNHibernate.Conventions; 4 using FluentNHibernate.Conventions.Instances; 5 using System; 6 using FluentNHibernate; 7 8 namespace Demo.Auto.Entities.Mapping 9 { 10 11 }
- 在namespace Demo.Auto.Entities.Mapping里添加DefaultAutomappingConfiguration的继承类AutoMappingConfiguration。设置哪些类型被映射成实体类,哪些类型被映射成组件类。
1 public class AutoMappingConfiguration : DefaultAutomappingConfiguration 2 { 3 /// <summary> 4 /// 类型是否是实体映射类型 5 /// </summary> 6 /// <param name="type"></param> 7 /// <returns></returns> 8 public override bool ShouldMap(Type type) 9 { 10 //跟Customer类在一个名称空间的所有的类都被映射 11 return type.Namespace == typeof(Customer).Namespace; 12 } 13 14 /// <summary> 15 /// 类型是否是值对象映射类型 16 /// </summary> 17 /// <param name="type"></param> 18 /// <returns></returns> 19 public override bool IsComponent(Type type) 20 { 21 //指定Address类和Name类是值对象映射类型 22 return type == typeof(Address) 23 || type == typeof(Name); 24 } 25 26 /// <summary> 27 /// 映射值对象类型属性到数据库字段名 28 /// </summary> 29 /// <param name="member">值对象属性</param> 30 /// <returns></returns> 31 public override string GetComponentColumnPrefix(Member member) 32 { 33 //映射到数据库列名的前缀为空。默认生成的组件列列名是类名+属性名。例:CustomerCity 34 return ""; 35 } 36 }
- 添加IIdConvention接口的继承类IdConvention。指定主键列列名、主键生成策略。
1 public class IdConvention : IIdConvention 2 { 3 public void Apply(IIdentityInstance instance) 4 { 5 instance.GeneratedBy.Native(); 6 } 7 }
这里指定所有的主键列的生成策略是Native的。默认的主键列名称是Id。
- 添加IPropertyConvention接口的继承类DefaultStringLengthConvention。指定一般属性的通用映射规则。
1 public class DefaultStringLengthConvention : IPropertyConvention 2 { 3 public void Apply(IPropertyInstance instance) 4 { 5 instance.Length(250); 6 } 7 }
这里指定所有string类型属性的长度是250个字符。默认是255。
- 添加IHasManyToManyConvention接口的继承类HasManyToManyConvention。指定Many-to-Many映射的一般规则。
1 public class HasManyToManyConvention : IHasManyToManyConvention 2 { 3 public void Apply(IManyToManyCollectionInstance instance) 4 { 5 //指定主键列列名是属性名+Id,例:ProductId 6 instance.Key.Column(instance.EntityType.Name + "Id"); 7 //指定外键列列名是属性名+Id,例:OrderId 8 instance.Relationship.Column(instance.Relationship.StringIdentifierForModel + "Id"); 9 10 var firstName = instance.EntityType.Name; //主表映射类属性名 11 var secondName = instance.ChildType.Name; //从表映射类属性名 12 //定义关系的中间表表名。按主表和从表属性名的字母顺序设置中间表表名。 13 //例:Product和Order,按字母顺序,字符串"Product"在"Order"之前,中间表表名设置为"ProductOrder"。 14 //控制反转只设置成只有一个方向。 15 if (StringComparer.OrdinalIgnoreCase.Compare(firstName, secondName) > 0) 16 { 17 instance.Table(string.Format("{0}{1}", firstName, secondName)); 18 instance.Not.Inverse(); //不反转 19 } 20 else 21 { 22 instance.Table(string.Format("{0}{1}", secondName, firstName)); 23 instance.Inverse(); //反转 24 } 25 //级联更新Casade,两个方向都设置成All 26 instance.Cascade.All(); 27 } 28 }
详细说明见代码中注释。
- 添加IHasManyConvention接口的继承类HasOneToManyConvention。指定One-to-Many的一般映射规则。
1 public class HasOneToManyConvention : IHasManyConvention 2 { 3 public void Apply(IOneToManyCollectionInstance instance) 4 { 5 //指定从表的外键列列名是属性名+Id,例:CustomerId 6 instance.OtherSide.Column(instance.OtherSide.Name + "Id"); 7 //级联更新Casade:主表到从表设置成All 8 instance.Cascade.All(); 9 } 10 }
9)在Mapping文件夹下添加文件MappingOverride.cs。在这个文件里添加一些继承IAutoMappingOverride接口的类,可以对具体的一些实体类的映射进行重写。
1 using Demo.Auto.Entities.Domain; 2 using FluentNHibernate.Automapping; 3 using FluentNHibernate.Automapping.Alterations; 4 5 namespace Demo.Auto.Entities.Mapping 6 { 7 8 }
在namespace Demo.Auto.Entities.Mapping下添加三个类:CustomerMappingOverride、ProductMappingOverride、OrderMappingOverride。分别对实体类Customer、Product、Order的映射进行部分重写。
1 public class CustomerMappingOverride : IAutoMappingOverride<Customer> 2 { 3 public void Override(AutoMapping<Customer> mapping) 4 { 5 mapping.Map(x => x.CreditRating).CustomType<Enum.CustomerCreditRating>(); 6 mapping.HasMany(x => x.Orders).Inverse().Cascade.AllDeleteOrphan().Fetch.Join(); 7 } 8 } 9 10 public class ProductMappingOverride : IAutoMappingOverride<Product> 11 { 12 public void Override(AutoMapping<Product> mapping) 13 { 14 mapping.Map(x => x.ProductCode).Not.Nullable().Length(10); 15 mapping.Map(x => x.ProductName).Not.Nullable().Length(50); 16 mapping.HasManyToMany(x => x.Orders).Cascade.AllDeleteOrphan(); 17 } 18 } 19 20 public class OrderMappingOverride : IAutoMappingOverride<Order> 21 { 22 public void Override(AutoMapping<Order> mapping) 23 { 24 mapping.References(x => x.Customer).Cascade.SaveUpdate(); 25 } 26 }
10)修改Main函数,测试Auto Mapping。
1 using Demo.Auto.Entities.Domain; 2 using Demo.Auto.Entities.Mapping; 3 using FluentNHibernate.Automapping; 4 using FluentNHibernate.Cfg; 5 using FluentNHibernate.Cfg.Db; 6 using NHibernate.Tool.hbm2ddl; 7 using System; 8 9 namespace Demo.Auto.Entities 10 { 11 class Program 12 { 13 const string connString = "server=localhost;" + "database=NHibernateDemoDB;" + "integrated security=SSPI;"; 14 static void Main(string[] args) 15 { 16 var cfg = new AutoMappingConfiguration(); 17 var configuration = Fluently.Configure() 18 .Database(MsSqlConfiguration.MsSql2008.ConnectionString(connString)) 19 .Mappings(m => 20 m.AutoMappings.Add(AutoMap.AssemblyOf<Customer>(cfg) 21 .Conventions.Setup(c => 22 { 23 c.Add<IdConvention>(); 24 c.Add<DefaultStringLengthConvention>(); 25 c.Add<HasOneToManyConvention>(); 26 c.Add<HasManyToManyConvention>(); 27 }) 28 .UseOverridesFromAssemblyOf<CustomerMappingOverride>() 29 .UseOverridesFromAssemblyOf<ProductMappingOverride>() 30 .UseOverridesFromAssemblyOf<OrderMappingOverride>() 31 ).ExportTo(@"c:daniel")) 32 .BuildConfiguration(); 33 34 var exporter = new SchemaExport(configuration); 35 exporter.Execute(true, false, false); 36 37 Console.Write("Hit enter to exit:"); 38 Console.ReadLine(); 39 } 40 } 41 }
- FluentConfiguration对象的Mapping方法传入Lamda表达式指定Mapping方式。
- AutoMap.AssemblyOf<Customer>(cfg):指定使用自动映射,传入使用自定义类AutoMappingConfiguration的对象cfg,按自定义类AutoMappingConfiguration中的重载方法进行映射。方法调用生成AutoPersistenceModel对象。
- AutoPersistenceModel对象的Conventions.Setup方法传入Lamda表达式,添加一系列的Convention。
- AutoPersistenceModel对象的UseOverridesFromAssemblyOf方法,传入继承于IAutoMappingOverride接口的类作为泛型参数,添加一系列的Override。
- ExportTo(@"c:daniel"))方法将自动映射的定义xml文件导出到文件夹c:daniel。
1 var exporter = new SchemaExport(configuration); 2 exporter.Execute(true, false, false);
这两行代码生成创建数据库表的SQL语句。SchemaExport对象的Execute方法传入三个bool类型参数。第一个参数表示是否将SQL语句显示到控制台,第二个参数表示是否立即执行SQL语句,第三个参数表示是否删除并重建数据库表。
在C盘下创建文件夹daniel,执行程序,得到控制台输出:
到C:daniel文件夹下,看到生成的三个xml配置文件。
打开这三个文件,看到跟手写的映射文件是一样的。
结语
Fluent NHibernate提供的Auto Mapping确实是一个很方便的方式,大量地减少了手写映射的代码量。对于新的项目的确是一个不错的映射方式。有兴趣的可以到Fluent NHibernate官网http://www.fluentnhibernate.org上去查看更详细的内容。