My code is accumulating object deletes and updates from the user and then tries to apply those in one shot. In the case I'm having troubles, it is a business requirement that the parent is only deleted if there are no children left, but the children have to be deleted manually.
I can reproduce the problem with the following code:
Mapping:
<class name="SomeClass" >
<id name="ID">
<generator class="native" />
</id>
<property name="Name" />
<many-to-one name="Parent" not-null="true" />
</class>
<class name="SomeParent" >
<id name="ID">
<generator class="native" />
</id>
<property name="Name" />
<set name="Children" batch-size="100" inverse="true">
<key column="Parent" not-null="true" />
<one-to-many class="SomeClass" />
</set>
</class>
Entities:
public class SomeClass
{
public virtual int ID { get; set; }
public virtual string Name { get; set; }
public virtual SomeParent Parent { get; set; }
}
public class SomeParent
{
public virtual int ID { get; set; }
public virtual string Name { get; set; }
public virtual ISet<SomeClass> Children { get; set; }
}
Program:
using (var sessionFactory = cfg.BuildSessionFactory())
using (var session = sessionFactory.OpenSession())
{
var p = new SomeParent();
var obj = new SomeClass() { Parent = p };
session.Save(p);
session.Save(obj);
session.Flush();
}
using (var sessionFactory = cfg.BuildSessionFactory())
using (var session = sessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
foreach (var p in session.CreateCriteria<SomeParent>().List<SomeParent>())
{
session.Delete(p);
}
foreach (var p in session.CreateCriteria<SomeClass>().List<SomeClass>())
{
session.Delete(p);
}
session.Flush();
tx.Commit();
}
This example works when I exchange the two Delete() loops. In my application I do not have the luxury of being able to influence the order in which users press the delete button.
This example also works, if I add cascade="delete" to the one-to-many mapping. This violates the business requirement that parents cannot be deleted while they still have children.
I would have expected NHibernate to handle this ordering for me. Especially as all necessary information is already available in the mapping. Am I simply doing something wrong or do I have to "manually" sort the deletes properly so that NHibernate will grok them?
I'm using NHibernate 3.2.0GA (build 3.2.0.4000 from nuget).
I don't understand your expectation that NHibernate should be able to handle this scenario. Since you are not specifying a cascade setting, then you have to handle the deletes yourself.
My suggestion is to keep track of the deletes in a collection and do not issue NHibernate Deletes until the data is validated. That is, create MarkForDeletion methods for parent and child objects. When the user clicks delete, these methods add the object to a collection.
When the user is ready to commit the transaction, loop through the child objects that have been marked for deletion, remove them from the parent's children collection, and set their reference to parent to null. Then loop through the parent objects that are marked for deletion and validate that their children collection is empty.
If the data is valid, you can then delete the parent objects and let the cascade setting (all or all-delete-orphan) take care of the child records.
Related
I have an entity where a composite id is used. I changed to code to make use of wrapping the composite id in a seperate key class. I expected that with Linq I could do a comparison on key object and with the Criteria API to use Restrictions.IdEq but both fail. I need to explicitly compare the key values to make it work.
I cannot find any documentation if this should work so for the moment I am stuck with direct comparisons but this means that when I alter the key that I also need to update the query code which is obviously not what I would want.
As a side note, I tried this with NHibernate 3.0.0 Alpha 2 and 3.
Domain
Mapping
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="Cwc.Pulse.Dal"
namespace="Cwc.Pulse.Dal">
<class name="AddonStatus">
<composite-id name="Id">
<key-many-to-one name="Context" column="Context_Id" class="Context" />
<key-property name="AddonType" column="Addon_Id"/>
</composite-id>
<property name="Status" />
</class>
</hibernate-mapping>
Class
public class AddonStatus
{
public virtual string Status { get; set; }
public virtual Key Id { get; protected set; }
public AddonStatus()
{
Id = new Key();
}
public class Key
{
public virtual Context Context { get; set; }
public virtual AddonType AddonType { get; set; }
public override int GetHashCode()
{
return ContextId.GetHashCode() ^ AddonType.GetHashCode();
}
public override bool Equals(object obj)
{
if (this == obj) return true;
var o = obj as Key;
if (null == o) return false;
return Context == o.Context && AddonType == o.AddonType;
}
}
}
Working queries
The queries below work and as you can see I compare the key values explicitly. I do not compare the key object.
Linq
from status
in session.Query<AddonStatus>()
where status.Id.Context == context && status.Id.AddonType == addonType
select status
Criteria API
session.CreateCriteria<AddonStatus>()
.Add(Restrictions.Eq("Id.Context", context))
.Add(Restrictions.Eq("Id.AddonType", addonType))
Expected to work but dont
I expect the following queries to work. Either in efficiently for linq in memory instead of the database but I expect the criteria api to be smart enough to handle such composite id´s in queries.
Both linq and criteria api queries make use of a Key object comparison.
var key = new AddonStatus.Key
{
Context = context,
AddonType = addonType
};
Linq
from status
in session.Query<AddonStatus>()
where status.Id == key
select status
Criteria API
session.CreateCriteria<AddonStatus>()
.Add(Restrictions.IdEq(key))
So if anyone has such a scenario working then what am I doing wrong?
Not directly an answer to your question, but it may be useful to you anyway. You could avoid the (explicit) composite key by mapping the AddonStatus as composite-element on the owner (most probably the Context):
<class name="Context">
<map name="AddonStates" table="AddonStatus">
<key column="Context_Id" /> <!-- Foreign key to the Context -->
<index column="Addon_Id" /> <!-- Dictionary key -->
<composite-element>
<property name="Status" /> <!-- data -->
</composite-element>
</map>
</class>
In the class Context is looks like this:
class Context
{
IDictionary<AddonType, AddonStatus> AddonStates { get; private set; }
}
This results and pretty the same database structure, but it is different to work with. I can't say if this is what you actually want, but it just looks like it.
Interestingly, I'm getting almost the exact opposite of this behavior in 2.1.2.
My mapping (simplified):
<!-- Subscriber class -->
<class name="Subscriber" >
<composite-id name="SubscriberKey" class="SubscriberKey">
<key-property name="Request" column="RequestID" type="int"/>
<key-many-to-one name="User" column="UserID" class="User" not-found="ignore" />
</composite-id>
<!-- User class - note that this goes to a different schema,
and is not mutable. Who knows if that's important... -->
<class name="User" schema="AnotherDb.dbo" mutable="false">
<id name="Id" column="UserID" type="int">
<generator class="native" />
</id>
<property name="FirstName" column="FirstName" type="string" />
<property name="LastName" column="LastName" type="string" />
goes to:
public class User
{
public virtual int? Id {get; protected set;}
public virtual string FirstName { get; protected set; }
public virtual string LastName { get; protected set; }
public User() { }
}
public class Subscriber
{
public virtual SubscriberKey SubscriberKey { get; set; }
public virtual User User { get; set; }
public Subscriber() { }
}
public class SubscriberKey
{
public override bool Equals(object obj)
{
if (obj is SubscriberKey && obj != null)
return ((SubscriberKey)obj).Request == Request
&& ((SubscriberKey)obj).User.Id == User.Id;
return false;
}
public override int GetHashCode()
{
return (Request.ToString() + User.Id.ToString()).GetHashCode();
}
public virtual int Request { get; set; }
public virtual User User { get; set; }
public SubscriberKey() { }
}
Things which work:
CreateCriteria<Subscriber>()
.Add(Restrictions.IdEq(keyInstance))
.UniqueResult<Subscriber>();
CreateCriteria<Subscriber>()
.Add(Restrictions.Eq("SubscriberKey.User.Id", aUserID))
.Add(Restrictions.Eq("SubscriberKey.Request", aRequestID))
.UniqueResult<Subscriber>();
Things which don't work:
Get<Subscriber>(keyInstance);
I'm thinking this is an inconsistency between their various ID-equaling query forms. When I get time, I'll be building a minimal unit test to submit as a bug example. I'd be interested in any / all thoughts anyone might have on this...
edit: Heeey, I figured it out!
Things which do work, now that I've read this
Get<Subscriber>(new SubscriberKey() {
User = Load<User>(aUserID), // the important part!
Request = aRequestID
});
This will create a proxy object for the User key, without hitting the database (unless necessary). If you swap Load<User> for Get<User>, you'll immediately hit the database to populate the object, rather than respecting your lazy-loading properties. Use Load.
And things like this are precisely why people suggest the (type)Repository pattern - I can do this behind the scenes: Get<>(new SK(){User=Load<>(key.User.Id)}, and still Get(key) by a single key, identical to every other object.
I would like to delete the ICollection PriceBreaks from Product.
I'm using the following method. However they dont seem to delete. What am i missing.
When i step thru. i notice that "product.PriceBreaks.Clear();" doesn't actually clear the items. Do i need to flush or something?
public void RemovePriceBreak(int productId)
{
using (ISession session = EStore.Domain.Helpers.NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
var product = session.Get<Product>(productId);
// i can see 20 PriceBreak records, tho nh prof saids PriceBreak table hasn't been hit
product.PriceBreaks.Clear();
session.SaveOrUpdate(product);
transaction.Commit(); // PriceBreak table is hit here with select statement, no delete?
}
}
Here are my hbm files
<class name="Product" table="Products">
<id name="Id" type="Int32" column="Id" unsaved-value="0">
<generator class="identity"/>
</id>
<property name="CompanyId" column="CompanyId" type="Int32" not-null="true" />
<property name="Name" column="Name"/>
<set name="PriceBreaks" table="PriceBreaks" generic="true" cascade="all-delete-orphan" inverse="true" >
<key column="ProductId" />
<one-to-many class="EStore.Domain.Model.PriceBreak, EStore.Domain" />
</set>
</class>
<class name="PriceBreak" table="PriceBreaks">
<id name="Id" type="Int32" column="Id" unsaved-value="0">
<generator class="identity"/>
</id>
<many-to-one name="Product" column="ProductId" not-null="true" class="EStore.Domain.Model.Product, EStore.Domain" />
</class>
My Entities
public class Product
{
public virtual int Id { get; set; }
public virtual ICollection<PriceBreak> PriceBreaks { get; set; }
public virtual void AddPriceBreak(PriceBreak priceBreak)
{
priceBreak.Product = this;
PriceBreaks.Add(priceBreak);
}
}
public class PriceBreak
{
public virtual int Id { get; set; }
public virtual Product Product { get; set; }
}
Here is the sql from nhprof
//
SELECT product0_.Id as Id0_0_
FROM Products product0_
WHERE product0_.Id = 23 /* #p0 */
and then
SELECT pricebreak0_.ProductId as ProductId1_,
pricebreak0_.Id as Id1_,
pricebreak0_.Id as Id1_0_,
pricebreak0_.ProductId as ProductId1_0_,
FROM PriceBreaks pricebreak0_
WHERE pricebreak0_.ProductId = 23 /* #p0 */
there is no update or select
Product probably needs to be associated with that session. It looks like you are trying to save a detached object. Which means there is no session to track that call to clear the PriceBreaks. Change it to this...
public void RemovePriceBreak(int productId)
{
using (ISession session = EStore.Domain.Helpers.NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
var product = session.Get<Product>(productId);
product.PriceBreaks.Clear();
session.SaveOrUpdate(product);
transaction.Commit();
}
}
or move the session and transaction management further up. Which would basically eliminate the need for this method.
Have you tried deleting the cascade="all" on the PriceBreak side ?
Have you tried using session.Save() instead of SaveOrUpdate() ?
Assume the following entity classes:
public class Player
{
public virtual int ID { get; set; }
public virtual string Name { get; set; }
public virtual Team Team { get; set; }
}
public class Team
{
public virtual int ID { get; set; }
public virtual string City { get; set; }
public virtual string Nickname { get; set; }
}
Assume the following mapping class for Player:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false">
<class name="Player">
<id name="ID" column="ID" type="System.Int32" unsaved-value="null">
<generator class="native"/>
</id>
<property name="Name" column="Name" not-null="true" type="System.String" length="50" insert="true" update="true"/>
<many-to-one name="Team" not-null="true" outer-join="auto" insert="true" update="true">
<column name="TeamID"/>
</many-to-one>
</class>
</hibernate-mapping>
And assume the following Player repository method:
public void Add(Player player)
{
using (ISession session = NHibernateHelper.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
session.Save(player);
transaction.Commit();
}
}
}
My question:
Must I load a full-fledged Team (parent object) when I want to create a new Player?
Or can I specify a "mock" object, and only specify the foreign key?
Player player = new Player
{
Name = "Tom Brady",
Team = new TeamRepository().GetTeamByCityAndNickname("New England", "Patriots") // Is this the only way?
// or can I do this?
// Team = new Team { ID = 22 }
};
new PlayerRepository().Add(player);
And if I can't specify a "mock"
object (specifying only the
foreign key), can you please explain
why I can't?
That is, can you please give me an idea about what's going on under the hood?
Heads-up:
A fellow had nearly the same question.
Here's the answer that made the most sense.
Interestingly, when speaking about EF
4.0 during a DotNetRocks episode, Julia Lerman
acknowledged that many people want to
use the foreign key in these types of
situations.
EDIT: This answer points to the essence of my question.
Think of it like having an object that
only keeps the Id and that will load
the rest if you ever need it. If
you're just passing it arround to
create relationships (like FKs), the
id is all you'll ever need.
Well if that's the case, then why do I need to be worried about proxy objects and such? Why can't I just create a "dummy" object and specify the foreign key value if that's all that really matters?
you use the foreign key like so...
Team = session.Load<Team>(id);
know the difference between load and get
If you have access to Session at this point you can call
Team = Session.Load<Team>(id);
The premise of Load is that it will create an NHibernate proxy that can resolve itself if needed. Of course you have to be sure the id exists or you will get an EntityNotFound error if it ever tries to resolve itself.
I have the following database schema:
http://lh4.ggpht.com/_SDci0Pf3tzU/SdM3XnAmmxI/AAAAAAAAEps/Ie3xW3ZVNfQ/s400/styleerror.png
And this is my mapping file:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="CodeSmithSampel.Generated.BusinessObjects" assembly="CodeSmithSampel">
<class name="CodeSmithSampel.Generated.BusinessObjects.Store, CodeSmithSampel" table="store" lazy="true">
<id name="Id" column="Id">
<generator class="native" />
</id>
<property name="Name" column="Name" />
<bag name="Employees" lazy="true" cascade="all-delete-orphan" inverse="true" >
<key column="Store_id"></key>
<one-to-many class="Employee"></one-to-many>
</bag>
<bag name="Products" table="storeproduct" lazy="true" cascade="all" inverse="true" >
<key column="Store_id"></key>
<many-to-many column="Product_id" class="Product" />
</bag>
</class>
</hibernate-mapping>
And ths is my Store entity class:
public partial class Store : BusinessBase<int>
{
#region Declarations
private string _name = String.Empty;
private IList<Employee> _employees = new List<Employee>();
private IList<Product> _products = new List<Product>();
#endregion
#region Constructors
public Store() { }
#endregion
#region Methods
public override int GetHashCode()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append(this.GetType().FullName);
sb.Append(_name);
return sb.ToString().GetHashCode();
}
#endregion
#region Properties
public virtual string Name
{
get { return _name; }
set
{
OnNameChanging();
_name = value;
OnNameChanged();
}
}
partial void OnNameChanging();
partial void OnNameChanged();
public virtual IList<Employee> Employees
{
get { return _employees; }
set
{
OnEmployeesChanging();
_employees = value;
OnEmployeesChanged();
}
}
partial void OnEmployeesChanging();
partial void OnEmployeesChanged();
public virtual IList<Product> Products
{
get { return _products; }
set
{
OnProductsChanging();
_products = value;
OnProductsChanged();
}
}
partial void OnProductsChanging();
partial void OnProductsChanged();
#endregion
}
The product class:
public partial class Product : BusinessBase<int>
{
#region Declarations
private float _price = default(Single);
private string _name = null;
private IList<Store> _stores = new List<Store>();
#endregion
#region Constructors
public Product() { }
#endregion
#region Methods
public override int GetHashCode()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append(this.GetType().FullName);
sb.Append(_price);
sb.Append(_name);
return sb.ToString().GetHashCode();
}
#endregion
#region Properties
public virtual float Price
{
get { return _price; }
set
{
OnPriceChanging();
_price = value;
OnPriceChanged();
}
}
partial void OnPriceChanging();
partial void OnPriceChanged();
public virtual string Name
{
get { return _name; }
set
{
OnNameChanging();
_name = value;
OnNameChanged();
}
}
partial void OnNameChanging();
partial void OnNameChanged();
public virtual IList<Store> Stores
{
get { return _stores; }
set
{
OnStoresChanging();
_stores = value;
OnStoresChanged();
}
}
partial void OnStoresChanging();
partial void OnStoresChanged();
#endregion
}
The mapping for the Product class:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="CodeSmithSampel.Generated.BusinessObjects" assembly="CodeSmithSampel">
<class name="CodeSmithSampel.Generated.BusinessObjects.Product, CodeSmithSampel" table="product" lazy="true">
<id name="Id" column="Id">
<generator class="native" />
</id>
<property name="Price" column="Price" />
<property name="Name" column="Name" />
<bag name="Stores" table="storeproduct" lazy="true" cascade="all" inverse="true" >
<key column="Product_id"></key>
<many-to-many column="Store_id" class="Store" />
</bag>
</class>
</hibernate-mapping>
What is particularly weird is that when I add a Store object to one of the product, the database record is not updated; the add doesn't seem to take place, although the new store object exists in the database:
IManagerFactory managerFactory = new ManagerFactory();
var productManager = managerFactory.GetProductManager();
var myProduct= productManager.GetById(2);
var myStore = new Store();
myStore.Name = "new Store"; //a "new store" entry is created in the Store table
myProduct.Stores.Add(myStore); // but this "new store" is not linked to the myproduct, as it should.
productManager.Session.CommitChanges();
Is there anything I miss?
Note: I generate the above code using CodeSmith.
Edit: The accepted answer works. The reason I got in this problem is because
Only one entity class should have inverse = true, not two. So either Product or Store should set the inverse to false. The code generation tool didn't handle this properly.
The correct way to Add Many to Many relationship is explained below. You must add two times.
Can this have anything to do with the fact that you have a surrogate key in the storeproducts table ?
What happens if you remove this surrogate key column Id, and put the primary key on the combination of the product_id and store_id columns ?
I believe that, if you want to have a surrogate key on the storeproducts table, you'll have to create yet another entity.
If you want to use the surrogate key, you'll have to use the idbag mapping.
How does your Product class and mapping look like ?
I see that you specify the 'inverse' attribute in your mapping of the Products collection in the Store entity.
If you do this (and thus you have a bi-directional association), then you should add the Store to the Stores collection of the product as well.
Since -from the NH documentation- :
Changes made only to the inverse end
of the association are not persisted.
This means that NHibernate has two
representations in memory for every
bidirectional association, one link
from A to B and another link from B to
A. This is easier to understand if you
think about the .NET object model and
how we create a many-to-many
relationship in C#:
category.Items.Add(item); // The category now "knows" about the relationship
item.Categories.Add(category); // The item now "knows" about the relationship
session.Update(item); // No effect, nothing will be saved!
session.Update(category); // The relationship will be saved
The non-inverse side is used to save
the in-memory representation to the
database. We would get an unneccessary
INSERT/UPDATE and probably even a
foreign key violation if both would
trigger changes! The same is of course
also true for bidirectional
one-to-many associations.
You may map a bidirectional
one-to-many association by mapping a
one-to-many association to the same
table column(s) as a many-to-one
association and declaring the
many-valued end inverse="true".
This means, that only one of the ends should be inverse.
Adding a Product to a store, should be done like this:
public class Store
{
public void AddProduct( Product p )
{
if( _products.Contains (p) == false )
{
_products.Add (p);
p.AddStore(this);
}
}
}
public class Product
{
public void AddStore( Store s )
{
if( _stores.Contains (s) == false )
{
_stores.Add (s);
s.AddProduct(this);
}
}
}
(Very important to check whether the collection already contains the item to be added; otherwise you'll end up in an infinite loop.
We need to map simple class using NHibernate:
public class CatalogItem
{
private IList<CatalogItem> children = new List<CatalogItem>();
public Guid Id { get; set; }
public string Name { get; set; }
public CatalogItem Parent { get; set; }
public IList<CatalogItem> Children
{
get { return children; }
}
public bool IsRoot { get { return Parent == null; } }
public bool IsLeaf { get { return Children.Count == 0; } }
}
There are a batch of tutorials in the internet on this subject, but none of them cover little nasty detail: we need order to be preserved in Children collection. We've tried following mapping, but it led to strange exeptions thrown by NHibernate ("Non-static method requires a target.").
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="Domain.Model" assembly="Domain">
<class name="CatalogItem" lazy="false">
<id name="Id" type="guid">
<generator class="guid" />
</id>
<property name="Name" />
<many-to-one name="Parent" class="CatalogItem" lazy="false" />
<list name="Children" cascade="all">
<key property-ref="Parent"/>
<index column="weight" type="Int32" />
<one-to-many not-found="exception" class="CatalogItem"/>
</list>
</class>
</hibernate-mapping>
Does anyone have any thoughts?
I'm no expert, but <key property-ref=...> looks strange to me in this usage. You should be able to do <key column="ParentID"/>, and NHibernate will automatically use the primary key of the associated class -- itself, in this case.
You may also need to set the list to inverse="true", since the relationship is bidirectional. [See section 6.8 in the docs.]