Recently I've come back to the question NHibernate vs EF5 for the Enterprise Application.
I know many important differences, but this one is the most suprising for me.
Consider two classic entities, Customer and Order (1:n):
public class Customer
{
public Customer()
{
Orders = new HashSet<Order>();
}
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
**public virtual ICollection<Order> Orders { get; set; }**
}
public class Order
{
public virtual Guid Id { get; set; }
public virtual Guid CustomerId { get; set; }
public virtual string Number { get; set; }
public virtual DateTime Date { get; set; }
**public virtual Customer Customer { get; set; }**
}
For both NHibernate and EF5 there are two-way mappings.
Code snippets for loading all customers AND all Orders in context of DbContext and Session for EF5 and NHibernate accordingly:
using (TestOrmForDalDbEntities context = new TestOrmForDalDbEntities())
{
context.Configuration.ProxyCreationEnabled = false;
IQueryable<Customer> customers = context.Customers;
customers.Load();
IQueryable<Order> orders = context.Orders;
orders.Load();
}
using (ISession session = _sessionFactory.OpenSession())
{
var customers = session.Query<Customer>().ToList();
var orders = session.Query<Order>().ToList();
}
The result is:
EF5: Each Customer has a collection of appropriate Orders (EF5 automatically connect them)
NHibernate: None of Customers has a collection of appropriate Orders. Even though each Order HAS link to an appropriate Customer.
The question is: is it an NHibernate idea not to connect in such way or it's me who doesn't know how to configure NHibernate?
P.S. Mappings for NHibernate:
public class CustomerMap : ClassMap<Customer>
{
public CustomerMap()
{
Id(x => x.Id).UnsavedValue(Guid.Empty).GeneratedBy.Guid();
Map(x => x.Name);
HasMany(x => x.Orders).Cascade.All();
}
}
public OrderMap()
{
Id(x => x.Id).UnsavedValue(Guid.Empty).GeneratedBy.Guid();
Map(x => x.Number);
Map(x => x.Date);
Map(x => x.CustomerId);
References(x => x.Customer);
}
The relation from Customers to Orders (HasMany) should be Inverse, meaning, it's the Order endpoint that has the foreign key to the Customer.
Related
I would like to databind the foreign key property Product.CategoryId to a Devexpess Lookupedit in Windows Forms Application.
So
lookEditCategory.DataBindings
.Add(new Binding("EditValue", Product, "CategoryId ", true,
DataSourceUpdateMode.OnPropertyChanged));
lookEditCategory.Properties.Columns.Clear();
lookEditCategory.Properties.NullText = "";
lookEditCategory.Properties.DataSource = CatCol;
lookEditCategory.Properties.ValueMember = "CategoryId";
lookEditCategory.Properties.DisplayMember = "CategoryName";
var col = new LookUpColumnInfo("CategoryName") { Caption = "Type" };
lookEditCategory.Properties.Columns.Add(col);
The problem is that Nhibernate does not expose the foreign key Product.CategoryId. Instead my entity and mapping are like this
public partial class Product
{
public virtual int ProductId { get; set; }
[NotNull]
[Length(Max=40)]
public virtual string ProductName { get; set; }
public virtual bool Discontinued { get; set; }
public virtual System.Nullable<int> SupplierId { get; set; }
[Length(Max=20)]
public virtual string QuantityPerUnit { get; set; }
public virtual System.Nullable<decimal> UnitPrice { get; set; }
public virtual System.Nullable<short> UnitsInStock { get; set; }
public virtual System.Nullable<short> UnitsOnOrder { get; set; }
public virtual System.Nullable<short> ReorderLevel { get; set; }
private IList<OrderDetail> _orderDetails = new List<OrderDetail>();
public virtual IList<OrderDetail> OrderDetails
{
get { return _orderDetails; }
set { _orderDetails = value; }
}
public virtual Category Category { get; set; }
public class ProductMap : FluentNHibernate.Mapping.ClassMap<Product>
{
public ProductMap()
{
Table("`Products`");
Id(x => x.ProductId, "`ProductID`")
.GeneratedBy
.Identity();
Map(x => x.ProductName, "`ProductName`")
;
Map(x => x.Discontinued, "`Discontinued`")
;
Map(x => x.SupplierId, "`SupplierID`")
;
Map(x => x.QuantityPerUnit, "`QuantityPerUnit`")
;
Map(x => x.UnitPrice, "`UnitPrice`")
;
Map(x => x.UnitsInStock, "`UnitsInStock`")
;
Map(x => x.UnitsOnOrder, "`UnitsOnOrder`")
;
Map(x => x.ReorderLevel, "`ReorderLevel`")
;
HasMany(x => x.OrderDetails)
.KeyColumn("`ProductID`")
.AsBag()
.Inverse()
.Cascade.None()
;
References(x => x.Category)
.Column("`CategoryID`");
}
}
}
I cannot add the property CategoryID in my Product entity and mapping because then it will be mapped twice.
Is there any solution?
Yes. Do NOT use your domain entities in the UI.
Sometimes your UI doesn't need (and shouldn't be aware of) all the properties of your domain objects.
Other times, it needs DTOs that contain data from different domain sources (for example- a list of CourseNames for the Student screen), or, like in your case- it needs the data to be represented in a slightly different way.
So the best way would be to create your DTOs with all (and only) the properties needed by the UI.
See this SO question for further details.
I try to query data using FluentNhibernate and I get this error: "Sequence contains more than one matching element"
Here are my classes and mappings:
public class Course
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual IList<Instructor> Instructors { get; set; }
}
public class Instructor
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual ImageData Portrait { get; set; }
public virtual ImageData PortraitThumb { get; set; }
public virtual IList<Course> TeachingCourses { get; private set; }
}
public class ImageData : Entity
{
public virtual int Id { get; private set; }
public virtual byte[] Data { get; set; }
}
public class CourseMap : ClassMap<Course>
{
public CourseMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasManyToMany(x => x.Instructors)
.Cascade.All()
.Table("CourseInstructor");
}
}
public class InstructorMap : ClassMap<Instructor>
{
public InstructorMap()
{
Id(x => x.Id);
Map(x=> x.Name);
References(x => x.Portrait)
.Nullable()
.Cascade.All();
References(x => x.PortraitThumb)
.Nullable()
.Cascade.All();
HasManyToMany(x => x.TeachingCourses)
.Cascade.All()
.Inverse()
.Table("CourseInstructor");
}
}
public class ImageDataMap : ClassMap<ImageData>
{
public ImageDataMap()
{
Id(x => x.Id);
Map(x => x.Data);
}
}
Then I try to get data using below code:
var course = session.CreateCriteria(typeof(Course))
.SetFetchMode("Instructors", FetchMode.Eager)
.SetFetchMode("Instructors.Portrait", FetchMode.Eager)
.SetFetchMode("Instructors.PortraitThumb", FetchMode.Eager)
.List<Course>();
But I get the following error: "Sequence contains more than one matching element"
Also, when I try this
var course = session.CreateCriteria(typeof(Course))
.SetFetchMode("Instructors", FetchMode.Eager)
.SetFetchMode("Instructors.Portrait", FetchMode.Eager)
.SetFetchMode("Instructors.PortraitThumb", FetchMode.Eager)
.SetResultTransformer(new DistinctRootEntityResultTransformer())
.List<Course>();
No error occurs but I get duplicate Instructor objects.
I did try below posts and some others as well. But it doesn't help.
NHibernate Eager loading multi-level child objects
Eager Loading Using Fluent NHibernate/Nhibernate & Automapping
FluentNhibernate uses a bag-mapping for many-to-many relations, if the mapped property is of type IList.
A bag mapping has a few major drawbacks Performance of Collections / hibernate. The one that currently bites you is that NH does not permit duplicate element values and, as they have no index column, no primary key can be defined.
Simply said NH does not know to which bag do they belong to when you join them all together.
Instead of a bag I would use a indexed variant a set, assuming that an Instructor does not has the same persistent Course assigned twice.
You can fix your query results by amending your domain classes, this tells FluentNhibernate to use a set instead of a bag by convention:
public class Course
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual Iesi.Collections.Generic.ISet<Instructor> Instructors { get; set; }
}
public class Instructor
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual ImageData Portrait { get; set; }
public virtual ImageData PortraitThumb { get; set; }
public virtual Iesi.Collections.Generic.ISet<Course> TeachingCourses { get; private set; }
}
In addition you can amend your mapping by using .AsSet(). FluentNHibernate: What is the effect of AsSet()?
I'm using Nhibernate 3.2 and fluent nhibernate, I have two tables Customer Group and Customer, and I use for lock management version control with TimeStamp Column.
I have the following classes and maps for these classes:
public class Customer
{
public Customer()
{
}
public virtual int CustomerID { get; set; }
public virtual CustomerGroup customerGroup { get; set; }
public virtual int CustomerGroupID { get; set; }
public virtual string CustomerRef { get; set; }
public virtual string NameE { get; set; }
public virtual string NameA { get; set; }
public virtual byte[] TimeStamp { get; set; }
}
and his map
public class CustomerMap : ClassMap<Customer> {
public CustomerMap() {
Table("Customer");
Id(x => x.CustomerID).GeneratedBy.Identity().Column("CustomerID");
Version(x =>x.TimeStamp).CustomType("BinaryBlob").Generated.Always().Column("TimeStamp");
DynamicUpdate();
OptimisticLock.Version();
References(x =>x.customerGroup).Column("CustomerGroupID").ForeignKey("CustomerGroupID");
Map(x => x.CustomerRef).Column("CustomerRef").Length(30).Unique();
Map(x => x.NameE).Column("NameE").Not.Nullable().Length(100).Unique();
Map(x => x.NameA).Column("NameA").Length(100);
and for Customer Group:
public class CustomerGroup {
public CustomerGroup() {
Customers = new List<Customer>(3);
}
public virtual int CustomerGroupID { get; set; }
public virtual IList<Customer> Customers { get; set; }
public virtual byte[] TimeStamp { get; set; }
}
and his map:
public CustomerGroupMap() {
Table("CustomerGroup");
Version(x => x.TimeStamp).CustomType("BinaryBlob").Generated.Always().Column("TimeStamp");
DynamicUpdate();
OptimisticLock.Version();
Id(x => x.CustomerGroupID).GeneratedBy.Identity().Column("CustomerGroupID");
HasMany(x => x.Customers).KeyColumn("CustomerGroupID");
}
When I create update in list of customers belong to specific Customer Group like this:
ISession Session = OpenSession();
Session.BeginTransaction();
var customerGroupInfo = Session.Query<CustomerGroup>().Fetch(x => x.Customers).Single<CustomerGroup>(x => x.CustomerGroupID == 98);
foreach (var item in customerGroupInfo.Customers)
{
item.NameE = "abc";
Session.Update(item);
}
Session.Transaction.Commit();
apply these sql statements:
UPDATE Customer SET NameE = 'abc'
WHERE CustomerID = 200 AND TimeStamp = 0x00000000000092EF
SELECT customer_.TimeStamp as TimeStamp1_ FROM Customer customer_
WHERE customer_.CustomerID = 200
UPDATE Customer SET NameE = 'abc'
WHERE CustomerID = 201 AND TimeStamp = 0x00000000000092F0
SELECT customer_.TimeStamp as TimeStamp1_ FROM Customer customer_
WHERE customer_.CustomerID = 201
.
.
.
and every update and every select operate in single round trip.
I set property adonet.batch_size property in configuration like this:
<property name="adonet.batch_size">20</property>
I read in this post this behavior founded by default in Nhibernate 3.2.
Any Tips to make batch work correctly?
You might look at changing your Session.FlushMode to something other than Automatic. That way, you could do something like this:
Session.FlushMode = NHibernate.FlushMode.Never
foreach (var item in customerGroupInfo.Customers)
{
item.NameE = "abc";
Session.Update(item);
}
Session.Flush();
Session.Transaction.Commit();
// Perhaps changing the flushmode after commit?
Session.FlushMode = NHibernate.FlushMode.Auto;
Edit :
Nevermind, see this excerpt from the docs: http://nhibernate.info/doc/nh/en/index.html#batch
It appears that batching doesn't get along with optimistic locking.
NHibernate supports batching SQL update commands (INSERT, UPDATE, DELETE) with the following limitations:
.NET Framework 2.0 or above is required,
**the Nhibernate's drive used for your RDBMS may not supports batching,**
since the implementation uses reflection to access members and types in System.Data assembly which are not normally visible, it may not function in environments where necessary permissions are not granted
**optimistic concurrency checking may be impaired since ADO.NET 2.0 does not return the number of rows affected by each statement in the batch, only the total number of rows affected by the batch.**
Hopefully the title of this question makes sense, if not, here is my elaboration.
With two entities, Brand and Affiliate and a many-to-may relationship between them i would like to be able to use a query to find the Affiliates where the BrandName is a variable value.
Here is the Affiliate class and Affiliate MapClass (simplified of course)
public class Affiliate
{
public virtual int Id { get; private set; }
public virtual DateTime DateReceived { get; set; }
public virtual IList<Brand> Brands { get; set; }
public Affiliate()
{
Brands = new List<Brand>();
}
}
public class AffiliateApplicationRecordMap : ClassMap<Affiliate>
{
public AffiliateApplicationRecordMap()
{
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.DateReceived, "TimeStampCreated");
HasManyToMany(x => x.Brands)
.Cascade.All()
.ParentKeyColumn("AffiliateID")
.ChildKeyColumn("BrandID")
.Table("AffiliateBrand");
}
}
There is a mapping table called AffiliateBrand which provides the many to many mapping.
Here is the Brand class and ClassMap
public class Brand
{
public virtual int ID { get; private set; }
public virtual string Name { get; set; }
public virtual IList<Affiliate> Affiliates{ get; set; }
public Brand()
{
Affiliates = new List<Affiliate>();
}
public virtual void AddAffiliateApplication(Affiliate affiliate)
{
affiliate.Brands.Add(this);
Brands.Add(affiliate);
}
}
public class BrandMap : ClassMap<Brand>
{
public BrandMap()
{
Id(x => x.ID).GeneratedBy.Identity();
Map(x => x.Name);
HasManyToMany(x => x.Affiliates)
.Cascade.All()
.Inverse()
.ParentKeyColumn("BrandID")
.ChildKeyColumn("PartnerID")
.Table("AffiliateBrand");
}
}
Now i'm tyring to write this query with NHibernate:
var result = session
.CreateCriteria(typeof(Partner))
.AddOrder(Order.Asc("DateReceived"))
.Add(Restrictions.Eq("Brands.Name", brandName))
.SetMaxResults(10)
.List<Partner>();
Now clearly this isn't working and i didn't really think it would. What i'm trying to do is get all Affiliates back where the Brand has a specific name. How do i write this query?
You need to add a join to your criteria using CreateAlias
var result = session
.CreateCriteria(typeof(Partner))
.AddOrder(Order.Asc("DateReceived"))
.CreateAlias("Brands", "brand")
.Add(Restrictions.Eq("brand.Name", brandName))
.SetMaxResults(10)
.List<Partner>();
Using fluentnhibernate i am having a problem with the link table insertion.
Here is my entities
public partial class Item
{
public virtual int Id
{
get;
set;
}
public virtual string Description
{
get;
set;
}
public virtual IList<Category> Categories
{
get;
set;
}
}
public partial class Category
{
public virtual int Id
{
get;
set;
}
public virtual string Name
{
get;
set;
}
public virtual string Description
{
get;
set;
}
public virtual IList<Item> Items
{
get;
set;
}
}
Here is my mappings.
public class ItemMapping : ClassMap<Item>
{
public ItemMapping()
{
Table("Item");
Schema("dbo");
Id(x => x.Id);
Map(x => x.Description);
HasManyToMany(x => x.Categories)
.ChildKeyColumn("Item_id")
.ParentKeyColumn("Category_id")
.Table("CategoriesToItems")
.AsSet();
}
}
public class CategoryMapping : ClassMap<Category>
{
public CategoryMapping()
{
Table("Category");
Schema("dbo");
Id(x => x.Id);
Map(x => x.Description);
Map(x => x.Name);
HasManyToMany(x => x.Items)
.ChildKeyColumn("Category_id")
.ParentKeyColumn("Item_id")
.Table("CategoriesToItems")
.AsSet();
}
}
Here is how i add it to collection in my mvc page
var category = CategoryTask.Query(x => x.Id == post.Category).FirstOrDefault();
var item = new Item
{
Categories = new List<Category> { category },
Tags = tags
};
ItemTasks.Save(item);
My question is why it doesnt add the relations in my link table "CategoriesToItems". The table is already in the database with Category_Id (FK, int, not null) and Item_Id (FK, int, not null).
Where is the problem? why it doesnt add it to relation table?
It's hard to say what's really wrong when we can't see what your ItemTasks.Save does under the covers. Are you wrapping your save in a transaction? If not, you should be.
You should call Session.Flush() just before the transaction.Commit() as well.
I am not certain if the problem has been solved, but it looks similar to my problem (fluentnhibernate hasmanytomany same identifier exception).
Also, it looks like your parent and child key columns are backward.