1
votes

I am attempting to do a simple one to many mapping in fluent NHibernate, however i receive the following exception:

"NHibernate.TransientObjectException : object references an unsaved transient instance - save the transient instance before flushing or set cascade action for the property to something that would make it autosave. Type: Voter.Domain.Entities.VoteOption, Entity: Voter.Domain.Entities.VoteOption"

I have tried numerous using Cascade().All() - but this makes no difference.

Please help me to get this cascade working! Much time already wasted...

I have the following entities:

public class Vote 
{
    public Vote()
    {
        VoteOptions = new List<VoteOption>();
    }

    public virtual int Id { get; protected set; }
    public virtual Guid VoteReference { get; set; }
    public virtual string Title { get; set; }
    public virtual string Description { get; set; }
    public virtual DateTime ValidFrom { get; set; }
    public virtual DateTime ValidTo { get; set; }

    public virtual IList<VoteOption> VoteOptions { get; set; }

    public virtual void AddOption(VoteOption voteOption)
    {
        VoteOptions.Add(voteOption);
    }

    public virtual void AddOptions(List<VoteOption> options)
    {
        foreach (var option in options.Where(option => VoteOptionAlreadyExists(option) == false))
        {
            VoteOptions.Add(option);
        }
    }

    private bool VoteOptionAlreadyExists(VoteOption voteOption)
    {
        return VoteOptions.Any(x => x.Description == voteOption.Description);
    }
}

public class VoteOption
{
    public virtual int Id { get; protected set; }
    public virtual string LongDescription { get; set; }
    public virtual string Description { get; set; }
    public virtual Vote Vote { get; set; }
}

And the following mappings:

    public VoteMap()
    {
        Table("Vote");
        Id(x => x.Id).GeneratedBy.Identity().Column("Id");
        Map(x => x.VoteReference).Column("VoteReference");
        Map(x => x.Title).Column("Title").Not.Nullable();
        Map(x => x.Description).Column("Description").Not.Nullable();
        Map(x => x.ValidFrom).Column("ValidFrom").Not.Nullable();
        Map(x => x.ValidTo).Column("ValidTo").Not.Nullable();

        HasMany(x => x.VoteOptions).KeyColumn("Vote_Id").Cascade.All();
    }

public class VoteOptionMap : ClassMap<VoteOption>
{
    public VoteOptionMap()
    {
        Table("VoteOption");
        Id(x => x.Id).GeneratedBy.Identity().Column("Id");
        Map(x => x.Description).Column("Description").Not.Nullable();
        Map(x => x.LongDescription).Column("LongDescription").Not.Nullable();

        References(x => x.Vote).Column("Vote_Id").Cascade.All();

    }
}

And the following SQL Server database tables:

CREATE TABLE dbo.Vote
(
Id INT IDENTITY(1,1) PRIMARY KEY,
VoteReference UNIQUEIDENTIFIER NULL,
Title VARCHAR(500) NOT NULL,
[Description] VARCHAR(1000) NOT NULL,
ValidFrom DATETIME NOT NULL,
ValidTo DATETIME NOT NULL
)


CREATE TABLE dbo.VoteOption
(
Id INT IDENTITY(1,1) PRIMARY KEY,
Vote_Id INT NOT NULL,
[Description] VARCHAR(500) NOT NULL,
LongDescription VARCHAR(5000) NOT NULL
)

Implementation code is:

public void Save()
    {
        var vote = new Vote
        {
            VoteReference = new Guid(),
            Title = "Events Vote",
            Description = "Which event would you like to see next?",
            ValidFrom = DateTime.Now.AddDays(-2),
            ValidTo = DateTime.Now.AddDays(3)
        };

        var options = new List<VoteOption>
                {
                    new VoteOption {Description = "What you want?", LongDescription = "Tell me all about it..."},
                    new VoteOption {Description = "Another option?", LongDescription = "Tell me some more..."}
                };

        vote.AddOptions(options);

        using (var session = sessionFactory().OpenSession())
        {
            using (var transaction = session.BeginTransaction())
            {
                //This works - but undermines cascade!
                //foreach (var voteOption in vote.VoteOptions)
                //{
                //    session.Save(voteOption);
                //}

                session.Save(vote);
                transaction.Commit();
            }
        }

    }

    private ISessionFactory sessionFactory()
    {
        var config = new Configuration().Configure();
        return Fluently.Configure(config)
            .Mappings(m => m.AutoMappings.Add(AutoMap.AssemblyOf<Vote>()))
            .BuildSessionFactory();
    }
2

2 Answers

1
votes

I would say, that setting as shown above (the fluent mapping) is ok. Other words, the code I see right now, seems to be having different issue, then the Exception at the top.

The HasMany cascade setting is OK, but I would suggest to mark it as inverse (see here for more info ... NHibernate will not try to insert or update the properties defined by this join...)

HasMany(x => x.VoteOptions)
  .KeyColumn("Vote_Id")
  .Inverse()
  .Cascade.All();

Also, the Reference should be in most case without Cascade: References(x => x.Vote).Column("Vote_Id");

Having this, and running your code we should be recieving at the moment the SqlException: *Cannot insert the value NULL into column 'Vote_Id'*

Because of the TABLE dbo.VoteOption defintion:

...
Vote_Id INT NOT NULL, // must be filled even on a first INSERT

So, the most important change should be in the place, where we add the voteOption into Vote collection (VoteOptions). We always should/must be providing the reference back, ie. voteOption.Vote = this;

public virtual void AddOption(VoteOption voteOption)
{
    VoteOptions.Add(voteOption);
    voteOption.Vote = this; // here we should/MUST reference back
}

public virtual void AddOptions(List<VoteOption> options)
{
    foreach (var option in options.Where(option => VoteOptionAlreadyExists(option) == false))
    {
        VoteOptions.Add(option);
        option.Vote = this; // here we should/MUST reference back
    }
}

After these adjustments, it should be working ok

1
votes

The cascade option can be set globally using Fluent NHibernate Automapping conventions. The issue that @Radim Köhler pointed out also needs to be corrected when adding items to the List.

Using global conventions:

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

DefaultCascade.All()

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(DefaultCascade.All());
              }
    )
  .BuildSessionFactory();

Now it will automap the cascade when saving.


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