Batch Insert - Foreign Key Not Working - nhibernate

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;

Related

Mapping by code on Class that has a property of type ICollection<>

I want to map a class that has a property of type ICollection<> using NHibernate mapping by code. The code below works. But I don't like the extra Person property within CarSet to make the mapping work.
public class PersonSet
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ICollection<CarSet> Cars { get; set; }
}
public class CarSet
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual PersonSet Person { get; set; }
}
public class PersonSetMap : ClassMapping<PersonSet>
{
public PersonSetMap()
{
Id(x => x.Id, m=>m.Generator(Generators.Identity));
Property(x=>x.Name);
Set(x => x.Cars, c =>
{
c.Key(k =>
{
k.Column("PersonId");
});
c.Cascade(Cascade.Persist);
c.Lazy(CollectionLazy.NoLazy);
}, r =>
{
r.OneToMany();
}
);
}
}
public class CarSetMap : ClassMapping<CarSet>
{
public CarSetMap()
{
Id(x => x.Id, m => m.Generator(Generators.Identity));
Property(x => x.Name);
ManyToOne(x => x.Person, m =>
{
m.Column("PersonId");
m.Cascade(Cascade.None);
m.NotNullable(true);
});
}
}
public void Save(){
using (var session = Cfg.Session)
using (var tx = session.BeginTransaction())
{
PersonSet John = new PersonSet { Name = PersonName.John };
John.Cars = new List<CarSet> {
new CarSet { Name = CarnName.BMW,Person = John},
new CarSet { Name = CarnName.BM,Person = John }};
session.Save(entity);
tx.Commit();
}
}
The code above generates SQL script below:
create table PersonSet (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (Id)
)
create table CarSet (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
PersonId INT not null,
primary key (id)
)
alter table CarSet
add constraint FKF967D6489A220265
foreign key (PersonId)
references PersonSet
What I want is to generate SQL script with difference shown below, and keep the rest the same:
create table CarSet (
Name NVARCHAR(255) null,
PersonId INT not null,
)
Ideally I want the CarSet like this instead:
public class CarSet
{
public virtual int PersonId { get; set; }
public virtual string Name { get; set; }
}
Any idea?
map Cars as ComponentCollection
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Car> Cars { get; set; }
}
class Car
{
public virtual Person Owner { get; set; }
public virtual string Name { get; set; }
}
public class PersonMap : ClassMapping<Person>
{
public PersonMap()
{
Id(x => x.Id, m => m.Generator(Generators.Identity));
Property(x => x.Name);
Set(x => x.Cars, c =>
{
c.Key(k => k.Column("PersonId"));
c.Cascade(NHibernate.Mapping.ByCode.Cascade.Persist);
c.Lazy(CollectionLazy.NoLazy);
}, r =>
{
r.Component(c =>
{
c.Parent(x => x.Owner);
c.Property(x => x.Name);
});
});
}
}
Your ideal solution isn't possible. To use a CarSet table without it's own ID column it has to be a component, but component sets can't have nullable columns. If it's ok for you to mark Name as not-null you can adapt the solution Firo posted.
If that's not ok you can at least solve your first request to remove the Person property. Just delete the property and mark the key column in your set mapping as not-nullable. CarSet will still be an entity (and therefore have it's own ID) but you don't need the reference to PersonSet in code.
Btw, why are your classes postfixed by Set? Just naming them Person and Car would be much better since they only represent one person or car, not a collection of them.

Fluent nhibernate map lookup table as enum

I am trying to map a lookup table to an enum using:
FluentNhibernate 1.3.0.733
NHibernate 3.3.1.4000
I am not able to load objects. If I remove the enum mapping I can load objects.
Code:
Order order = session.Get<Order>(id);
Error:
Provided id of the wrong type. Expected: Order+OrderStatus, got System.Int32
Object:
public class Order
{
public enum OrderStatus
{
PaymentPending = 0
}
public virtual int Id { get; set; }
public virtual Customer Customer { get; set; }
public virtual Address Address { get; set; }
public virtual IList<OrderLine> OrderLines { get; set; }
public virtual OrderStatus Status { get; set; }
public virtual DateTime Created { get; set; }
public Order()
{
OrderLines = new List<OrderLine>();
}
}
Mapping: (I have cut the mapping down to these fields for testing)
public OrderMapping()
{
Table("orders");
Id(x => x.Id);
Id(x => x.Status, "state_id").CustomType<Order.OrderStatus>().Not.Nullable();
References(x => x.Address).Cascade.All().Column("address_id");
References(x => x.Customer).Cascade.All().Column("customer_id");
}
Tables:
CREATE TABLE [order_states] (
[id] INTEGER NOT NULL PRIMARY KEY,
[state] NVARCHAR(50) NOT NULL
);
CREATE TABLE [orders] (
[id] INTEGER NOT NULL PRIMARY KEY,
[customer_id] INTEGER NOT NULL,
[address_id] INTEGER NOT NULL,
[state_id] INTEGER NOT NULL,
[created] DATE,
FOREIGN KEY(customer_id) REFERENCES customers(id),
FOREIGN KEY(address_id) REFERENCES addresses(id),
FOREIGN KEY(state_id) REFERENCES order_states(id)
);
What am I doing wrong?
Caused by a silly error.
I had the the status mapped as an id and not as a property.
Correct mapping:
public OrderMapping()
{
Table("orders");
Id(x => x.Id);
Map(x => x.Status, "state_id").CustomType<Order.OrderStatus>().Not.Nullable();
References(x => x.Address).Cascade.All().Column("address_id");
References(x => x.Customer).Cascade.All().Column("customer_id");
}

Fluent NHibernate automapping composite id with component

I have this complex situation: a database of countries/regions/states/cities which primary key is composed by a code (nvarchar(3)) in a column called "Id" plus all key columns of "ancestors" (regions/states/cities).
So the table country has only one key coumn (Id) while cities has 4 key columns (Id, StateId,regionId,CountryId). Obviously they're all related, so each ancestor column is a foreign key to the related table.
I have Entities in my Model that map this relationships. But they all derive from one type called Entity<T> where T may be a simple type (string, in etc) or a complex one (a component implementing the key).
Entity<T> implements a single property called Id of type T.
For each db table, if it has a comlex key, I implement it in a separate component, which oveerides also Equals and GetHashCode() Methods (in future I'll implement those in the Entity base class).
So I have a RegionKey componet that has 2 properties (Id and CountryId).
I have conventions for Foreign Key and primary key naming and type and that is ok.
I have also Mapping ovverrides for each complex Entity.
For simplicity, lets concentrate only on Countries and Regions table. Here they are:
public class Country: Entity<string>
{
public virtual string Name { get; set; }
public virtual IList<Region> Regions { get; set; }
}
public class Region: Entity<RegionKey>
{
public virtual string Name { get; set; }
public virtual Country Country { get; set; }
}
and the RegionKey component:
namespace Hell.RealHellState.Api.Entities.Keys
{
[Serializable]
public class RegionKey
{
public virtual string Id { get; set; }
public virtual string CountryId { get; set; }
public override bool Equals(object obj)
{
if (obj == null)
return false;
var t = obj as RegionKey;
if (t == null)
return false;
return Id == t.Id && CountryId == t.CountryId;
}
public override int GetHashCode()
{
return (Id + "|" + CountryId).GetHashCode();
}
}
}
Here is the configuration of AutoPersistenceModel:
public ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
.Database(
MsSqlCeConfiguration.Standard
.ConnectionString(x=>x.Is(_connectionString))
)
.Mappings(m => m.AutoMappings.Add(AutoMappings))
.ExposeConfiguration(BuildSchema)
.BuildSessionFactory();
}
private AutoPersistenceModel AutoMappings()
{
return AutoMap.Assembly(typeof (Country).Assembly)
.IgnoreBase(typeof(Entity<>))
.Conventions.AddFromAssemblyOf<DataFacility>()
.UseOverridesFromAssembly(GetType().Assembly)
.Where(type => type.Namespace.EndsWith("Entities"));
}
private static void BuildSchema(Configuration config)
{
//Creates database structure
new SchemaExport(config).Create(false, true);
//new SchemaUpdate(config).Execute(false, true);
}
Here is the Regions entity overrides
public class RegionMappingOverride : IAutoMappingOverride<Region>
{
public void Override(AutoMapping<Region> mapping)
{
mapping.CompositeId(x=>x.Id)
.KeyProperty(x => x.Id, x=> x.ColumnName("Id").Length(3).Type(typeof(string)))
.KeyProperty(x => x.CountryId, x => x.ColumnName("CountryId").Length(3).Type(typeof(string)));
}
}
Ok now when I test this mapping I got an error saying: The data types of the columns in the relationship do not match.
I have also tried this override:
public void Override(AutoMapping<Region> mapping)
{
mapping.CompositeId()
.ComponentCompositeIdentifier(x=>x.Id)
.KeyProperty(x => x.Id.Id, x=> x.ColumnName("Id").Length(3).Type(typeof(string)))
.KeyProperty(x => x.Id.CountryId, x => x.ColumnName("CountryId").Length(3).Type(typeof(string)));
}
And it almost work but it creates a Regions table with a single column key of varbinary(8000) which is not what I want:
CREATE TABLE [hell_Regions] (
[Id] varbinary(8000) NOT NULL
, [Name] nvarchar(50) NULL
, [CountryId] nvarchar(3) NULL
);
GO
ALTER TABLE [hell_Regions] ADD CONSTRAINT [PK__hell_Regions__0000000000000153] PRIMARY KEY ([Id]);
GO
ALTER TABLE [hell_Regions] ADD CONSTRAINT [FK_Regions_Country] FOREIGN KEY ([CountryId]) REFERENCES [hell_Countries]([Id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
GO
I don't have a clue of how to deal with it since it seems to me everythin is ok.
Thanks in advance for your answers
Ok I menaged to solve it: I had to sign the CompositeId class as MAPPED, since it is a component. So this is my new RegionMappingOverride:
public class RegionMappingOverride : IAutoMappingOverride<Region>
{
public void Override(AutoMapping<Region> mapping)
{
mapping.CompositeId(x=>x.Id)
.Mapped()
.KeyProperty(x =>x.Id,x=>x.Length(3))
.KeyProperty(x => x.CountryId, x=>x.Length(3));
}
}
Now the sql created is correct:
create table hell_Countries (
Id NVARCHAR(3) not null,
Name NVARCHAR(50) null,
primary key (Id)
)
create table hell_Regions (
Id NVARCHAR(3) not null,
CountryId NVARCHAR(3) not null,
Name NVARCHAR(50) null,
primary key (Id, CountryId)
)
alter table hell_Regions
add constraint FK_Region_Country
foreign key (CountryId)
references hell_Countries

Inserting records in postgres using NHibernate 3.2 (loquacious) and bycode mappings

I am working on a very basic NHibernate 3.2 task, inserting records into existing Postgres tables. I am using very simple object so that they make sense for this question.
The postgres tables are as follows:
CREATE TABLE cat
(
id serial NOT NULL,
"name" character varying(50) NOT NULL,
sex_id integer NOT NULL,
CONSTRAINT cat_pkey PRIMARY KEY (id),
CONSTRAINT cat_sex_id_fkey FOREIGN KEY (sex_id)
REFERENCES sex (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
OIDS=FALSE
);
CREATE TABLE sex
(
id serial NOT NULL,
"name" character varying(10) NOT NULL,
CONSTRAINT sex_pkey PRIMARY KEY (id),
CONSTRAINT sex_name_key UNIQUE (name)
)
WITH (
OIDS=FALSE
);
My mapping classes are as follows:
public class CatMap : ClassMapping<Cat>
{
public CatMap()
{
Table("cat");
Id(x => x.Id, map =>
{
map.Column("id");
map.Generator(NHibernate.Mapping.ByCode.Generators.Native);
});
Property(x => x.Name, map =>
{
map.Column("name");
map.Length(50);
map.NotNullable(true);
});
ManyToOne(x => x.Sex, map =>
{
map.Column("Sex");
map.Unique(true);
map.ForeignKey("cat_sex_id_fkey");
});
}
}
public class SexMap : ClassMapping<Sex>
{
public SexMap()
{
Table("sex");
Id(x => x.Id, map =>
{
map.Column("id");
map.Generator(Generators.Native);
});
Property(x => x.Name, map =>
{
map.Column("name");
map.Unique(true);
map.Length(10);
map.NotNullable(true);
});
}
}
My data classes are as follows:
public class Sex
{
public Sex()
{
}
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class Cat
{
public Cat()
{
}
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Sex Sex { get; set; }
}
Finally, the class containing the code where I am actually attempting to use all of the above.
public class Class1
{
public string DoSomething()
{
var sessionFactory = CreateSessionFactory();
using (var session = sessionFactory.OpenSession())
{
using (var transaction = session.BeginTransaction())
{
Postgres.Tables.Sex sex1 = new Postgres.Tables.Sex() { Name = "Male" };
Postgres.Tables.Sex sex2 = new Postgres.Tables.Sex() { Name = "Female" };
Postgres.Tables.Cat cat1 = new Postgres.Tables.Cat() { Name = "Cat1" };
cat1.Sex = sex1;
Postgres.Tables.Cat cat2 = new Postgres.Tables.Cat() { Name = "Cat2" };
cat2.Sex = sex2;
session.SaveOrUpdate(sex1);
session.SaveOrUpdate(sex2);
session.SaveOrUpdate(cat1);
session.SaveOrUpdate(cat2);
transaction.Commit();
}
}
return "I created the cats.";
}
private static ISessionFactory CreateSessionFactory()
{
NHibernate.Mapping.ByCode.ModelMapper modelMapper = new NHibernate.Mapping.ByCode.ModelMapper();
System.Type[] mappingTypes = typeof(Postgres.Tables.Mappings.CatMap).Assembly.GetExportedTypes().Where(t => t.Name.EndsWith("Map")).ToArray();
modelMapper.AddMappings(mappingTypes);
Configuration cfg = new Configuration();
cfg.Proxy(p => p.ProxyFactoryFactory<NHibernate.Bytecode.DefaultProxyFactoryFactory>());
cfg.DataBaseIntegration(d =>
{
d.ConnectionString = "server=192.168.1.126;Port=5432;Database=simple;User Id=postgres;Password=postgres;";
d.Dialect<NHibernate.Dialect.PostgreSQL82Dialect>();
});
cfg.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities());
return cfg.BuildSessionFactory();
}
}
I receive a GenericADOException at "session.SaveOrUpdate(cat1)" with the message "could not insert: [DAL.Postgres.Tables.Cat][SQL: INSERT INTO cat (name, Sex) VALUES (?, ?); select lastval()]". The InnerException is "{"ERROR: 42703: column \"sex\" of relation \"cat\" does not exist"}".
I am a bit stumped at how to properly assign "sex1" to "cat1" and "sex2" to "cat2" so that the first is Male and the second is Female.
Thank you for any input.
m.Column("Sex") as the name suggests denote column name and not the property name as you specified. So you should write m.Column("sex_id"). map.ForeignKey("cat_sex_id_fkey") is used by NH as a FK name when NH create db schema from your mapping.

NHibernate Entity with Composite-Id Saving Issue

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