Can I make a Fluent NHibernate foreign key convent

2019-01-25 10:11发布

问题:

I have a database schema where the convention for a foreign key's name is:

ForeignTable.Name + ForeignTable.PrimaryKeyName

So, for a Child table referencing a Parent table with a primary key column named Key, the foreign key will look like ParentKey.

Is there a way to create this convention in my Fluent NHibernate mapping?

Currently I'm using a ForeignKeyConvention implementation like this:

public class ForeignKeyNamingConvention : ForeignKeyConvention
{
    protected override string GetKeyName(PropertyInfo property, Type type)
    {
        if (property == null)
        {
            // Relationship is many-to-many, one-to-many or join.
            if (type == null)
                throw new ArgumentNullException("type");

            return type.Name + "ID";
        }

        // Relationship is many-to-one.
        return property.Name + "ID";
    }
}

This works exactly as I want for all types which have "ID" as a primary key. What I would like to do is replace the constant "ID" with the name of the primary key of the type being referenced.

If this isn't currently possible with Fluent NHibernate, I'm happy to accept that answer.

回答1:

If you can get the Mapping<T> for a class, you can get the name of its Id column.

public class MyForeignKeyConvention: ForeignKeyConvention
{
    public static IList<IMappingProvider> Mappings = new List<IMappingProvider>();

    protected override string GetKeyName( System.Reflection.PropertyInfo property, Type type )
    {
        var pk = "Id";

        var model = new PersistenceModel();
        foreach( var map in Mappings ) {
            model.Add( map );
        }

        try {
            var mymodel = (IdMapping) model.BuildMappings()
                .First( x => x.Classes.FirstOrDefault( c => c.Type == type ) != null )
                .Classes.First().Id;

            Func<IdMapping, string> getname = x => x.Columns.First().Name;
            pk = getname( mymodel );
        } catch {
        }

        if (property == null) {
            return type.Name + pk;
        }
        return type.Name + property.Name;
    }
}

We can get the Mapping object with a little bit of plumbing.

The constructors of ClassMap<T> can pass this into our collection of Mappers.

For AutoMapping<T>, we can use Override as follows.

.Mappings( m => m.AutoMappings.Add( AutoMap.AssemblyOf<FOO>()
    .Override<User>( u => {
        u.Id( x => x.Id ).Column( "UID" );
        MyForeignKeyConvention.Mappings.Add( u );
    }
)


回答2:

Take a look at conventions and especially at implementing a custom foreign key convention.


UPDATE:

Here's an example. Assuming the following domain:

public class Parent
{
    public virtual int Id { get; set; }
}

public class Child
{
    public virtual string Id { get; set; }
    public virtual Parent Parent { get; set; }
}

which needs to be mapped to this schema:

create table Child(
    Id integer primary key, 
    ParentId integer
)

create table Parent(
    Id integer primary key
)

you could use this convention:

public class CustomForeignKeyConvention : IReferenceConvention
{
    public void Apply(IManyToOneInstance instance)
    {
        instance.Column(instance.Class.Name + "Id");
    }
}

and to create the session factory:

var sf = Fluently
    .Configure()
    .Database(
        SQLiteConfiguration.Standard.UsingFile("data.db3").ShowSql()
    )
    .Mappings(
        m => m.AutoMappings.Add(AutoMap
            .AssemblyOf<Parent>()
            .Where(t => t.Namespace == "Entities")
            .Conventions.Add<CustomForeignKeyConvention>()
        )
    )
    .BuildSessionFactory();


回答3:

For a system wide convention I believe this would serve the purpose best. ( I wasn't sure whether to include the whole text or just a portion here, since I answered it here already)

Here's the solution with links to current Fluent NHibernate & automapping documentation.

The issue (a simple example):

Say you have the simple example (from fluent's wiki) with an Entity and it's Value Objects in a List:

public class Product
{
  public virtual int Id { get; set; }
  //..
  public virtual Shelf { get; set; }
}

public class Shelf
{
  public virtual int Id { get;  set; }
  public virtual IList<Product> Products { get; set; }

  public Shelf()
  {
    Products = new List<Product>();
  }
}

With tables which have e.g.

Shelf 
id int identity

Product 
id int identity 
shelfid int

And a foreign key for shelfid -> Shelf.Id


You would get the error: invalid column name ... shelf_id


Solution:

Add a convention, it can be system wide, or more restricted.

ForeignKey.EndsWith("Id")

Code example:

var cfg = new StoreConfiguration();
var sessionFactory = Fluently.Configure()
  .Database(/* database config */)
  .Mappings(m =>
    m.AutoMappings.Add(
      AutoMap.AssemblyOf<Product>(cfg)
          .Conventions.Setup(c =>
              {
                  c.Add(ForeignKey.EndsWith("Id"));
              }
    )
  .BuildSessionFactory();

Now it will automap the ShelfId column to the Shelf property in Product.


More info

Wiki for Automapping

Table.Is(x => x.EntityType.Name + "Table")
PrimaryKey.Name.Is(x => "ID")
AutoImport.Never()
DefaultAccess.Field()
DefaultCascade.All()
DefaultLazy.Always()
DynamicInsert.AlwaysTrue()
DynamicUpdate.AlwaysTrue()
OptimisticLock.Is(x => x.Dirty())
Cache.Is(x => x.AsReadOnly())
ForeignKey.EndsWith("ID")

See more about Fluent NHibernate automapping conventions