In order to make myself clear, I have created a most basic case to describe my problem. Let's say I have 3 tables:
CREATE TABLE [dbo].[Product](
[ProductID] [int] IDENTITY(1,1) NOT NULL,
[ProductName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED ( [ProductID] ASC )
) ON [PRIMARY]
CREATE TABLE [dbo].[OrderHeader](
[HeaderID] [int] IDENTITY(1,1) NOT NULL,
[Comment] [varchar](100) NULL,
CONSTRAINT [PK_OrderHeader] PRIMARY KEY CLUSTERED ( [HeaderID] ASC )
) ON [PRIMARY]
CREATE TABLE [dbo].[OrderDetail](
[HeaderID] [int] NOT NULL, /* FK to OrderHeader table */
[ProductID] [int] NOT NULL, /* FK to Product table */
[CreatedOn] [datetime] NOT NULL,
CONSTRAINT [PK_OrderDetail] PRIMARY KEY CLUSTERED
(
[HeaderID] ASC,
[ProductID] ASC
)
) ON [PRIMARY]
And I have created correponding entity classes and mapping classes.
public class Product {
public virtual int? Id { get; set; }
public virtual string Name { get; set; }
}
public class ProductMap : ClassMap<Product> {
public ProductMap() {
Table("Product");
Id(x => x.Id, "ProductID").GeneratedBy.Identity();
Map(x => x.Name, "ProductName");
}
}
public class OrderHeader {
public virtual int? Id { get; set; }
public virtual string Comment { get; set; }
public virtual IList<OrderDetail> Details { get; set; }
}
public class OrderHeaderMap : ClassMap<OrderHeader> {
public OrderHeaderMap() {
Table("OrderHeader");
Id(x => x.Id, "HeaderID").GeneratedBy.Identity();
Map(x => x.Comment, "Comment");
HasMany<OrderDetail>(x => x.Details)
.KeyColumn("HeaderID")
.Inverse()
.Cascade
.All();
}
}
public class OrderDetail {
public virtual OrderHeader OrderHeader { get; set; }
public virtual Product Product { get; set; }
public virtual DateTime? CreatedOn { get; set; }
public override bool Equals(object obj) {
OrderDetail other = obj as OrderDetail;
if (other == null) {
return false;
} else {
return this.Product.Id == other.Product.Id && this.OrderHeader.Id == other.OrderHeader.Id;
}
}
public override int GetHashCode() {
return (OrderHeader.Id.ToString() + "|" + Product.Id.ToString()).GetHashCode();
}
}
public class OrderDetailMap : ClassMap<OrderDetail> {
public OrderDetailMap() {
Table("OrderDetail");
CompositeId()
.KeyReference(x => x.Product, "ProductID")
.KeyReference(x => x.OrderHeader, "HeaderID");
References<OrderHeader>(x => x.OrderHeader, "HeaderID").ForeignKey().Not.Nullable().Fetch.Join();
References<Product>(x => x.Product, "ProductID").ForeignKey().Not.Nullable();
Version(x => x.CreatedOn).Column("CreatedOn").Generated.Always();
}
}
I have also created NH Session Provider
public class NHibernateSessionProvider {
private static ISessionFactory sessionFactory;
public static ISessionFactory SessionFactory {
get {
if (sessionFactory == null) {
sessionFactory = createSessionFactory();
}
return sessionFactory;
}
}
private static ISessionFactory createSessionFactory() {
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ShowSql()
.ConnectionString(c => c.FromConnectionStringWithKey("TestDB")))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<OrderHeaderMap>())
.BuildSessionFactory();
}
}
And a NH repository class is also created
public class NHibernateRepository<T, TId> {
protected ISession session = null;
protected ITransaction transaction = null;
public NHibernateRepository() {
this.session = NHibernateSessionProvider.SessionFactory.OpenSession();
}
public void Save(T entity) {
session.SaveOrUpdate(entity);
}
public void AddNew(T entity) {
session.Save(entity);
}
public void BeginTransaction() {
transaction = session.BeginTransaction();
}
public void CommitTransaction() {
transaction.Commit();
closeTransaction();
}
public void RollbackTransaction() {
transaction.Rollback();
closeTransaction();
closeSession();
}
private void closeTransaction() {
transaction.Dispose();
transaction = null;
}
private void closeSession() {
session.Close();
session.Dispose();
session = null;
}
public void Dispose() {
if (transaction != null) {
CommitTransaction();
}
if (session != null) {
session.Flush();
closeSession();
}
}
}
In my code, I have created 2 different ways to save this master/detail structure with composite-id.
private static void method1() {
NHibernateRepository<Product, int?> repoProduct = new NHibernateRepository<Product, int?>();
NHibernateRepository<OrderHeader, int?> repo = new NHibernateRepository<OrderHeader, int?>();
OrderHeader oh = new OrderHeader();
oh.Comment = "Test Comment " + DateTime.Now.ToString();
oh.Details = new List<OrderDetail>();
for (int i = 0; i < 2; i++) {
oh.Details.Add(new OrderDetail
{
OrderHeader = oh,
Product = repoProduct.GetById(i + 3)
});
}
repo.AddNew(oh);
}
private static void method2() {
NHibernateRepository<OrderHeader, int?> repoHeader = new NHibernateRepository<OrderHeader, int?>();
OrderHeader oh = new OrderHeader();
oh.Comment = "Test Comment " + DateTime.Now.ToString();
repoHeader.Save(oh);
NHibernateRepository<OrderDetail, int?> repoDetail = new NHibernateRepository<OrderDetail, int?>();
for (int i = 0; i < 2; i++) {
OrderDetail od = new OrderDetail
{
OrderHeaderId = oh.Id,
OrderHeader = oh,
ProductId = i + 3,
Product = new Product
{
Id = i + 3
},
};
repoDetail.AddNew(od);
}
}
But for both methods, the OrderDetail table is never saved. I have turned on ShowSql() to see SQL statement executed on console, no SQL generated to save OrderDetail table at all.
I did quite a lot of search everywhere and could not have a clear conclusion what is wrong.
Anybody has some clue, what exactly do I need to do to save an entity with composite-id?
Thanks
Hardy
Both the model and the mapping are incorrect.
Remove OrderHeaderId and ProductId from OrderDetail.
Then, the Composite id should include OrderHeader and Product as references (I think with Fluent it's KeyReference instead of KeyProperty; in XML it's key-many-to-one instead of key-property)
Then, add a proper Cascade setting, as Cole suggested.
Sample usage:
using (var session = GetSessionFromSomewhere())
using (var tx = session.BeginTransaction())
{
var orderHeader = new OrderHeader();
...
orderHeader.Details.Add(new OrderDetail
{
OrderHeader = orderHeader;
Product = session.Load<Product>(someProductId);
});
session.Save(orderHeader);
tx.Commit();
}
Everything in that block is required.
I don't think that the composite-id is what is causing you issues. I think it's the way you have your OrderDetails mapped in your OrderHeader map.
I think it should be something like this instead:
HasMany<OrderDetail>(x => x.Details).KeyColumn("HeaderID").Inverse().Cascade.AllDeleteOrphan();
Edit:
You should listen to Diego below and change your mapping to:
public class OrderDetailMap : ClassMap<OrderDetail> {
public OrderDetailMap() {
Table("OrderDetail");
CompositeId()
.KeyReference(x => x.Product, "ProductID")
.KeyReference(x => x.OrderHeader, "HeaderID");
Version(x => x.CreatedOn).Column("CreatedOn").Generated.Always();
}
}
The code you have in your above mapping of OrderDetails is what is causing you the error "Invalid index 2 for this SqlParameterCollection with Count=2."
References<OrderHeader>(x => x.OrderHeader, "HeaderID").ForeignKey().Not.Nullable().Fetch.Join();
References<Product>(x => x.Product, "ProductID").ForeignKey().Not.Nullable();
well firstly, your OrderDetail is mapped wrongly: You may not map one column multiple times. Here you both assign it for composite-id as well as have a many-to-one. Your composite-id can (and should) have 2 many-to-one properties and not just value properties.
This is evident in your last comment on Diego's answer, see also IndexOutOfRangeException Deep in the bowels of NHibernate
Secondly you are setting an inverse on the OrderHeader.Details collection which if i remember correctly means method1 would not cause an insert on the OrderDetail
Related
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();
}
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
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
I have two tables in a database: Order and OrderLine. OrderLine has a not nullable column (OrderId) with foreign key pointing to Order.Id.
In code I have two classes: Order and OrderLine. Order has a collection of OrderLines but OrderLine does not have a reference to Order.
It is it correct that you can’t map this relation without introducing a reference from OrderLine to Order or maintain a private field with the order id inside the OrderLine as indicated in this blog post (variation 3)?
Thanks in advance…
The following code shows the enties and mappings
namespace NhibernateMappingTests
{
[TestClass]
public class MappingExample2
{
[TestMethod]
public void InsertTest()
{
using (var session = NHibernateHelper.OpenSession())
{
using (var transaction = session.BeginTransaction())
{
Order order = new Order();
order.AddOrderLine(new OrderLine());
session.Save(order);
transaction.Commit();
}
}
}
}
public class Order
{
public Order()
{
OrderLines = new List<OrderLine>();
}
public int Id { get; set; }
public IList<OrderLine> OrderLines { get; set; }
public void AddOrderLine(OrderLine orderLine)
{
OrderLines.Add(orderLine);
}
}
public class OrderLine
{
public int Id { get; set; }
}
public class OrderMap : ClassMap<Order>
{
public OrderMap()
{
Not.LazyLoad();
Id(x => x.Id).GeneratedBy.Identity();
HasMany(x => x.OrderLines)
.AsBag()
.Cascade.All()
.KeyColumn("OrderId");
}
}
public class OrderLineMap : ClassMap<OrderLine>
{
public OrderLineMap()
{
Not.LazyLoad();
Id(x => x.Id).GeneratedBy.Identity();
}
}
public class NHibernateHelper
{
private static ISessionFactory _sessionFactory;
private static ISessionFactory SessionFactory
{
get
{
if (_sessionFactory == null)
InitializeSessionFactory();
return _sessionFactory;
}
}
private static void InitializeSessionFactory()
{
_sessionFactory = Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(#"data source=(local)\db01;initial catalog=MyDatabase;persist security info=false;packet size=4096;integrated security=sspi;").ShowSql())
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<TimeSeries>())
//.ExposeConfiguration(cfg => new SchemaExport(cfg).Create(true, true))
.BuildSessionFactory();
}
public static ISession OpenSession()
{
return SessionFactory.OpenSession();
}
}
}
It result in the following exception:
Test method NhibernateMappingTests.MappingExample2.InsertTest threw exception:
NHibernate.Exceptions.GenericADOException: could not insert: [NhibernateMappingTests.OrderLine][SQL: INSERT INTO [OrderLine] DEFAULT VALUES; select SCOPE_IDENTITY()] ---> System.Data.SqlClient.SqlException: Cannot insert the value NULL into column 'OrderId', table 'MyDatabase.dbo.OrderLine'; column does not allow nulls. INSERT fails.
You can actually map the relationship only on one side, this shouldn't matter.
Though the update statement will be executed in this case. To fix the exception, you'll just have to change the mapping to
HasMany(x => x.OrderLines)
.AsBag()
.Cascade.AllDeleteOrphan() // or .All
.KeyColumn("OrderId")
.Not.KeyNullable();
I'm trying to do a batch insert and it's not working. I thought I had this working but something seems to have broken and I'd appreciate it if someone could show me what.
Edit - Here's the database schema:
CREATE TABLE [dbo].[Categories](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](100) NOT NULL,
CONSTRAINT [PK_Categories] PRIMARY KEY CLUSTERED ([Id])
)
CREATE TABLE [dbo].[ProductTopSellersCategory](
[ProductId] [int] NOT NULL,
[CategoryId] [int] NOT NULL,
[Order] [int] NOT NULL,
CONSTRAINT [PK_ProductTopSellersCategory]
PRIMARY KEY CLUSTERED ([ProductId], [CategoryId])
)
ALTER TABLE [dbo].[ProductTopSellersCategory] ADD
CONSTRAINT [FK_ProductTopSellersCategory_Products]
FOREIGN KEY ([ProductId]) REFERENCES [dbo].[Products] ([Id]),
CONSTRAINT [FK_ProductTopSellersCategory_Categories]
FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([Id])
I have the following entities:
public class Category {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class ProductTopSellerCategory {
public virtual ProductTopSellerCategoryIdentifier Id { get; set; }
private Product _product;
public virtual Product Product {
get { return _product; }
set { _product = value; Id.ProductId = _product.Id; }
}
private Category _category;
public virtual Category Category {
get { return _category; }
set { _category = value; Id.CategoryId = _category.Id; }
}
[Required]
public virtual int Order { get; set; }
public ProductTopSellerCategory() {
Id = new ProductTopSellerCategoryIdentifier();
}
}
[Serializable]
public class ProductTopSellerCategoryIdentifier {
public virtual int ProductId { get; set; }
public virtual int CategoryId { get; set; }
#region Composite Id Members
public override bool Equals(object obj) {
if (obj == null || !(obj is ProductTopSellerCategoryIdentifier))
return false;
var i = (ProductTopSellerCategoryIdentifier)obj;
return ProductId == i.ProductId && CategoryId == i.CategoryId;
}
public override int GetHashCode() {
return ToString().GetHashCode();
}
public override string ToString() {
return ProductId + "|" + CategoryId;
}
#endregion
}
With the corresponding fluent mappings:
public class CategoryMap : ClassMap<Category> {
public CategoryMap() {
Table("Categories");
Id(x => x.Id);
Map(x => x.Name);
}
}
public class ProductTopSellerCategoryMap : ClassMap<ProductTopSellerCategory> {
public ProductTopSellerCategoryMap() {
Table("ProductTopSellersCategory");
CompositeId(x => x.Id)
.KeyProperty(x => x.ProductId)
.KeyProperty(x => x.CategoryId);
References(x => x.Product).ReadOnly();
References(x => x.Category).ReadOnly();
Map(x => x.Order, "[Order]");
}
}
Now when I say:
var category = new Category() { Name = "Test 1" };
var product = session.Get<Product>(1);
var topSeller = new ProductTopSellerCategory() { Product = product, Category = category };
session.SaveOrUpdate(category);
session.SaveOrUpdate(topSeller);
session.Transaction.Commit();
It throws the error:
The INSERT statement conflicted with the FOREIGN KEY constraint
"FK_ProductTopSellersCategory_Categories". The conflict occurred in
database "xxx", table "dbo.Categories", column 'Id'. The statement has
been terminated.
I've tried to simplify this example as much as possible. I'd really appreciate the help. Thanks
You have a one-to-many relationship between Category and ProductTopSellerCategory with just the many side mapped. Normally you would use the inverse attribute on the collection mapped on the one side but you don't have that mapped so I suggest:
using (var txn = session.BeginTransaction())
{
var category = new Category() { Name = "Test 1" };
session.Save(category);
session.Flush();
var product = session.Get<Product>(1);
var productTopSellerCategory = new ProductTopSellerCategory() { Product = product, Category = category };
session.Save(productTopSellerCategory);
txn.Commit();
}
The problem with your original code is that NHibernate is attempting to insert the new ProductTopSellerCategory then update the category. It's doing this because the inverse attribute is not set. Forcing NHibernate to insert the new Category by flushing the session should resolve the problem.
I think I've found a solution. It's a little bit of a hack but it meant I didn't have to change my entities and mappings. The issue happens because the CategoryId in the identity type doesn't point to the same reference as the Category.Id in the top sellers entity. To fix this issue I need to add the following just before I insert the top seller:
topSeller.Id.CategoryId = topSeller.Category.Id;
The references are unidirectional. The table (StoreProduct) for this entity is actually a join table that has these fields:
Store_id
Product_id
ExtraBit
So I went with an entity having a compoundID (store_id and product_id) and the ExtraBit is just a string:
public class StoreProduct
{
protected StoreProduct():this(null,null,null){ }
public StoreProduct(Store c_Store, Product c_Product, String c_ExtraBit)
{
Store = c_Store;
Product = c_Product;
ExtraBit = c_ExtraBit;
}
public virtual int Product_id { get; set; }
public virtual int Store_id { get; set; }
public virtual Store Store { get; set; }
public virtual Product Product { get; set; }
public virtual String ExtraBit { get; set; }
public override int GetHashCode()
{
return Store.GetHashCode() + Product.GetHashCode();
}
public override bool Equals(object obj)
{
StoreProduct obj_StoreProduct;
obj_StoreProduct = obj as StoreProduct;
if (obj_StoreProduct == null)
{
return false;
}
if (obj_StoreProduct.Product != this.Product && obj_StoreProduct.Store != this.Store)
{
return false;
}
return true;
}
}
And the mapping:
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
LazyLoad();
CompositeId().KeyProperty(x => x.Store_id).KeyProperty(x => x.Product_id);
References(x => x.Store).ForeignKey("Store_id").Cascade.All();
References(x => x.Product).ForeignKey("Product_id").Cascade.All();
Map(x => x.ExtraBit);
}
}
It doesn't work though, when I tried saving the StoreProduct and its newly created Store and product. Can anyone help? Here is some output:
Unhandled Exception: System.ArgumentOutOfRangeException: Index was out of range.
Must be non-negative and less than the size of the collection.
Parameter name: index
at System.ThrowHelper.ThrowArgumentOutOfRangeException()
at System.Data.SQLite.SQLiteParameterCollection.GetParameter(Int32 index)
at System.Data.Common.DbParameterCollection.System.Collections.IList.get_Item
(Int32 index)
at NHibernate.Type.Int32Type.Set(IDbCommand rs, Object value, Int32 index) in
d:\CSharp\NH\NH\nhibernate\src\NHibernate\Type\Int32Type.cs:line 60
at NHibernate.Type.NullableType.NullSafeSet(IDbCommand cmd, Object value, Int
32 index) in d:\CSharp\NH\NH\nhibernate\src\NHibernate\Type\NullableType.cs:line
180
at NHibernate.Type.NullableType.NullSafeSet(IDbCommand st, Object value, Int3
2 index, ISessionImplementor session) in d:\CSharp\NH\NH\nhibernate\src\NHiberna
te\Type\NullableType.cs:line 139
at NHibernate.Type.ComponentType.NullSafeSet(IDbCommand st, Object value, Int
32 begin, ISessionImplementor session) in d:\CSharp\NH\NH\nhibernate\src\NHibern
ate\Type\ComponentType.cs:line 221
at NHibernate.Persister.Entity.AbstractEntityPersister.Dehydrate(Object id, O
bject[] fields, Object rowId, Boolean[] includeProperty, Boolean[][] includeColu
mns, Int32 table, IDbCommand statement, ISessionImplementor session, Int32 index
) in d:\CSharp\NH\NH\nhibernate\src\NHibernate\Persister\Entity\AbstractEntityPe
rsister.cs:line 2418
Edit: Thanks to the help bellow I seem to have a decent solution:
Store Mapping and Class:
namespace compoundIDtest.Domain.Mappings
{
public class StoreMap : ClassMap<Store>
{
public StoreMap()
{
Id(x => x.Id).Column("Store_id");
Map(x => x.Name);
HasMany(x => x.Staff)
.Inverse()
.Cascade.All();
HasManyToMany(x => x.Products)
.Cascade.All()
.Table("StoreProduct");
}
}
}
namespace compoundIDtest.Domain.Entities
{
public class Store
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual IList<Product> Products { get; set; }
public virtual IList<Employee> Staff { get; set; }
public virtual IList<StoreProduct> StoreProducts { get; set; }
public Store()
{
Products = new List<Product>();
Staff = new List<Employee>();
}
public virtual void AddProduct(Product product)
{
product.StoresStockedIn.Add(this);
Products.Add(product);
}
public virtual void AddEmployee(Employee employee)
{
employee.Store = this;
Staff.Add(employee);
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
public override bool Equals(object obj)
{
Store obj_Store;
obj_Store = obj as Store;
if (obj_Store == null)
{
return false;
}
if (obj_Store.Name != this.Name)
{
return false;
}
return true;
}
}
}
Product Mapping And Class
namespace compoundIDtest.Domain.Mappings
{
public class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Id(x => x.Id).Column("Product_id");
Map(x => x.Name);
Map(x => x.Price);
HasManyToMany(x => x.StoresStockedIn)
.Cascade.All()
.Inverse()
.Table("StoreProduct");
}
}
}
namespace compoundIDtest.Domain.Entities
{
public class Product
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual double Price { get; set; }
public virtual IList<Store> StoresStockedIn { get; set; }
public virtual IList<StoreProduct> StoreProducts { get; set; }
public Product()
{
StoresStockedIn = new List<Store>();
StoreProducts = new List<StoreProduct>();
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
public override bool Equals(object obj)
{
Product obj_Product;
obj_Product = obj as Product;
if (obj_Product == null)
{
return false;
}
if (obj_Product.Name != this.Name)
{
return false;
}
return true;
}
}
}
And StoreProduct
namespace compoundIDtest.Domain.Mappings
{
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
LazyLoad();
CompositeId().KeyReference(x => x.Store, "Store_id").KeyReference(x => x.Product, "Product_id");
References(x => x.Store, "Store_id").Not.Update().Not.Insert().Cascade.All();
References(x => x.Product, "Product_id").Not.Update().Not.Insert().Cascade.All();
Map(x => x.ExtraBit);
}
}
}
namespace compoundIDtest.Domain.Entities
{
public class StoreProduct
{
public StoreProduct(){}
public virtual Store Store { get; set; }
public virtual Product Product { get; set; }
public virtual String ExtraBit { get; set; }
public override int GetHashCode()
{
if (this.ExtraBit != null)
{
return Store.GetHashCode() + Product.GetHashCode() + ExtraBit.GetHashCode();
}
return Store.GetHashCode() + Product.GetHashCode();
}
public override bool Equals(object obj)
{
StoreProduct obj_StoreProduct;
obj_StoreProduct = obj as StoreProduct;
if (obj_StoreProduct == null)
{
return false;
}
if (obj_StoreProduct.Product != this.Product && obj_StoreProduct.Store != this.Store && obj_StoreProduct.ExtraBit != this.ExtraBit)
{
return false;
}
return true;
}
}
}
And here is code for an app to test the above:
using System;
using System.IO;
using compoundIDtest.Domain.Entities;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
using FluentNHibernate.Conventions;
namespace compoundIDtest
{
class Program
{
private const string DbFile = "firstProgram.db";
static void Main()
{
// create our NHibernate session factory
var sessionFactory = CreateSessionFactory();
using (var session = sessionFactory.OpenSession())
{
// populate the database
using (var transaction = session.BeginTransaction())
{
// create a couple of Stores each with some Products and Employees
var barginBasin = new Store { Name = "Bargin Basin" };
var superMart = new Store { Name = "SuperMart" };
var CornerShop = new Store { Name = "Corner Shop" };
var potatoes = new Product { Name = "Potatoes", Price = 3.60 };
var fish = new Product { Name = "Fish", Price = 4.49 };
var milk = new Product { Name = "Milk", Price = 0.79 };
var bread = new Product { Name = "Bread", Price = 1.29 };
var cheese = new Product { Name = "Cheese", Price = 2.10 };
var waffles = new Product { Name = "Waffles", Price = 2.41 };
var poison = new Product { Name = "Poison", Price = 1.50 };
var daisy = new Employee { FirstName = "Daisy", LastName = "Harrison" };
var jack = new Employee { FirstName = "Jack", LastName = "Torrance" };
var sue = new Employee { FirstName = "Sue", LastName = "Walkters" };
var bill = new Employee { FirstName = "Bill", LastName = "Taft" };
var joan = new Employee { FirstName = "Joan", LastName = "Pope" };
var storeproduct = new StoreProduct { Store = CornerShop, Product = poison, ExtraBit = "Extra Bit"};
//session.SaveOrUpdate(CornerShop);
//session.SaveOrUpdate(poison);
session.Save(storeproduct);
// add products to the stores, there's some crossover in the products in each
// store, because the store-product relationship is many-to-many
AddProductsToStore(barginBasin, potatoes, fish, milk, bread, cheese);
AddProductsToStore(superMart, bread, cheese, waffles);
// add employees to the stores, this relationship is a one-to-many, so one
// employee can only work at one store at a time
AddEmployeesToStore(barginBasin, daisy, jack, sue);
AddEmployeesToStore(superMart, bill, joan);
// save both stores, this saves everything else via cascading
session.SaveOrUpdate(barginBasin);
session.SaveOrUpdate(superMart);
//session.SaveOrUpdate(CornerShop);
//session.SaveOrUpdate(poison);
//session.SaveOrUpdate(storeproduct);
transaction.Commit();
}
}
using (var session = sessionFactory.OpenSession())
{
// retreive all stores and display them
using (var transaction = session.BeginTransaction())
{
var products = session.CreateCriteria(typeof(Product))
.List<Product>();
foreach (var product in products)
{
product.Price = 100;
session.SaveOrUpdate(product);
}
var storeproducts = session.CreateCriteria(typeof(StoreProduct)).List<StoreProduct>();
foreach (StoreProduct storeproduct in storeproducts)
{
if (storeproduct.Store.Name == "SuperMart")
{
storeproduct.ExtraBit = "Thank you, come again";
}
session.SaveOrUpdate(storeproduct);
}
transaction.Commit();
}
}
Console.ReadKey();
}
private static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
.Database(SQLiteConfiguration.Standard
.UsingFile(DbFile))
.Mappings(m =>
m.FluentMappings.AddFromAssemblyOf<Program>())
.ExposeConfiguration(BuildSchema)
.BuildSessionFactory();
}
private static void BuildSchema(Configuration config)
{
// delete the existing db on each run
if (File.Exists(DbFile))
File.Delete(DbFile);
// this NHibernate tool takes a configuration (with mapping info in)
// and exports a database schema from it
new SchemaExport(config)
.Create(false, true);
}
private static void WriteStorePretty(Store store)
{
Console.WriteLine(store.Name);
Console.WriteLine(" Products:");
foreach (var product in store.Products)
{
Console.WriteLine(" " + product.Name);
}
Console.WriteLine(" Staff:");
foreach (var employee in store.Staff)
{
Console.WriteLine(" " + employee.FirstName + " " + employee.LastName);
}
Console.WriteLine();
}
public static void AddProductsToStore(Store store, params Product[] products)
{
foreach (var product in products)
{
store.AddProduct(product);
}
}
public static void AddEmployeesToStore(Store store, params Employee[] employees)
{
foreach (var employee in employees)
{
store.AddEmployee(employee);
}
}
}
}
I had a mapping pretty much identical to this and the way I ended up mapping it was like this:
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
CompositeId()
.KeyReference(x => x.Store, "Store_id")
.KeyReference(x => x.Product, "Product_id");
Map(x => x.ExtraBit);
}
}
Inside of my Store and Product classes I have add and remove methods that make the creation of this middle class almost invisible. Example below:
public class Store
{
public IList<StoreProduct> StoreProducts { get; set; }
//Other properties and Constructors
public virtual void AddProduct(Product productToAdd, string extraBit)
{
StoreProduct newStoreProduct = new StoreProduct(this, productToAdd, extraBit);
storeProducts.Add(newStoreProduct);
}
}
In addition to the above I had HasMany's to a StoreProduct collection in my Store and Product classes that are set to Cascade.AllDeleteOrphan()
I was never able to be able to map the StoreProduct such that when it was saved by itself it would create a new Store and a new Product. I had to eventually map it like the above. So your Store or Product will need to exist before you actually create the relationship (StoreProduct) between them depending on which side you are creating your new StoreProduct from.
Edit:
You may also be able to map it like this to achieve what you are wanting:
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
CompositeId()
.KeyReference(x => x.Store, "Store_id")
.KeyReference(x => x.Product, "Product_id");
References(x => x.Store, "Store_id")
.Not.Update()
.Not.Insert()
.Cascade.All();
References(x => x.Product, "Product_id")
.Not.Update()
.Not.Insert()
.Cascade.All();
Map(x => x.ExtraBit);
}
}
I'm getting an error with NHibernate when I try to perfrom a ISession.Delete on any table with a One to Many relationship.
NHibernate is trying to set the foreign key to the parent table in the child table to null, rather than just deleting the child table row.
Here is my domain:
public class Parent
{
public Parent()
{
_children = new List<Child>();
}
public int Id { get; set; }
public string SimpleString { get; set; }
public DateTime? SimpleDateTime { get; set; }
private IList<Child> _children;
public IEnumerable<Child> Children
{
get { return _children; }
}
public void AddChild(Child child)
{
child.Parent = this;
_children.Add(child);
}
}
public class Child
{
public int Id { get; set; }
public string SimpleString { get; set; }
public DateTime? SimpleDateTime { get; set; }
[JsonIgnore]
public Parent Parent { get; set; }
}
I have set-up the Fluent NHibernate mappings as follows:
public class ParentMap : ClassMap<Parent>
{
public ParentMap()
{
Not.LazyLoad();
Id(x => x.Id);
Map(x => x.SimpleString);
Map(x => x.SimpleDateTime);
HasMany(x => x.Children)
.Not.LazyLoad()
.KeyColumn("ParentId").Cascade.AllDeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
}
}
public class ChildMap : ClassMap<Child>
{
public ChildMap()
{
Not.LazyLoad();
Id(x => x.Id);
Map(x => x.SimpleString);
Map(x => x.SimpleDateTime);
References(x => x.Parent).Not.Nullable().Column("ParentId").Cascade.All().Fetch.Join();
}
}
I've told NHibernate to Cascade.AllDeleteOrphan() but it's still trying to set the ParentId foriegn key to null here is the test I setup:
public void Delete_GivenTableWithChildren_WillBeDeletedFromDB()
{
int id;
using (var createSession = MsSqlSessionProvider.SessionFactory.OpenSession())
{
var parent = new Parent();
parent.AddChild(new Child { SimpleString = "new child from UI" });
using (var trx = createSession.BeginTransaction())
{
createSession.Save(parent);
trx.Commit();
id = parent.Id;
}
}
using (var firstGetSession = MsSqlSessionProvider.SessionFactory.OpenSession())
{
var result = firstGetSession.Get<Parent>(id);
Assert.IsNotNull(result);
}
using (var deleteSession = MsSqlSessionProvider.SessionFactory.OpenSession())
{
using (var trx = deleteSession.BeginTransaction())
{
deleteSession.Delete("from " + typeof(Parent).Name + " o where o.Id = :Id", id, NHibernateUtil.Int32);
trx.Commit();
}
}
using (var session = MsSqlSessionProvider.SessionFactory.OpenSession())
{
var result = session.Get<Parent>(id);
Assert.IsNull(result);
}
}
Which is failing on the deleteSession.Delete line after attempting the following SQL:
exec sp_executesql N'UPDATE [Child] SET ParentId = null WHERE ParentId = #p0',N'#p0 int',#p0=5
with:
NHibernate.Exceptions.GenericADOException : could not delete collection: [SaveUpdateOrCopyTesting.Parent.Children#5][SQL: UPDATE [Child] SET ParentId = null WHERE ParentId = #p0]
----> System.Data.SqlClient.SqlException : Cannot insert the value NULL into column 'ParentId', table 'SaveUpdateCopyTestingDB.dbo.Child'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
Does anyone know what I've done wrong in my mappings, or know of a way to stop NHibernate from attempting to null the foreign key id?
Thanks
Dave
Try setting .Inverse() on the ParentMap's HasMany, so it looks like:
HasMany(x => x.Children)
.Not.LazyLoad()
.KeyColumn("ParentId").Cascade.AllDeleteOrphan().Inverse()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
I'm not sure if cascade works if you delete with HQL.
Try this:
var parent = deleteSession.Load<Parent>(id)
deleteSession.Delete(parent);
It's a pity that you don't have lazy loading. Load would not need the entity to be read from the database, it would just create a proxy in memory.