I am then using Fluent NHibernate and its automapping feature to map the the following simplified POCO classes:
public class Webpage
{
public virtual int Id { get; set; }
public virtual string UrlIdentifier { get; set; }
public virtual WebpageType WebpageType { get; set; }
}
public class WebpageType
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
I am then overriding the following mapping to explicitly set no cascading from Webpage to WebpageType:
public class WebpageMap : IAutoMappingOverride<Webpage>
{
public void Override(AutoMapping<Webpage> mapping)
{
mapping.References(w => w.WebpageType).Cascade.None();
}
}
For any pur NHibernate readers, here are the xml mappings produced by fluent:
<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="EveryPage.Core.Domain.Webpage, EveryPage.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="`Webpage`">
<id name="Id" type="System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" unsaved-value="0">
<column name="Id" />
<generator class="identity" />
</id>
<property name="UrlIdentifier" type="System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="UrlIdentifier" />
</property>
<many-to-one cascade="none" class="EveryPage.Core.Domain.WebpageType, EveryPage.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="WebpageType">
<column name="WebpageType_id" />
</many-to-one>
</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="EveryPage.Core.Domain.WebpageType, EveryPage.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="`WebpageType`">
<id name="Id" type="System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" unsaved-value="0">
<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>
</class>
</hibernate-mapping>
The problem comes when I test that updates do not cascade to WebpageType via webpage, basically they do!!
I have the following test:
[Test]
public void Assert_SaveOrUpdate_On_Webpage_Does_Not_Cascade_Update_To_WebpageType()
{
// Get the existing webpage.
webpage = _webpageRepository.Get("~/testwebpage1.aspx");
// Update the WebpageType.
const string webpageTypeName = "qwerty test";
webpage.WebpageType.Name = webpageTypeName;
// Save the changes.
Assert.DoesNotThrow(() => _webpageRepository.SaveOrUpdate(webpage));
// We need to flush the changes to the store for it to execute the changes.
Assert.DoesNotThrow(() => NHibernateSession.Current.Flush());
// Remove the webpage and tag from the level 1 cache so we force a trip to the store on our next check.
NHibernateSession.Current.Evict(webpage);
// Check that the webpageType has not been updated.
webpageType = _webpageTypeRepository.Get(webpageType.Id);
Assert.AreNotEqual(webpageTypeName, webpageType.Name);
}
The above test is wrapped in a global transaction.
The test fails and NHibernate does execute an update to the Name of the related WebpageType. The delete and save(create new) cascades work correctly and do not cascade.
Have I missunderstood cascade and/or is there a problem with my logic/test.
Any help/advice is appreciated. Thanks.
If you are trying to stop your app from accidentally changing properties on WebPageType, I think it would be easier and safer to achieve this by marking WebPageType as ReadOnly in the mapping. Then you won't need to protect it via handling cascading in all its associations.
I think this is a misunderstanding of what cascading means.
In your example, NHibernate will update the Name property of you WebPageType no matter what you set cascading to. If you think about it, how would the NHibernate library tell if you're manipulating the property's value using the association from the WebPage instance, or if it's done "directly"?
The settings for cascading in NHibernate tells how associations between entities should be handled, not how the actual value inside each entity is handled. For example, you can set delete cascading, which will automatically delete associated entities when the entity itself is deleted.
Things blog post might make things a bit clearer, or at least work as some kind of reference: http://ayende.com/Blog/archive/2006/12/02/NHibernateCascadesTheDifferentBetweenAllAlldeleteorphansAndSaveupdate.aspx
What does your repository do? Make sure it doesn't run a saveorupdate on the webpagetype. If it isn't then I don't see any obvious explanation for this behaviour.
Related
I'm having a problem with NHibernate and the mapping file when I try to save my drink object in my MVC application. My mapping file is an embedded resource and my hibernate.cfg.xml is copy always.
Here are my class.cs:
namespace FrancosPoS.DBMapping {
public class drink {
public drink() { }
public virtual int id { get; set; }
public virtual string type { get; set; }
public virtual string price { get; set; }
}
}
My XML mapping:
<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping assembly="FrancosPoS.DBMapping" namespace="FrancosPoS.DBMapping" xmlns="urn:nhibernate-mapping-2.2">
<class name="drink" table="drink" lazy="true" >
<id name="id">
<generator class="identity" />
<column name="id" sql-type="int(11)" not-null="true" />
</id>
<property name="type">
<column name="type" sql-type="varchar(25)" not-null="true" />
</property>
<property name="price">
<column name="price" sql-type="varchar(8)" not-null="true" />
</property>
</class>
</hibernate-mapping>
By the way, the connection opens and close fine if I don't try to save it on the database.
Here is my Solution Explorer:
Solution Explorer Image
And here is the error that is driving me nuts:
"Error: NHibernate.MappingException: FrancosPoS.DBMapping.drink.hbm.xml(6,8): XML validation error: The element 'id' in namespace 'urn:nhibernate-mapping-2.2' has invalid child element 'column' in namespace 'urn:nhibernate-mapping-2.2'. ---> System.Xml.Schema.XmlSchemaValidationException: The element 'id' in namespace 'urn:nhibernate-mapping-2.2' has invalid child element 'column' in namespace 'urn:nhibernate-mapping-2.2'.\r\n --- End of inner exception stack trace ---
the problem is in node id, it does not have a child, remove that node and it will simply be like this:
<id name="id">
<generator class="identity" />
</id>
the column's name is specified in id node, and you dont have to tell column has "not-null" constraint since id columns are always required
Also, in this line of your xml mapping:
<hibernate-mapping assembly="FrancosPoS.DBMapping" namespace="FrancosPoS.DBMapping" xmlns="urn:nhibernate-mapping-2.2">
"assembly" attribute seems to be wrong, it should be the name of your assembly (which i'm almost sure is "FrancosPos").
I am relatively new to using Nhibernate but basic things already are working.
Now I have to map the generic entity that implements the tree structure. Separately, each one (only generic or only tree) works fine.
Here's the code for the model:
public class Test<T>
{
public virtual Int64 Id { get; set; }
public string Name { get; set; }
public IList<Test<T>> Children { get; set; }
}
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="PersistencyObjectModel" namespace="PersistencyObjectModel.Domain">
<class name="Test`1[String]">
<id name="Id">
<generator class="guid"/>
</id>
<property name="Name" length="50" not-null="true" unique="true"/>
<set name="Children" table="TEST_TEST_LINK" cascade="all-delete-orphan" >
<key column="ParentId"/>
<many-to-many column="ChildId" class="Test`1[String]"/>
</set>
</class>
</hibernate-mapping>
When I use that model, I get the following Nhibernate error:
{"persistent class PersistencyObjectModel.Domain.Test`1[[PersistencyObjectModel.Domain.String,
PersistencyObjectModel]], PersistencyObjectModel not found"}
What does this error mean and how can I fix it?
Update: try this
<hibernate-mapping namespace="PersistencyObjectModel.Domain"
assembly="PersistencyObjectModel"
xmlns="urn:nhibernate-mapping-2.2">
<class name="PersistencyObjectModel.Domain.Test`1[[System.String,
mscorlib, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089]]"
table="TestOfString">
<id name="Id">
<generator class="guid.comb"/>
</id>
<property name="Name" length="50" not-null="true" unique="true"/>
<set name="Children" table="TEST_TEST_LINK">
<key column="ParentId" />
<many-to-many column="ChildId"
class="PersistencyObjectModel.Domain.Test`1[[System.String,
mscorlib, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089]]"/>
</set>
</class>
</hibernate-mapping>
Some things to note:
You can't use a guid generator with a long propery; change that to Guid
You must specify the name of the entity table
Use fully qualified names.
Suppose I have this (simplified)
Class Cliente
Id(v => v.numero_cliente, "numero_cliente")
HasMany(v => v.Acionamentos).Cascade.All().LazyLoad()
Class Movimentacao
References(v => v.Cliente, "id_Fornecedor")
Class Acionamento
References(v => v.Cliente, "numero_cliente")
Fluent nHibernate will generate wrong SQL, for example:
If i try to get Acionamentos,then it will throw an incorrect SQL:
SELECT * FROM Acionamentos WHERE id_Fornecedor=p0
But on my Acionamento Mapping i set an reference to a column named numero_cliente and not to id_Fornecedor
If I use always the same column name "numero_cliente" on all References, no problem happens. But i am afraid i will not be able to guarantee that all column names for the Client class will be the same on all tables.
Does somebody knows what to do? Can the Fluent NHibernate team see this and post an comment here?
If you want the exact SQL here is:
could not initialize a collection:
[Sistema.Clientes.Cliente.Acionamentos#019012938/07][SQL: SELECT acionament0_.id_Fornecedor as id7_1_, acionament0_.id_Acionamento as id1_1_, acionament0_.id_Acionamento as id1_6_0_, acionament0_.DataHora as DataHora6_0_, acionament0_.Tipo as Tipo6_0_, acionament0_.Descricao as Descricao6_0_, acionament0_.numero_cliente as numero5_6_0_, acionament0_.id_Usuario as id6_6_0_ FROM clientes.acionamento acionament0_ WHERE acionament0_.id_Fornecedor=?
The error above throwns when trying to get the Cliente.Acionamentos
Below is the HBM XML:
Sistema.Clientes.Cliente.hbm.xml:
<id name="numero_cliente" type="System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="numero_cliente" />
<generator class="assigned" />
</id>
<bag cascade="all" lazy="true" name="Acionamentos" mutable="true">
<key>
<column name="id_Fornecedor" /><!-- oopps, this should be numero_cliente -->
</key>
<one-to-many class="Sistema.CRM.Acionamento, Sistema, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bag>
Sistema.CRM.Acionamento.hbm.xml:
<many-to-one class="Sistema.Clientes.Cliente, Sistema, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="Cliente">
<column name="numero_cliente" />
</many-to-one>
Estoque.Movimentacao.hbm.xml:
<many-to-one class="Sistema.Clientes.Cliente, Sistema, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="Cliente" not-found="ignore">
<column name="id_Fornecedor" />
</many-to-one>
With the help steps you provided and some pray I solved it, adding the KeyColumn!
Id(v => v.numero_cliente, "numero_cliente")
HasMany(v => v.Acionamentos).KeyColumn("numero_cliente").Cascade.All().LazyLoad()
after adding that, then the generated HBM was changed to:
<bag cascade="all" lazy="true" name="Acionamentos" mutable="true">
<key>
<column name="numero_cliente" />
</key>
<one-to-many class="Sistema.CRM.Acionamento, Sistema, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bag>
and no more SQL errors happened.
I am happy that I will be able to use it now! I really did not want to use EF
I have a one-to-many parent/child relationship between a user and a session. Here are the mappings:
<class name="ApplicationUser" table="APPLICATION_USER" lazy="true">
<id name="Id" column="APPLICATION_USER_ID">
<generator class="guid.comb" />
</id>
<property name="UserName" column="USER_NAME" />
<property name="Password" column="PASSWORD" />
<bag name="UserSessions" lazy="true" cascade="all-delete-orphan" inverse="true">
<key column="APPLICATION_USER_ID"></key>
<one-to-many class="UserSession"></one-to-many>
</bag>
</class>
<class name="UserSession" table="USER_SESSION" lazy="true">
<id name="Id" column="USER_SESSION_ID">
<generator class="guid.comb" />
</id>
<property name="Created" column="CREATED" />
<many-to-one name="ApplicationUser" column="APPLICATION_USER_ID" class="ApplicationUser" unique="true" />
</class>
I have a simple method in ApplicationUser that creates the session relationship:
public virtual UserSession LogInUser()
{
UserSession session = new UserSession() { ApplicationUser = this };
_userSessions.Add(session);
return session;
}
When a user logs in, there are several database interactions that occur, wrapped in a transaction. In addition to creating the new user session, I want to be able to audit that the user has logged in, and include the new session ID with the audit data. However, since the UserSession hasn't been persisted yet, its ID is empty.
I tried saving the ApplicationUser to generate the user session ID:
IApplicationUserManager manager = ManagerFactory.GetApplicationUserManager();
ApplicationUser user = manager.GetById(userId);
user.LogInUser();
manager.Save(user);
I thought this would work because of the all-delete-orphan cascade setting, but it doesn't. Is there a way to force ID generation before a Flush() or transaction commit?
Is there a way to force ID generation before a Flush() or transaction commit?
No. I would use <generator class="assigned" /> for the UserSession class and initialize it in the class's constructor.
Also, you might want to think twice about exposing the Password as a property.
Calling Session.Save (that's NHibernate's Session btw) should generate the ID. That's one of the main reasons for using Guids - they can be generated without going to the database.
If the save doesn't cascade from ApplicationUser, you will just need to manually pass the UserSession to Session.Save. I can't see why this is would be a problem though.
I am having an issue with saving entities with one-to-one relationships. I just want to save the parent entity and have the child be save aswell but I am having to save both separately.
This is an example of what I am having to do, otherwise the child is not saved.
var session = SessionProvider.OpenSession.Session;
using (var tx = session.BeginTransaction())
{
try
{
session.SaveOrUpdate(parent);
if (parent.Child.IsPersisted)
{
session.Update(parent.Child);
}
else
{
session.Save(parent.Child);
}
}
}
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false" assembly="xxx">
<class name="Parent" polymorphism="explicit" table="Parent">
<id name="Id" column="JointID" type="int">
<generator class="native" />
</id>
<one-to-one name="Child" class="Child" />
</class>
</hibernate-mapping>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false"
assembly="xxx">
<class name="Child" xmlns="urn:nhibernate-mapping-2.2" polymorphism="explicit" table="Child">
<id name="Id" column="JointID" type="int" unsaved-value="0">
<generator class="native" />
</id>
<many-to-one name="Parent" column="JointID" insert="false" update="false" />
</class>
</hibernate-mapping>
Any ideas on how I can just make it save without having to do two save calls?
When I set the relationship to cascade as suggested below I get foreign key constraint errors. If I analyse the queries with NHProf, its trying to use the temporary id (-1) as the JointId in the insert statement rather than the newly created parent id. The JointId in the Parent table is an Identity key, perhaps that is an issue?
You'll need to enable cascading on your <one-to-one> mapping to have this work properly.
Something like:
<one-to-one name="Child" class="Child" cascade="save-update" />
You can read up on the various cascade settings here.