views:

757

answers:

4

I am not exactly an NHibernate expert, so this may be a lack of understanding in that department. I have two simple entities with a many-to-many relationship

public class Category
{
    public virtual int Id { get; private set; }
    public virtual string Name { get; set; }
    public virtual string Description { get; set; }
    public virtual bool Visible { get; set; }

    public virtual IList<Product> Products { get; set; }
}

and

public class Product
{
    public virtual int Id { get; private set; }
    public virtual string Name { get; set; }
    public virtual string Description { get; set; }
    public virtual decimal Price { get; set; }
    public virtual bool Visible { get; set; }
    public virtual IList<Category> Categories { get; set; }
}

My configuration is

    public static void BuildFactory()
    {
        _factory = Fluently.Configure()
            .Database(
                MsSqlConfiguration
                    .MsSql2008
                    .ConnectionString(_connectionString)
                    .ShowSql()
            )
            .Mappings(m =>
                m.AutoMappings.Add(
                    AutoMap.AssemblyOf<Database>()
                        .Where(t => t.Namespace == "FancyStore.Models.Catalog")
                        .Override<Product>(k => 
                            k.HasManyToMany(x => x.Categories)
                                .Table("ProductsToCategories")
                                    .ParentKeyColumn("Product_id")
                                    .ChildKeyColumn("Category_id")
                                .Cascade.AllDeleteOrphan())
                        .Override<Category>(k =>
                            k.HasManyToMany(x => x.Products)
                                .Table("ProductsToCategories")
                                    .ParentKeyColumn("Category_id")
                                    .ChildKeyColumn("Product_id")
                                .Cascade.AllDeleteOrphan().Inverse())
                )
            )
            .ExposeConfiguration(SetConfiguration)
            .BuildConfiguration()
            .BuildSessionFactory();
    }

So I started without the overrides (added them in later after some googling suggested it), and am generating my schema with ExportSchema. ExportSchema knows about the many to many relationship (even without the overrides), because it generated a join table (ProductsToCategories).

I wanted to add some simple test data, so I did it like this

        using (var db = new Repository<Category>())
        {
            for (int i = 0; i < 6; i++)
            {
                var cat = new Category
                {
                    Name = "Things" + i,
                    Description = "Things that are things",
                    Visible = true,
                    Products = new List<Product>()
                };

                for (int k = 0; k < 6; k++)
                {
                    var prod = new Product
                    {
                        Name = "Product" + k,
                        Description = "Product for " + cat.Name,
                        Visible = true,
                        Categories = new List<Category>(new[] { cat })
                    };
                    cat.Products.Add(prod);
                }
                db.Save(cat);
            }
        }

This saves products and categories properly, but it does not save any of the relationships in the join table. Looking at the sql a call to category.Products generates, it is actually looking in that join table for products (and cant find any, since it is empty)

Any help would be greatly appreciated :-)

EDIT: Removed an inverse, problem still happening :(

EDIT2:

Here are the mapping files for catalog and product (note: the name is kind of silly, but this is just a quick prototype-ish sort of deal)

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="none" default-lazy="true">
  <class xmlns="urn:nhibernate-mapping-2.2" name="FancyStore.Models.Catalog.Product, FancyStore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="`Product`">
    <id name="Id" type="System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Id" />
      <generator class="identity" />
    </id>
    <property name="Name" type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Name" />
    </property>
    <property name="Description" type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Description" />
    </property>
    <property name="Price" type="System.Decimal, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Price" />
    </property>
    <property name="Visible" type="System.Boolean, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Visible" />
    </property>
    <bag cascade="all-delete-orphan" inverse="true" name="Categories" table="ProductsToCategories">
      <key>
        <column name="Product_id" />
      </key>
      <many-to-many class="FancyStore.Models.Catalog.Category, FancyStore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
        <column name="Category_id" />
      </many-to-many>
    </bag>
  </class>
</hibernate-mapping>

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="none" default-lazy="true">
  <class xmlns="urn:nhibernate-mapping-2.2" name="FancyStore.Models.Catalog.Category, FancyStore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="`Category`">
    <id name="Id" type="System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Id" />
      <generator class="identity" />
    </id>
    <property name="Name" type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Name" />
    </property>
    <property name="Description" type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Description" />
    </property>
    <property name="Visible" type="System.Boolean, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="Visible" />
    </property>
    <bag cascade="all-delete-orphan" name="Products" table="ProductsToCategories">
      <key>
        <column name="Category_id" />
      </key>
      <many-to-many class="FancyStore.Models.Catalog.Product, FancyStore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
        <column name="Product_id" />
      </many-to-many>
    </bag>
  </class>
</hibernate-mapping>
+2  A: 

I might be mistaken, but it looks like you marked the relationship as "inverse" for both sides.

You should do this for only one side of the relationship, and make sure you call Save on the other one.

Also, I believe the relationship needs to be valid on both sides before you save (add cat to prod.Categories, in addition to what you're already doing).

My understanding of these kinds of relationships is admittedly fuzzy, but this is what my research over the months has come up with (and solved some of my own problems).

Sapph
ive got Categories = new List<Category>(new[] { cat }) in my product initializer, which is shorthand for "Make a new list<category>, and add a new array of Categories to it with a single element (cat)". I tried removing both inverses, and adding it to one or the other side and nothing worked. :( Thanks for the answer though, pretty sure I want to inverse the category, so I just call save on product. (once I get this issue sorted out)
Matt Briggs
Oops, I'm very sorry, should have read your code more carefully (missed the initializer). Tangentially, though, you could also just do `new List<Category>() { cat }` and skip the array part.
Sapph
A: 

You have inverse on the wrong side of the association. Inverse means "the other side manages this association".

What you need is either to remove Inverse on Category and put it on Product, or change Product to be the 'owning' entity, like this:

//make sure to add it to BOTH sides of the association
product.Categories.Add(cat);
cat.Products.Add(product);

db.Save(cat);  //if Product has Inverse
/* or */
db.Save(prod); //if Category has Inverse
Ben Scheirman
tried flipping the inverse, still no go :(
Matt Briggs
A: 

What about if you were to apply the fluent mappings for Product and Category first, before the automapping. For example,

public class Product
{
    public virtual int Id { get; private set; }
    public virtual string Name { get; set; }
    public virtual string Description { get; set; }
    public virtual decimal Price { get; set; }
    public virtual bool Visible { get; set; }
    public virtual IList<Category> Categories { get; set; }

    public Product()
    {
        Categories = new List<Category>();
    }
}

public class ProductMap : ClassMap<Product>
{
    Id(x => x.Id);
    Map(x => x.Name);
    Map(x => x.Description);
    Map(x => x.Price);
    Map(x => x.Visible);
    HasManyToMany(x => x.Categories)
      .Table("ProductsToCategories")
      .ParentKeyColumn("Product_id")
      .ChildKeyColumn("Category_id")
      .Cascade.AllDeleteOrphan();
}

public class Category
{
    public virtual int Id { get; private set; }
    public virtual string Name { get; set; }
    public virtual string Description { get; set; }
    public virtual bool Visible { get; set; }
    public virtual IList<Product> Products { get; set; }

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

public class CategoryMap : ClassMap<Category>
{
    Id(x => x.Id);
    Map(x => x.Name);
    Map(x => x.Description);
    Map(x => x.Visible);
    HasManyToMany(x => x.Products)
      .Table("ProductsToCategories")
      .ParentKeyColumn("Category_id")
      .ChildKeyColumn("Product_id")
      .Cascade.AllDeleteOrphan().Inverse();
}

with configuration,

public static void BuildFactory()
{
    _factory = Fluently.Configure()
        .Database(
            MsSqlConfiguration
                .MsSql2008
                .ConnectionString(_connectionString)
                .ShowSql()
        )
        .Mappings(m => {
            m.FluentMappings.AddFromAssemblyOf<Database>();
            m.AutoMappings.Add(
              AutoMap.AssemblyOf<Database>()
              .Where(t => t.Namespace == "FancyStore.Models.Catalog"));
        })
        .ExposeConfiguration(SetConfiguration)
        .BuildConfiguration()
        .BuildSessionFactory();
}
Ajw
A: 

@Matt Briggs, I've figured it out. You have to set one relationship as Inverse() and save the other entity, add references to both entities (prod.Categories.Add() + cat.Products.Add()) and commit the transaction. Unless you commit the query for the join table won't be generated. – HeavyWave May 13 at 12:43

Matt Briggs