Fluent NHibernate 自动映射用于单向多对多关系





5.00/5 (3投票s)
Fluent NHibernate 自动映射用于单向多对多关系
引言
Fluent Nhibernate 框架是一个很棒的框架,你可以在 NHibernate 框架之上以类的方式指定强类型映射。
虽然开发者可以为每个实体显式地指定自己的映射,但 Fluent NHibernate 也提供了自动映射的选项,在这种情况下,映射会根据你教给 Fluent NHibernate 的一些规则(Fluent NHibernate 约定)自动创建。因此,对于一些直观的情况,你不需要创建它们的映射。在我的例子中,我的系统中有很多查找表,自动映射将为我节省大量时间。
另一方面,我也支持显式映射与自动映射并行。我认为这是必要的,因为我们总是会遇到一些我们的自动映射无法处理的特殊和罕见的关联。当然,Fluent NHibernate 的重写是处理这些特殊情况的一种选择,但我并不太支持它,因为我认为每次遇到一个特殊情况时,它可能会导致映射分散到许多重写中。
背景
创建 Fluent NHibernate 自动映射在大多数情况下都相当直接,但当涉及到 ManyToMany 关系时,情况并非如此简单,我确实花了很长时间才让它如我所愿地工作,因为我和 Fluent NHibernate 之间存在一些概念上的差异。
在我看来,我的情况是一个常见的情况:单向 ManyToMany 关系。客户有一个产品列表,而产品没有与客户的任何引用,因为它被许多客户使用。当然,这样的关系会立即要求我们问:为了在数据库中建立这种关系,它是外键还是连接表?换句话说,它是一对多(外键)关系,还是多对多(使用连接表连接两个表)关系?
Fluent NHibernate 会立即将这种关系视为一对多关系,因此它会使用 HasMany convention 类而不是 HasManyToMany convention 类来处理 ManyToMany 关系。事实上,这让我很失望!他怎么会决定将这种关系视为一对多,而大多数情况下应该将其视为多对多,因为它是一方的?我希望在这种情况下使用连接表!至少他应该给我选择使用 HasMany 还是 ManyToMany 的选项。
使用代码
为了让我能够工作,我决定强制 Fluent NHibernate 理解这种关系是单向 ManyToMany 关系,而不是 HasMany 关系。为此,我不得不这样做:
- 为 HasManyToMany 关系创建一个自定义的自动映射步骤:Fluent NHibernate 使用步骤(不确定命名的概念是什么……总之)这些类继承自 IAutomappingStep,代表所有可能的关联类型。它会检查所有可能的关联,并将它们与这些步骤进行匹配。如果一个关联适合该步骤,它就会根据该步骤来映射关联。因此,应该使用这个自定义步骤而不是现有的步骤,以使其理解这种关系(单向多对多)是真正的多对多关系。
- 用修改过的 ManyToManyStep 替换现有的 ManyToManyStep。
实际上,理解以上几点是最重要的,因为我们现在知道了 Fluent NHibernate 如何处理我们想理解的情况,我们就能完成工作。
public class CustomHasManyToManyStep : IAutomappingStep
{
private readonly IAutomappingConfiguration cfg;
public CustomHasManyToManyStep(IAutomappingConfiguration cfg)
{
this.cfg = cfg;
}
public bool ShouldMap(Member member)
{
var type = member.PropertyType;
if (type.Namespace != "Iesi.Collections.Generic" &&
type.Namespace != "System.Collections.Generic")
return false;
if (type.HasInterface(typeof(IDictionary)) || type.ClosesInterface(typeof(IDictionary<,>)) || type.Closes(typeof(System.Collections.Generic.IDictionary<,>)))
return false;
//SamerA: Tweak to allow unidirectional ManyToMany mapping along with the Bidirectional
return (IsBidirectionalManyToMany(member) || IsUnidirectionalManyToMany(member));
}
static Member GetInverseCollectionProperty(Member member)
{
var type = member.PropertyType;
var expectedInversePropertyType = type.GetGenericTypeDefinition()
.MakeGenericType(member.DeclaringType);
var argument = type.GetGenericArguments()[0];
return argument.GetProperties()
.Select(x => x.ToMember())
.Where(x => x.PropertyType == expectedInversePropertyType && x != member)
.FirstOrDefault();
}
static bool IsBidirectionalManyToMany(Member member)
{
var type = member.PropertyType;
var expectedInversePropertyType = type.GetGenericTypeDefinition()
.MakeGenericType(member.DeclaringType);
var argument = type.GetGenericArguments()[0];
return argument.GetProperties()
.Select(x => x.ToMember())
.Any(x => x.PropertyType == expectedInversePropertyType && x != member);
}
static bool IsUnidirectionalManyToMany(Member member)
{
var type = member.PropertyType;
var argument = type.GetGenericArguments()[0];
return argument.GetProperties()
.Select(x => x.ToMember()).All(x => x.PropertyType != member.DeclaringType && x != member);
}
public void Map(ClassMappingBase classMap, Member member)
{
var inverseProperty = GetInverseCollectionProperty(member);
var parentSide = inverseProperty == null ? member.DeclaringType : cfg.GetParentSideForManyToMany(member.DeclaringType, inverseProperty.DeclaringType);
var mapping = GetCollection(member);
ConfigureModel(member, mapping, classMap, parentSide);
classMap.AddCollection(mapping);
}
static CollectionMapping GetCollection(Member property)
{
var collectionType = CollectionTypeResolver.Resolve(property);
return CollectionMapping.For(collectionType);
}
void ConfigureModel(Member member, CollectionMapping mapping, ClassMappingBase classMap, Type parentSide)
{
// TODO: Make the child type safer
mapping.SetDefaultValue(x => x.Name, member.Name);
mapping.Relationship = CreateManyToMany(member, member.PropertyType.GetGenericArguments()[0], classMap.Type);
mapping.ContainingEntityType = classMap.Type;
mapping.ChildType = member.PropertyType.GetGenericArguments()[0];
mapping.Member = member;
SetDefaultAccess(member, mapping);
SetKey(member, classMap, mapping);
if (parentSide != member.DeclaringType)
mapping.Inverse = true;
}
void SetDefaultAccess(Member member, CollectionMapping mapping)
{
var resolvedAccess = MemberAccessResolver.Resolve(member);
if (resolvedAccess != Access.Property && resolvedAccess != Access.Unset)
{
// if it's a property or unset then we'll just let NH deal with it, otherwise
// set the access to be whatever we determined it might be
mapping.SetDefaultValue(x => x.Access, resolvedAccess.ToString());
}
if (member.IsProperty && !member.CanWrite)
mapping.SetDefaultValue(x => x.Access, cfg.GetAccessStrategyForReadOnlyProperty(member).ToString());
}
ICollectionRelationshipMapping CreateManyToMany(Member property, Type child, Type parent)
{
var mapping = new ManyToManyMapping
{
Class = new TypeReference(property.PropertyType.GetGenericArguments()[0]),
ContainingEntityType = parent
};
mapping.AddDefaultColumn(new ColumnMapping { Name = child.Name + "_id" });
return mapping;
}
void SetKey(Member property, ClassMappingBase classMap, CollectionMapping mapping)
{
var columnName = property.DeclaringType.Name + "_id";
var key = new KeyMapping();
key.ContainingEntityType = classMap.Type;
key.AddDefaultColumn(new ColumnMapping { Name = columnName });
mapping.SetDefaultValue(x => x.Key, key);
}
}
关注点
现在,查看上面的代码,我们需要关注的最重要的事情是:
1. IsBirectionalManyToMany 和 IsUnidirectionalManyToMany 方法:前者通过查看当前类在另一侧是否有计数列表来确定这是否是完全的多对多关系。而 IsUnidirectionalManyToMany 检查当前类在另一侧是否没有 List 或 Property,这就是我们将这种情况视为 ManyToMany 关系的地方。
2. Map 方法:现在我们已经通过知道如何以及返回 ShouldMap 方法的 True 来同意这是一种单向 ManyToMany 关系,我们需要进行映射。对于单向关系,映射方式与双向关系相同,只是在单向关系中,父方总是固定的,因为另一方对此关系或任何引用一无所知。
最后一步是用我们的自定义步骤替换现有的步骤,以启用我们修复的这种关系。这可以在你的 Default Automapping Configuration 类(继承自 DefaultAutomappingConfiguration)中实现。
public override IEnumerable<IAutomappingStep> GetMappingSteps(AutoMapper mapper, FluentNHibernate.Conventions.IConventionFinder conventionFinder)
{
//SamerA: Replace the original HasManyToManyStep with the Customized HasManyToManyStep to allow Unidirectional ManyToMany
//Mapping when the other side has no reference for the current side.
//NOTE: an alternative to the below code is to redefine all steps with the CustomHasManyToManyStep here, the only preference is use
//the already created steps instead of creating new ones along with the old ones.
//Get all defined steps
var steps = base.GetMappingSteps(mapper, conventionFinder).ToList();
var index = steps.FindIndex(x => x.GetType() == typeof(HasManyToManyStep));
steps.RemoveAt(index);
steps.Insert(index, new CustomHasManyToManyStep(this));
return steps;
}
这里的代码相当简单,我们找到原始的那个,移除它,插入新的自定义的,然后返回修改后的新列表。