I've a One to Many mapping.
An entity with a collection persisted on two table 1:N
This is the class and the mapping:
public class Test
{
public virtual string Id { get; set; }
public virtual string Description { get; set; }
public virtual IList<TestItem> Items { get; set; }
public Test()
{
Items = new List<TestItem>();
}
}
public class TestItem
{
public virtual string Id { get; set; }
public virtual Test Test { get; set; }
public virtual string ItemCode { get; set; }
public virtual string ItemData { get; set; }
}
public class TestMap : ClassMapping<Test>
{
public TestMap()
{
Id(x => x.Id, m => m.Column("IDTest"));
Bag(x => x.Items, c =>
{
c.Key(k =>
{
k.NotNullable(true);
k.Column("IDTest");
});
c.Cascade(Cascade.DeleteOrphans);
c.Lazy(CollectionLazy.NoLazy);
c.Inverse(true);
}, r => r.OneToMany(m =>
{
m.NotFound(NotFoundMode.Exception);
m.Class(typeof(TestItem));
}));
}
}
public class TestItemMap : ClassMapping<TestItem>
{
public TestItemMap()
{
Id(x => x.Id, m => m.Column("IDTestItem"));
ManyToOne(x => x.Test, m =>
{
m.Column("IDTest");
m.NotNullable(false);
m.Lazy(LazyRelation.NoLazy);
});
Property(x => x.ItemCode);
Property(x => x.ItemData);
}
}
And this is the code.
If I remove the marked line I get the error.
var session = SessionFactory.OpenSession();
using (var tr = session.BeginTransaction())
{
var test = new Test();
test.Id = "T01";
test.Description = "Desc TEST 01";
session.SaveOrUpdate(test); // If Removed Get INSERT ERROR on TestItem
var item = new TestItem { Id = "NEW01", ItemCode = "A", Test = test, ItemData = "New T01A" };
test.Items.Add(item);
session.SaveOrUpdate(item);
session.SaveOrUpdate(test);
tr.Commit();
}
My question is:
Is this the best practice to persist a "one to many" relation ?
It is possible to use the code below and save all only saving the header Row (test) and automatically cascading all inserts on the child table??
var session = SessionFactory.OpenSession();
using (var tr = session.BeginTransaction())
{
var test = new Test();
test.Id = "T01";
test.Description = "Desc TEST 01";
//session.SaveOrUpdate(test); // If Removed Get INSERT ERROR on TestItem
var item = new TestItem { Id = "NEW01", ItemCode = "A", Test = test, ItemData = "New T01A" };
test.Items.Add(item);
//session.SaveOrUpdate(item);
session.SaveOrUpdate(test);
tr.Commit();
}
I suppose I need to change something on my mapping code but i do not undestand what!!!
Many days of work, googling, stackoverflowing but nothing change the result.
Thank you!!!
Q: Is this the best practice to persist a "one to many" relation ?
A: If you're talking about a "composition" relationship, the cascading approach can be a good choice.
Q: It is possible to use the code below and save all only saving the header Row (test) and automatically cascading all inserts on the child table??
A: Yes it is possible. But your code should look like this:
public class TestMap : ClassMapping<Test>
{
public TestMap()
{
Id(x => x.Id, m => m.Column("IDTest"));
Bag(x => x.Items, c =>
{
c.Key(k =>
{
k.NotNullable(true);
k.Column("IDTest");
});
c.Cascade(Cascade.All | Cascade.DeleteOrphans); //<- My suggestion
c.Lazy(CollectionLazy.NoLazy);
c.Inverse(true);
}, r => r.OneToMany(m =>
{
m.NotFound(NotFoundMode.Exception);
m.Class(typeof(TestItem));
}));
}
}
Related
here are my parent class mappings:
public class Result : BaseEntity<long, Result>
{
public virtual string Stuff { get; set; }
public virtual Iesi.Collections.Generic.ISet<ResultRequestParam> Request { get; set; }
public virtual Iesi.Collections.Generic.ISet<ResultResponseParam> Response { get; set; }
public Result()
{
Request = new HashedSet<Request>();
Response = new HashedSet<Response>();
}
}
public class ResultMap : ClassMapping<Result>
{
public ResultMap()
{
Id(x => x.Id, map => map.Generator(Generators.Identity));
Property(x => x.Stuff, map => { map.NotNullable(false); map.Length(256); });
Set(x => x.Request, map =>
{
map.Key(x => x.Column("ResultId"));
map.Cascade(Cascade.Persist | Cascade.Remove | Cascade.DeleteOrphans);
map.Inverse(true);
},
m => m.OneToMany());
Set(x => x.Response, map =>
{
map.Key(x => x.Column("ResultId"));
map.Cascade(Cascade.Persist | Cascade.Remove | Cascade.DeleteOrphans);
map.Inverse(true);
},
m => m.OneToMany());
}
}
here is my child class mapping:
public enum ResultParamType
{
Request = 0,
Response = 1
}
public abstract class ResultParam : BaseEntity<long, ResultParam>
{
public virtual string ParamKey { get; set; }
public virtual string ParamValue { get; set; }
public virtual Result Result { get; set; }
}
public class ResultParamMap : ClassMapping<ResultParam>
{
public ResultParamMap()
{
Table("ResultParam");
Id(x => x.Id, map => map.Generator(Generators.Identity));
Discriminator(x =>
{
x.Column("ParmType");
x.Type(NHibernateUtil.String);
x.Length(20);
x.Force(true);
x.NotNullable(true);
});
Property(x => x.ParamKey, map =>
{
map.Length(1000);
map.NotNullable(true);
});
Property(x => x.ParamValue, map =>
{
map.Length(3000);
map.NotNullable(false);
});
ManyToOne(x => x.Result, map =>
{
map.Column("ResultId");
map.NotNullable(true);
});
}
}
public class ResultRequestParam : ResultParam { }
public class ResultRequestParamMap : SubclassMapping<ResultRequestParam>
{
public ResultRequestParamMap()
{
DiscriminatorValue(ResultParamType.Request.ToString());
}
}
public class ResultResponseParam : ResultResponseParam { }
public class ResultResponseParamMap : SubclassMapping<ResultResponseParam>
{
public ResultResponseParamMap()
{
DiscriminatorValue(ResultParamType.Response.ToString());
}
}
Now In my provider I want to start my query with Result and then add restrictions for the child set. Similar to this query:
SELECT
*
FROM
[Result] a
JOIN [ResultParam] b on b.ResultId = a.Id
WHERE
b.ParamKey = 'hello' or b.ParamValue = 'hello'
But there's really no clear way to do this from what I've found. It seems everyone just starts with the child and works up to the parent. This wont work for me as I have two virtually mapped child entities via a discriminator value...
Welp since no one was of any help on this site I banged my head against the wall 45 times and just did a subquery. Example:
var subquery = QueryOver.Of<ResultRequestParam >()
.JoinAlias(x => x.Result, () => aR)
.Where(() => <filtering for child table>)
.Select(Projections.Distinct(Projections.Property(() => aR.Id)));
q = q.WithSubquery.WhereProperty(() => aR.Id).In(subquery);
I feel like this could be better but it works. Maybe this will help someone else who is stuck in Nhibernate hell.
I have the following parent entity Department which contains a collection of child entities Sections
public class Department
{
private Iesi.Collections.Generic.ISet<Section> _sections;
public Department()
{
_sections = new HashedSet<Section>();
}
public virtual Guid Id { get; protected set; }
public virtual string Name { get; set; }
public virtual ICollection<Section> Sections
{
get { return _sections; }
}
public virtual int Version { get; set; }
}
public partial class Section
{
public Section()
{
_employees = new HashedSet<Employee>();
}
public virtual Guid Id { get; protected set; }
public virtual string Name { get; set; }
public virtual Department Department { get; protected set; }
public virtual int Version { get; set; }
}
I would like to transform (flatten) it to the following DTO
public class SectionViewModel
{
public string DepartmentName { get; set; }
public string SectionName { get; set; }
}
Using the following code.
SectionModel sectionModel = null;
Section sections = null;
var result = _session.QueryOver<Department>().Where(d => d.Company.Id == companyId)
.Left.JoinQueryOver(x => x.Sections, () => sections)
.Select(
Projections.ProjectionList()
.Add(Projections.Property<Department>(d => sections.Department.Name).WithAlias(() => sectionModel.DepartmentName))
.Add(Projections.Property<Department>(s => sections.Name).WithAlias(() => sectionModel.SectionName))
)
.TransformUsing(Transformers.AliasToBean<SectionModel>())
.List<SectionModel>();
I am however getting the following exception: could not resolve property: Department.Name of: Domain.Section
I have even tried the following LINQ expression
var result = (from d in _session.Query<Department>()
join s in _session.Query<Section>()
on d.Id equals s.Department.Id into ds
from sm in ds.DefaultIfEmpty()
select new SectionModel
{
DepartmentName = d.Name,
SectionName = sm.Name ?? null
}).ToList();
Mappings
public class DepartmentMap : ClassMapping<Department>
{
public DepartmentMap()
{
Id(x => x.Id, m => m.Generator(Generators.GuidComb));
Property(x => x.Name,
m =>
{
m.Length(100);
m.NotNullable(true);
});
Set(x => x.Sections,
m =>
{
m.Access(Accessor.Field);
m.Inverse(true);
m.BatchSize(20);
m.Key(k => { k.Column("DeptId"); k.NotNullable(true); });
m.Table("Section");
m.Cascade( Cascade.All | Cascade.DeleteOrphans);
},
ce => ce.OneToMany());
}
}
public class SectionMap : ClassMapping<Section>
{
public SectionMap()
{
Id(x => x.Id, m => m.Generator(Generators.GuidComb));
Property(x => x.Name,
m =>
{
m.Length(100);
m.NotNullable(true);
});
ManyToOne(x => x.Department,
m =>
{
m.Column("DeptId");
m.NotNullable(true);
});
}
}
But this throws a method or operation is not implemented.
Seeking guidance on what I am doing wrong or missing.
NHibernate doesn't know how to access a child property's child through the parent entity. A useful thing to remember about QueryOver is that it gets translated directly into SQL. You couldn't write the following SQL:
select [Section].[Department].[Name]
right? Therefore you can't do the same thing in QueryOver. I would create an alias for the Department entity you start on and use that in your projection list:
Department department;
Section sections;
var result = _session.QueryOver<Department>(() => department)
.Where(d => d.Company.Id == companyId)
.Left.JoinQueryOver(x => x.Sections, () => sections)
.Select(
Projections.ProjectionList()
.Add(Projections.Property(() => department.Name).WithAlias(() => sectionModel.DepartmentName))
.Add(Projections.Property(() => sections.Name).WithAlias(() => sectionModel.SectionName))
)
.TransformUsing(Transformers.AliasToBean<SectionModel>())
.List<SectionModel>();
I noticed in your comment you'd like an order by clause. Let me know if you need help with that and I can probably come up with it.
Hope that helps!
This may be now fixed in 3.3.3. Look for
New Feature
[NH-2986] - Add ability to include collections into projections
Not sure but if this is your problem specifically but if you are not using 3.3.3 then upgrade and check it out.
Aslo check out the JIRA
Have you tried a linq query like
from d in Departments
from s in d.Sections
select new SectionModel
{
DepartmentName = d.Name,
SectionName = s == null ? String.Empty : s.Name
}
I need to map an etity to a mater tabele of a DB.
This Entity has OneToMany with another.
I need to map a Collection othe the master entity to all rows of the Child table.
But I also need to map a Property with a single row getted from the child table and filtered by a criteria that return always only one row.
Someting like a Component but in a filtered child table.
This is My Mapping:
public class Test
{
public virtual string Id { get; set; }
public virtual string Description { get; set; }
public virtual IList<TestItem> Items { get; set; }
public virtual TestItem Item { get; set; }
public Test()
{
Items = new List<TestItem>();
}
}
public class TestItem
{
public virtual string Id { get; set; }
public virtual Test Test { get; set; }
public virtual string ItemCode { get; set; }
public virtual string ItemData { get; set; }
}
public class TestMap : ClassMapping<Test>
{
public TestMap()
{
Id(x => x.Id, m => m.Column("IDTest"));
//IPOTETICAL
SomeComponent(x => x.Item, c => // How to map a filtered collection to a single property??
{
c.Key(k =>
{
k.NotNullable(true);
k.Column("IDTest");
});
**// This Is the filter**
c.Filter("itemsFilter", f => f.Condition("ItemCode = :itemsCodeValue"));
}, r => r.OneToMany(m =>
{
m.NotFound(NotFoundMode.Exception);
m.Class(typeof(TestItem));
}));
Bag(x => x.Items, c => // All Child Rows
{
c.Key(k =>
{
k.NotNullable(true);
k.Column("IDTest");
});
c.Cascade(Cascade.All | Cascade.DeleteOrphans);
c.Lazy(CollectionLazy.NoLazy);
c.Inverse(true);
}, r => r.OneToMany(m =>
{
m.NotFound(NotFoundMode.Exception);
m.Class(typeof(TestItem));
}));
}
}
public class TestItemMap : ClassMapping<TestItem>
{
public TestItemMap()
{
Id(x => x.Id, m => m.Column("IDTestItem"));
ManyToOne(x => x.Test, m =>
{
m.Column("IDTest");
m.NotNullable(false);
m.Lazy(LazyRelation.NoLazy);
});
Property(x => x.ItemCode);
Property(x => x.ItemData);
}
}
I found something about DynamicComponent but I do not know if it is for me...
http://notherdev.blogspot.it/2012/01/mapping-by-code-dynamic-component.html
Thank You!!
I've created 2 objects:
public class Set
{
public Set()
{
_sorts = new List<Sort>();
}
public virtual int Id { get; set; }
public virtual string Code { get; set; }
private ICollection<Sort> _sorts;
public virtual ICollection<Sort> Sorts
{
get { return _sorts; }
set { _sorts = value; }
}
}
public class Sort
{
public Sort()
{
_sets = new List<Set>();
}
public virtual int Id { get; set; }
public virtual string Name { get; set; }
private ICollection<Set> _sets;
public virtual ICollection<Set> Sets
{
get { return _sets; }
set { _sets = value; }
}
}
And 2 mappings:
public class SetMapping: ClassMapping<Set>
{
public SetMapping()
{
Table("Sets");
Id(x => x.Id, map => map.Generator(IdGeneratorSelector.CreateGenerator()));
Property(x => x.Code, map =>
{
map.Length(50);
map.NotNullable(false);
});
Bag(x => x.Sorts, map =>
{
map.Key(k =>
{
k.Column("SetId");
k.NotNullable(true);
});
map.Cascade(Cascade.All);
map.Table("SetsToSorts");
map.Inverse(true);
}, r => r.ManyToMany(m => m.Column("SortId")));
}
}
public class SortMapping: ClassMapping<Sort>
{
public SortMapping()
{
Table("Sorts");
Id(x => x.Id, map => map.Generator(IdGeneratorSelector.CreateGenerator()));
Property(x => x.Name, map =>
{
map.Length(50);
map.NotNullable(false);
});
}
}
usage:
Set can have many sorts
Sort can belong to many sets.
And I would like to use this as:
var set = new Set() {Code = "001"};
var sort = new Sort {Name = "My name"};
set.Sorts.Add(sort);
sort.Sets.Add(set);
Somehow the relations are not working yet because when I try to use the above code to add sorts to set for example and commit then I don't see any records saved to the SetsToSorts linked table.
Does anyone have a clue what I'm missing in my mapping? Or otherwise doing wrong?
Thank you,
Joost
Your mapping says that Set's Sort collection is inverse (map.Inverse(true)). That means the other side of the bidirectional association is responsible for persisting changes.
But your Sort class mapping doesn't have any collection mapping. Remove map.Inverse(true) on SetMapping or add noninverse collection mapping to SortMapping.
I'm trying to figure out how to use CompositeId to map another class. Here's a test case:
The tables:
TestParent:
TestParentId (PK)
FavoriteColor
TestChild:
TestParentId (PK)
ChildName (PK)
Age
The classes in C#:
public class TestParent
{
public TestParent()
{
TestChildList = new List<TestChild>();
}
public virtual int TestParentId { get; set; }
public virtual string FavoriteColor { get; set; }
public virtual IList<TestChild> TestChildList { get; set; }
}
public class TestChild
{
public virtual TestParent Parent { get; set; }
public virtual string ChildName { get; set; }
public virtual int Age { get; set; }
public override int GetHashCode()
{
return Parent.GetHashCode() ^ ChildName.GetHashCode();
}
public override bool Equals(object obj)
{
if (obj is TestChild)
{
var toCompare = obj as TestChild;
return this.GetHashCode() != toCompare.GetHashCode();
}
return false;
}
}
The Fluent NHibernate maps:
public class TestParentMap : ClassMap<TestParent>
{
public TestParentMap()
{
Table("TestParent");
Id(x => x.TestParentId).Column("TestParentId").GeneratedBy.Native();
Map(x => x.FavoriteColor);
HasMany(x => x.TestChildList).KeyColumn("TestParentId").Inverse().Cascade.None();
}
}
public class TestChildMap : ClassMap<TestChild>
{
public TestChildMap()
{
Table("TestChild");
CompositeId()
.KeyProperty(x => x.ChildName, "ChildName")
.KeyReference(x => x.Parent, "TestParentId");
Map(x => x.Age);
References(x => x.Parent, "TestParentId"); /** breaks insert **/
}
}
When I try to add a new record, I get this error:
System.ArgumentOutOfRangeException :
Index was out of range. Must be
non-negative and less than the size of
the collection. Parameter name: index
I know this error is due to the TestParentId column being mapped in the CompositeId and References calls. However, removing the References call causes another error when querying TestChild based on the TestParentId.
Here's the code that does the queries:
var session = _sessionBuilder.GetSession();
using (var tx = session.BeginTransaction())
{
// create parent
var p = new TestParent() { FavoriteColor = "Red" };
session.Save(p);
// creat child
var c = new TestChild()
{
ChildName = "First child",
Parent = p,
Age = 4
};
session.Save(c); // breaks with References call in TestChildMap
tx.Commit();
}
// breaks without the References call in TestChildMap
var children = _sessionBuilder.GetSession().CreateCriteria<TestChild>()
.CreateAlias("Parent", "p")
.Add(Restrictions.Eq("p.TestParentId", 1))
.List<TestChild>();
Any ideas on how to create a composite key for this scenario?
I found a better solution that will allow querying and inserting. The key is updating the map for TestChild to not insert records. The new map is:
public class TestChildMap : ClassMap<TestChild>
{
public TestChildMap()
{
Table("TestChild");
CompositeId()
.KeyProperty(x => x.ChildName, "ChildName")
.KeyReference(x => x.Parent, "TestParentId");
Map(x => x.Age);
References(x => x.Parent, "TestParentId")
.Not.Insert(); // will avoid "Index was out of range" error on insert
}
}
Any reason you can't modify your query to just be
_sessionBuilder.GetSession().CreateCriteria<TestChild>()
.Add(Restrictions.Eq("Parent.TestParentId", 1))
.List<TestChild>()
Then get rid of the reference?