We have Bar and Foo classes, they have a simple many-to-many relationship:
public partial class Bar
{
public virtual Guid BarId { get; set; } = Guid.Empty;
public virtual IList<Foo> ManyBarsInManyFoos { get; protected set; } = new Collection<Foo>();
}
public partial class Foo
{
public virtual Guid FooId { get; set; } = Guid.Empty;
public virtual IList<Bar> ManyFoosInManyBars { get; protected set; } = new Collection<Bar>();
}
At the beginning, I used bag to map this many-to-many:
<class name="Bar" table="Bars" optimistic-lock="version">
<id name="BarId" unsaved-value="{00000000-0000-0000-0000-000000000000}">
<generator class="guid" />
</id>
<timestamp name="LastChange" />
<bag name="ManyBarsInManyFoos" table="ManyBarsInManyFoos" cascade="save-update" inverse="false">
<key column="ManyBarsInManyFoosBarId" />
<many-to-many column="ManyFoosInManyBarsFooId" class="Many2Many.DomainModel.Foo" />
</bag>
</class>
<class name="Foo" table="Foos" optimistic-lock="version">
<id name="FooId" unsaved-value="{00000000-0000-0000-0000-000000000000}">
<generator class="guid" />
</id>
<timestamp name="LastChange" />
<bag name="ManyFoosInManyBars" table="ManyBarsInManyFoos" cascade="save-update" inverse="false">
<key column="ManyFoosInManyBarsFooId" />
<many-to-many column="ManyBarsInManyFoosBarId" class="Many2Many.DomainModel.Bar" />
</bag>
</class>
That works fine, but we discovered that when removing a single association (a single row in ManyBarsInManyFoos table) NHibernate delete all rows related to that BarId and then re-insert the other rows:
'UPDATE dbo.Bars SET LastChange = #p0 WHERE BarId = #p1 AND LastChange = #p2',N'#p0 datetime,#p1 uniqueidentifier,#p2 datetime',#p0='2018-05-22 17:22:17.760',#p1='F111C29E-41C1-483F-AC28-053A53713A10',#p2='2018-05-22 17:21:44.410'
'UPDATE dbo.Foos SET LastChange = #p0 WHERE FooId = #p1 AND LastChange = #p2',N'#p0 datetime,#p1 uniqueidentifier,#p2 datetime',#p0='2018-05-22 17:22:17.760',#p1='5204C39B-265F-47F7-AA30-2D104A6FCEFB',#p2='2018-05-22 17:21:44.440'
'DELETE FROM dbo.ManyBarsInManyFoos WHERE ManyBarsInManyFoosBarId = #p0',N'#p0 uniqueidentifier',#p0='F111C29E-41C1-483F-AC28-053A53713A10'
'INSERT INTO dbo.ManyBarsInManyFoos (ManyBarsInManyFoosBarId, ManyFoosInManyBarsFooId) VALUES (#p0, #p1)',N'#p0 uniqueidentifier,#p1 uniqueidentifier',#p0='F111C29E-41C1-483F-AC28-053A53713A10',#p1='D1A635A0-360D-4AC1-98EE-EE3CE47BE63C'
'INSERT INTO dbo.ManyBarsInManyFoos (ManyBarsInManyFoosBarId, ManyFoosInManyBarsFooId) VALUES (#p0, #p1)',N'#p0 uniqueidentifier,#p1 uniqueidentifier',#p0='F111C29E-41C1-483F-AC28-053A53713A10',#p1='58EA2B80-C8D9-4708-90D7-FA110816715B'
'DELETE FROM dbo.ManyBarsInManyFoos WHERE ManyFoosInManyBarsFooId = #p0',N'#p0 uniqueidentifier',#p0='5204C39B-265F-47F7-AA30-2D104A6FCEFB'
'INSERT INTO dbo.ManyBarsInManyFoos (ManyFoosInManyBarsFooId, ManyBarsInManyFoosBarId) VALUES (#p0, #p1)',N'#p0 uniqueidentifier,#p1 uniqueidentifier',#p0='5204C39B-265F-47F7-AA30-2D104A6FCEFB',#p1='B67DE063-BFD9-4AE3-BADA-2B3B5669295D'
'INSERT INTO dbo.ManyBarsInManyFoos (ManyFoosInManyBarsFooId, ManyBarsInManyFoosBarId) VALUES (#p0, #p1)',N'#p0 uniqueidentifier,#p1 uniqueidentifier',#p0='5204C39B-265F-47F7-AA30-2D104A6FCEFB',#p1='0DB243BC-3C89-43E0-AD55-C0CD8120E711'
It seems that this behaviour is needed to handle multiple relationships between same objects in the bag, that is not indexed.
But we need (due to a trigger or something similar) that existing many-to-many rows in the ManyBarsInManyFoos table are not deleted (except for that really related to the removed relation).
First question: Is there a way to avoid this "massive" delete and tell NHibernate to delete only the involved row?
Thinking that probably this is not possibile, I tried to use list instead of bag:
<list name="ManyBarsInManyFoos" table="ManyBarsInManyFoos" cascade="save-update">
<key column="ManyBarsInManyFoosBarId" />
<index column="Position"/>
<many-to-many column="ManyFoosInManyBarsFooId" class="Many2Many.DomainModel.Foo" />
</list>
-------
<list name="ManyFoosInManyBars" table="ManyBarsInManyFoos" cascade="save-update" inverse="true">
<key column="ManyFoosInManyBarsFooId" />
<index column="Position"/>
<many-to-many column="ManyBarsInManyFoosBarId" class="Many2Many.DomainModel.Bar" />
</list>
but things become a little bit more strange (for me) on removing an existing association:
'UPDATE dbo.Bars SET LastChange = #p0 WHERE BarId = #p1 AND LastChange = #p2',N'#p0 datetime,#p1 uniqueidentifier,#p2 datetime',#p0='2018-05-22 17:56:46.480',#p1='301E92EA-5E37-4948-A0FE-00884D5EB221',#p2='2018-05-22 17:56:32.180'
'DELETE FROM dbo.ManyBarsInManyFoos WHERE ManyBarsInManyFoosBarId = #p0 AND Position = #p1',N'#p0 uniqueidentifier,#p1 int',#p0='301E92EA-5E37-4948-A0FE-00884D5EB221',#p1=2
'UPDATE dbo.ManyBarsInManyFoos SET ManyFoosInManyBarsFooId = #p0 WHERE ManyBarsInManyFoosBarId = #p1 AND Position = #p2',N'#p0 uniqueidentifier,#p1 uniqueidentifier,#p2 int',#p0='CE0C01A3-4F0E-4E93-A33C-22AAE68482D9',#p1='301E92EA-5E37-4948-A0FE-00884D5EB221',#p2=0
'UPDATE dbo.ManyBarsInManyFoos SET ManyFoosInManyBarsFooId = #p0 WHERE ManyBarsInManyFoosBarId = #p1 AND Position = #p2',N'#p0 uniqueidentifier,#p1 uniqueidentifier,#p2 int',#p0='193C5714-FD1C-4C51-AD5F-6B19F262046F',#p1='301E92EA-5E37-4948-A0FE-00884D5EB221',#p2=1
Other rows are not deleted but may be "reused" and "switched" behind the scenes from an object to another. Not a valid solution.
Finally, I tried to use map... Tried, but I was not able to understand how I can create the right mapping tags for this simple relationship.
Therefore, second question: How can I map this many-to-many with map tag? And/or how can we map this many-to-many relationship in an optimized way, having NHibernate to simply delete the single involved row in ManyBarsInManyFoos table, instead of deleting many rows to reinsert those after a while or "reassign" relation rows to different objects?
Related
I have the following two tables:
Jobs AreaID, JobNo (composite key)
Logs LogID, AreaID, JobNo
I need to get all jobs that don't have any logs associated with them. In SQL I could do:
SELECT Jobs.AreaID,
Jobs.JobNo
FROM Jobs
LEFT JOIN Logs
ON Jobs.AreaID = Logs.AreaID
AND Jobs.JobNo = Logs.JobNo
WHERE Logs.LogID is null
But I'm not sure how to accomplish this with NHibernate. Could anyone offer any pointers?
Here are my mappings:
<class name="Job" table="Jobs">
<composite-key name="Id">
<key-property name="JobNo"/>
<key-many-to-one name="Area" class="Area" column="AreaID"/>
</composite-key>
</class>
<class name="Log" table="Logs">
<id name="Id" column="LogID">
<generator class="identity"/>
</id>
<property name="JobNo"/>
<many-to-one name="Area" class="Area" column="AreaID"/>
</class>
Thanks
Update
OK, I modified Nosila's answer slightly, and this is now doing what I wanted:
Log logs = null;
return session.QueryOver<Job>()
.Left.JoinAlias(x => x.Logs, () => logs)
.Where(x => logs.Id == null)
.List<Job>();
I also had to add this to my Job mapping:
<bag name="Logs">
<key>
<column name="JobNo"></column>
<column name="DivisionID"></column>
</key>
<one-to-many class="Log"/>
</bag>
Thanks for the help. :)
I'm not familiar with composite identifiers as I don't use them so for all I know NHibernate will automatically create the proper left join. None the less, the (non-tested) query below should get you started.
Job jobAlias = null;
Log logAlias = null;
YourDto yourDto = null;
session.QueryOver<Job>()
// Here is where we set what columns we want to project (e.g. select)
.SelectList(x => x
.Select(x => x.AreaID).WithAlias(() => jobAlias.AreaID)
.Select(x => x.JobNo).WithAlias(() => jobAlias.JobNo)
)
.Left.JoinAlias(x => x.Logs, () => logAlias, x.JobNo == logAlias.JobNo)
.Where(() => logAlias.LogID == null)
// This is where NHibernate will transform what you have in your `SelectList()` to a list of objects
.TransformUsing(Transformers.AliasToBean<YourDto>())
.List<YourDto>();
public class YourDto
{
public int AreaID { get; set; }
public int JobNo { get; set; }
}
Note: You need NHibernate 3.2 in order to set join conditions.
Job job = null;
var jobsWithoutLogs = session.QueryOver(() => job)
.WithSubquery.WhereNotExists(QueryOver.Of<Log>()
.Where(log => log.Job == job)
.Select(Projections.Id()))
.List()
Update: i saw you added the mapping. The Above Code only works for the following mapping
<class name="Log" table="Logs">
<id name="Id" column="LogID">
<generator class="identity"/>
</id>
<many-to-one name="Job" >
<column name="JobNo"/>
<column name="AreaID"/>
<many-to-one />
</class>
Hey,
I have to map the following entities:
class Document
{
public int DocumentId { get; set; }
public DocumentList ContainingList { get; set; }
}
class DocumentList
{
public int DocumentListId { get; set; }
public DateTime LastUpdateTime { get; set; }
public IList<Doucment> Documents { get; set; }
}
With the constraint that only one DocumentList can own a specific document (altough a collection-table exists here).
Mapping has to rely on the following tables (which cannot be changed, for the sake of simplicity):
TB_DOC
------
DOC_ID (int, PK)
DOC_CONTENT (blob)
TB_DOC_LIST
-----------
DOC_LIST_ID (int, PK)
DOC_LIST_UPDATE_TIME (datetime)
TB_LIST_AND_DOCS
----------------
DOC_LIST_ID
DOC_ID
So the mapping i tohught of would be like this:
enter code here
<class name="DocumentList" table="TB_DOC_LIST">
<id name="DocumentListId">
<column name="DOC_LIST_ID"/>
<generator class="assigned" />
</id>
<property name="LastUpdateTime" column="DOC_LIST_UPDATE_TIME ">
<set name="Documents" table="TB_LIST_AND_DOCS">
<key column="DOC_ID"></key>
<one-to-many class="Document" />
</set>
</class>
and:
<class name="Document" table="TB_DOC">
<id name="DocumentId">
<column name="DOC_ID"/>
<generator class="assigned" />
</id>
[ ??? ] - property to reference the "owner" document list
</class>
Now, following the known patterns, i can't figure out how should the back-link from Document to the DocumentList be mapped, since i have a "weired| one-to-many relation here, broken by a third table.
I also don't want a Document object to reference an IList to solve this with back-referencing many-to-many, since each Document has only one such "owner" DocumentList.
Any elegant idea? what am i mispercepting here?
cant test it right now, but a join could be used to get the reference id.
<class name="Document" table="TB_DOC">
<id name="DocumentId">
<column name="DOC_ID"/>
<generator class="assigned" />
</id>
<join table="TB_LIST_AND_DOCS">
<key column="DOC_ID"/>
<many-to-one class="DocumentList">
<column name="DOC_LIST_ID" />
</many-to-one>
</join>
</class>
I am customizing N2CMS's database structure, and met with an issue. The two classes are listed below.
public class Customer : ContentItem
{
public IList<License> Licenses { get; set; }
}
public class License : ContentItem
{
public Customer Customer { get; set; }
}
The nhibernate mapping are as follows.
<class name="N2.ContentItem,N2" table="n2item">
<cache usage="read-write" />
<id name="ID" column="ID" type="Int32" unsaved-value="0" access="property">
<generator class="native" />
</id>
<discriminator column="Type" type="String" />
</class>
<subclass name="My.Customer,My" extends="N2.ContentItem,N2" discriminator-value="Customer">
<join table="Customer">
<key column="ItemID" />
<bag name="Licenses" generic="true" inverse="true">
<key column="CustomerID" />
<one-to-many class="My.License,My"/>
</bag>
</join>
</subclass>
<subclass name="My.License,My" extends="N2.ContentItem,N2" discriminator-value="License">
<join table="License" fetch="select">
<key column="ItemID" />
<many-to-one name="Customer" column="CustomerID" class="My.Customer,My" not-null="false" />
</join>
</subclass>
Then, when get an instance of Customer, the customer.Licenses is always empty, but actually there are licenses in the database for the customer. When I check the nhibernate log file, I find that the SQL query is like:
SELECT licenses0_.CustomerID as CustomerID1_,
licenses0_.ID as ID1_,
licenses0_.ID as ID2_0_,
licenses0_1_.CustomerID as CustomerID7_0_,
FROM n2item licenses0_
inner join License licenses0_1_
on licenses0_.ID = licenses0_1_.ItemID
WHERE licenses0_.CustomerID = 12 /* #p0 */
It seems that nhibernate believes that the CustomerID is in the 'n2item' table. I don't know why, but to make it work, I think the SQL should be something like this.
SELECT licenses0_.ID as ID1_,
licenses0_.ID as ID2_0_,
licenses0_1_.CustomerID as CustomerID7_0_,
FROM n2item licenses0_
inner join License licenses0_1_
on licenses0_.ID = licenses0_1_.ItemID
WHERE licenses0_1_.CustomerID = 12 /* #p0 */
Could any one point out what's wrong with my mappings? And how can I get the correct licenses of one customer? Thanks in advance.
I'm not sure whether the SQL is incorrect, because the parent class mapping uses a discriminator so I'd expect all properties to be stored in the same table as the base class (n2item). However I'm not familiar with the "join table" syntax, I generally use joined-subclass so I might be misunderstanding.
Assuming the subclass mapping is correct, could the problem with the licenses be something to do with no Cascade setting being set for that collection?
Using NHibernate I want to filter a collection in a class to contain ONLY a subset of possible objects. Below I am including a sample table data to help explain. I can find no way to do this using NHibernate.
Table:DataObject
DataObjectId(PK) / Name / CurrentVersion
11 "data.txt" 2
12 "info.txt" 3
Table:DataObjectVersion
Id / Comment / VersionNumber / DataObjectId(FK)
31 "Genesis" 1 11 <= Ignore this object
32 "Changed data" 2 11 <= Get this object
34 "Genesis" 1 12 <= Ignore this object
35 "Changed info" 2 12 <= Ignore this object
36 "Added info" 3 12 <= Get this object
I want to join on a non-foreign key DataObject.CurrentVersion = DataObjectVersion.VersionNumber for each DataObject in one command.
Here are the classes and mapping files:
public class DataObject
{
public virtual int DataObjectId { get; set; }
public virtual string Name { get; set; }
public virtual int CurrentVersionNumber { get; set; }
public virtual IList<DataObjectVersion> Versions { get; set; }
}
<class name="DataObject" table="DataObject" lazy="false">
<id name="DataObjectId" column="DataObjectId" type="int">
<generator class="assigned" />
</id>
<property name="Name" column="Name" type="String(512)" />
<property name="CurrentVersionNumber" column="CurrentVersionNumber" type="int" />
<bag name="Versions" cascade="all-delete-orphan" inverse="true" lazy="false" >
<key column="DataObjectId" />
<one-to-many class="DataObjectVersion" />
</bag>
</class>
public class DataObjectVersion
{
public virtual int DataObjectVersionId { get; set; }
public virtual string Comment { get; set; }
public virtual int VersionNumber { get; set; }
public virtual int DataObjectId { get; set; }
}
<class name="DataObjectVersion" table="DataObjectVersion" lazy="false">
<id name="Id" column="DataObjectVersionId" type="int">
<generator class="assigned" />
</id>
<property name="Comment" column="Comment" type="String(512)" />
<property name="VersionNumber" column="VersionNumber" type="int" />
<property name="DataObjectId" column="DataObjectId" type="int" />
</class>
if you want to filter the collection on demand, using a filter is a valid choice.
You would need to declare the filter on both the Version class and in the bag element and apply the filter from the NHibernateSession.EnableFilter method
if you always want to fetch a single Version in the bag then implement a 'where' in the mapping of the bag:
<bag name="Versions" cascade="all-delete-orphan" inverse="true" lazy="false" where="CurrentVersionNumber = Versions.VersionNumber" >
<key column="DataObjectId" />
<one-to-many class="DataObjectVersion" />
</bag>
note that in the 'where' you write proper SQL not HQL and as such the proper SQL i write above probably has to be changed to reflect your schema
Additionally if a single object is to be fetched setting up a bag and the according IList may be an overkill.
Applying a formula property and a DataObjectVersion object in the class may be more appropriate
in the class DataObject replace the IList with
public virtual DataObjectVersion Version { get; set; }
and in the mapping replace the 'bag' with something in the lines of
<property name="Version" type="DataObjectVersion" update="false" insert="false" formula="(select v.DataObjectVersionId, v.Comments, v.VersionNumber, v.DataObjectId from DataObjectVersion v where v.VersionNumber = CurrentVersionNumber)" />
again only proper SQL is allowed
i've used computed properties with native datatypes (datetime, string etc) and fetching an Entity may (or may not) need something more or different
Last but not least, you could apply a filter on the collection after you have fetched the primary object DataObject by creating a filter on the collection
IList<DataObjectVersion> fVersion =
NHibernateSession.CreateFilter(do.Versions, "where VersionNumber = :ver")
.SetParameter("ver", do.CurrentVersionNumber)
.List<DataObjectVersion>();
where the do.Versions collection is not initialized, only the results fetched in the separate fVersion collection and this is a second SELECT after already having made the round-trip to the db for the DataObject fetch.
Presumably your VersionNumber increments as the user changes the data and you're trying to get the latest one. If you consider the VersionNumber as an "Age" field instead (i.e. where 0 is the latest / youngest version, 1 is the next oldest and so on) then your problems becomes how to get all the entities with an Age of 0. This can be done using a filter: http://nhibernate.info/doc/nh/en/index.html#objectstate-filters
I have a class with following description:
public class Customer {
public ISet<Client> Contacts { get; protected set;}
}
I want to map Contacts property onto following table:
CREATE TABLE user_contacts (
user1 uuid NOT NULL,
user2 uuid NOT NULL
)
I want it to map bidirectionally, i.e. when Customer1 added to Customer2's Contacts, Customer1's Contacts collection should contain Customer2 (maybe only after entity reload). How could I do that?
Update Sure I can map left-to-right and right-to-left sets and then combine then at runtime, but it'll... hmm... untasty... Is there other solution? Any way, thank you very match, FryHard!
Take a look at this link on what hibernate calls unidirectional many-to-many associations. In Castle ActiveRecord I make use of HasAndBelongsToMany links, but I am not sure how exactly it is mapped in nhibernate.
Though taking a look at your question a little deeper, it looks like you will be linking bidirectionally from customer to user_contacts, which could break the many-many link. I will play with an example and see what I can come up with.
An Export of the hbm files from ActiveRecord shows this
<?xml version="1.0" encoding="utf-16"?>
<hibernate-mapping auto-import="true" default-lazy="false" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:nhibernate-mapping-2.2">
<class name="NHibernateMapping.Customer, NHibernateMapping" table="Customer" schema="dbo">
<id name="Id" access="property" column="Id" type="Int32" unsaved-value="0">
<generator class="identity">
</generator>
</id>
<property name="LastName" access="property" type="String">
<column name="LastName" not-null="true"/>
</property>
<bag name="ChildContacts" access="property" table="user_contacts" lazy="false">
<key column="user1" />
<many-to-many class="NHibernateMapping.Customer, NHibernateMapping" column="user2"/>
</bag>
<bag name="ParentContacts" access="property" table="user_contacts" lazy="false" inverse="true">
<key column="user2" />
<many-to-many class="NHibernateMapping.Customer, NHibernateMapping" column="user1"/>
</bag>
</class>
</hibernate-mapping>
ActiveRecord example:
[ActiveRecord("Customer", Schema = "dbo")]
public class Customer
{
[PrimaryKey(PrimaryKeyType.Identity, "Id", ColumnType = "Int32")]
public virtual int Id { get; set; }
[Property("LastName", ColumnType = "String", NotNull = true)]
public virtual string LastName { get; set; }
[HasAndBelongsToMany(typeof(Customer), Table = "user_contacts", ColumnKey = "user1", ColumnRef = "user2")]
public IList<Customer> ChildContacts { get; set; }
[HasAndBelongsToMany(typeof(Customer), Table = "user_contacts", ColumnKey = "user2", ColumnRef = "user1", Inverse = true)]
public IList<Customer> ParentContacts { get; set; }
}
Hope it helps!