Consider the following class (simplified in order to focus in the core problem):
public class Question
{
public virtual string QuestionId { get; set; }
public virtual string Text { get; set; }
public virtual string Hint { get; set; }
}
and tables:
Question
- QuestionId ((primary key, identity column and key)
- Code
QuestionTranslation
- QuestionTranslationId (primary key, identity column; not really relevant to the association)
- QuestionId (composite key element 1)
- CultureName (composite key element 2) (sample value: en-US, en-CA, es-ES)
- Text
- Hint
How I can map the Question class so the Text and Hint properties are populated using the current thread's culture. If the thread's culture is changed I would like the Text and Hint properties to automatically return the appropriate value without the need for the Question entity to be reloaded.
Note that I'm only outlining the relevant class and properties from the business side. I'm totally open to any new class or property needed to achieve the desired functionality.
An alternative to Firo's answer (yes, I copied it and adapted it and feel bad about this).
It uses a dictionary and maps the translations as composite element (so it doesn't need the id at all)
public class Question
{
public virtual string QuestionId { get; set; }
public virtual string Text
{
get
{
var translation = Translations[CultureInfo.CurrentCulture.Name];
if (translation != null) return translation.Text
return null;
}
set
{
GetTranslation(CultureInfo.CurrentCulture.Name).Text = value;
}
}
public virtual string Hint
{
get
{
var translation = Translations[CultureInfo.CurrentCulture.Name];
if (translation != null) return translation.Hint
return null;
}
set
{
GetTranslation(CultureInfo.CurrentCulture.Name).Hint = value;
}
}
private QuestionTranslation GetTranslation(CultureInfo.CurrentCulture.Name)
{
QuestionTranslation translation;
if (!Translations.TryGetValue(CultureInfo.CurrentCulture.Name, out translation))
{
translation = new QuestionTranslation()
Translations[CultureInfo.CurrentCulture.Name] = translation;
}
return translation;
}
protected virtual IDictionary<string, QuestionTranslation> Translations { get; private set; }
}
class QuestionTranslation
{
// no id, culture name
public virtual string Text { get; set; }
public virtual string Hint { get; set; }
}
mapping:
<class name="Question">
<id name="QuestionId" column="QuestionId"/>
<map name="Translations" table="QuestionTranslation" lazy="true">
<key column="QuestionId"/>
<index column="CultureName"/>
<composite-element class="QuestionTranslation">
<property name="Text"/>
<property name="Hint"/>
</composite-element>
</bag>
</class>
Edited to reflect changed answer:
public class Question
{
public virtual string QuestionId { get; set; }
public virtual string Text
{
get
{
var currentculture = CultureInfo.CurrentCulture.Name;
return Translations
.Where(trans => trans.CultureName == currentculture)
.Select(trans => trans.Text)
.FirstOrDefault();
}
set
{
var currentculture = CultureInfo.CurrentCulture.Name;
var translation = Translations
.Where(trans => trans.CultureName == currentculture)
.FirstOrDefault();
if (translation == null)
{
translation = new QuestionTranslation();
Translations.Add(translation);
}
translation.Text = value;
}
}
public virtual string Hint
{
get
{
var currentculture = CultureInfo.CurrentCulture.Name;
return Translations
.Where(trans => trans.CultureName == currentculture)
.Select(trans => trans.Hint)
.FirstOrDefault();
}
set
{
var currentculture = CultureInfo.CurrentCulture.Name;
var translation = Translations
.Where(trans => trans.CultureName == currentculture)
.FirstOrDefault();
if (translation == null)
{
translation = new QuestionTranslation();
Translations.Add(translation);
}
translation.Hint = value;
}
}
protected virtual ICollection<QuestionTranslation> Translations { get; set; }
}
class QuestionTranslation
{
public virtual int Id { get; protected set; }
public virtual string CultureName { get; set; }
public virtual string Text { get; set; }
public virtual string Hint { get; set; }
}
<class name="Question" xmlns="urn:nhibernate-mapping-2.2">
<id name="QuestionId" column="QuestionId"/>
<bag name="Translations" table="QuestionTranslation" lazy="true">
<key>
<column name="QuestionId"/>
</key>
<one-to-many class="QuestionTranslation"/>
</bag>
</class>
<class name="QuestionTranslation" table="QuestionTranslation" xmlns="urn:nhibernate-mapping-2.2">
<id name="QuestionTranslationId"/>
<many-to-one name="ParentQuestion" column="QuestionId"/>
</class>
if you have a lot of translations then change ICollection<QuestionTranslation> Translations { get; set; } to IDictionary<string, QuestionTranslation> Translations { get; set; } and map as <map> but normally the above should do it
Related
I am trying to convert my FluentNHibernate mappings to NHibernate Mapping By-code using NHibernate 3.3.3. The goal is to upgrade to NHibernate 3.3.3 and to cut down on the number of assemblies being distributed.
However when I compile and run I get the following exception:
NHibernate.MappingException: Multi-columns property can't be mapped through single-column API.
The XML mapping FluentNHibernate gets my looks like this:
<many-to-one cascade="none" class="TextDto" fetch="join" lazy="false" name="Name" not-found="ignore">
<column name="NameTextId" unique="false" />
<column name="LanguageId" unique="false" />
</many-to-one>
Here is my new By-Code mapping:
this.ManyToOne(u => u.Name, c =>
{
c.Cascade(Cascade.None);
c.Class(typeof(TextDto));
c.Columns(
x =>
{
x.Name("NameTextId");
x.Unique(false);
},
x =>
{
x.Name("LanguageId");
x.Unique(false);
});
c.Fetch(FetchKind.Join);
c.Lazy(LazyRelation.NoLazy);
c.NotFound(NotFoundMode.Ignore);
c.Unique(false);
});
This is the old FluentNHibernate mapping:
References(x => x.Name)
.Columns("NameTextId", "LanguageId")
.Cascade.None()
.Fetch.Join()
.NotFound.Ignore()
.Not.Unique()
.Not.LazyLoad();
For Completeness the property type involved:
public class TextDto
{
public TextCompositeId Id { get; set; }
public string PluralText { get; set; }
public string SingularText { get; set; }
public override bool Equals(object obj)
{
var text = (TextDto)obj;
if (text == null) return false;
return this.Id.Equals(text.Id);
}
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
}
And an example of the property in an entity:
public class CharacteristicValue
{
public CharacteristicValueCompositeId Id { get; set; }
public TextDto Name { get; set; }
public string LanguageIdentity { get; set; }
public string Value
{
get
{
string value = null;
if (this.ValueMultilingual != null) return this.ValueMultilingual.SingularText;
else if (!string.IsNullOrEmpty(this.ValueMeta)) return this.ValueMeta;
return value;
}
}
public TextDto ValueMultilingual { get; set; }
public string ValueMeta { get; set; }
public override bool Equals(object obj)
{
if (obj == null) return false;
if (object.ReferenceEquals(this, obj)) return true;
CharacteristicValue characteristicValue = obj as CharacteristicValue;
if (characteristicValue == null) return false;
if (this.Id != characteristicValue.Id) return false;
return true;
}
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
}
So, how do I get the xml-mapping I used to get with FluentNHibernate but with NHiberbate's Mapping By-Code?
In your mapping, remove the c.Unique(false); from the ManyToOne mapping. This setting we do apply for each column now.
this.ManyToOne(u => u.Name, c =>
{
... // the same as above
// c.Unique(false); // it is setting now related to columns
});
And you will recieve
<many-to-one name="Name" class="TextDto" fetch="join" lazy="false" not-found="ignore">
<column name="NameTextId" unique="true" />
<column name="LanguageId" />
</many-to-one>
If you will change uniqueness on one of the columns:
x =>
{
x.Name("NameTextId");
x.Unique(true); // change here
},
The unique constraint would be added to that column:
<column name="NameTextId" unique="true" />
It is my firs time using NHibernate, I'm getting source code for a program from my friend after that, the program is running well after that I'm trying to add "Stock.hbm.xml" as following:
`
<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="NBooks.Core.Models"
assembly="NBooks.Core">
<class name="Stock" table="Stocks" lazy="false">
<id name="ID">
<column name="Stock_ID" />
<generator class="identity" />
</id>
<property name="Stock_name" column="Stock_name" />
<property name="Comp_ID" column="Comp_ID" />
<property name="Stock_Code" column="Stock_Code" />
<property name="Address" column="Address" />
<property name="Nots" column="Nots" />
</class>
</hibernate-mapping>
`
with my class "Stock.cs"
using System;
using System.Collections.Generic;
namespace NBooks.Core.Models
{
public class Stock : BaseModel<Stock>
{
public virtual string Stock_name { get; set; }
public virtual string Stock_Code { get; set; }
public virtual int Comp_ID { get; set; }
public virtual string Notes { get; set; }
public virtual string Address { get; set; }
public virtual bool Inactive { get; set; }
public Stock()
{
}
public Stock(string name)
{
this.Stock_name = name;
}
}
public class StockEventArgs : EventArgs
{
public Stock Stock { get; set; }
public StockEventArgs(Stock Stock)
{
this.Stock = Stock;
}
}
public delegate void StockEventHandler(Stock sender, EventArgs e);
}
the Base model is:
using System;
using System.Collections.Generic;
using NBooks.Core.Util;
using NBooks.Data.NHibernate;
using NHibernate;
namespace NBooks.Core.Models
{
public interface IBaseModel
{
int Id { get; set; }
}
public class BaseModel<T> : IBaseModel
{
IList<string> errors = new List<string>();
public virtual int Id { get; set; }
public virtual bool HasErrors {
get { return errors.Count > 0; }
}
public virtual IList<string> Errors {
get { return errors; }
}
public BaseModel()
{
}
public virtual void Validate()
{
Errors.Clear();
}
public virtual void SaveOrUpdate()
{
ITransaction trans = null;
try {
ISession session = NHibernateHelper.OpenSession();
trans = session.BeginTransaction();
session.SaveOrUpdate(this);
session.Flush();
trans.Commit();
} catch (Exception ex) {
LoggingService.Error(ex.Message);
MessageService.ShowError(ex.Message);
trans.Rollback();
}
}
public virtual void Delete()
{
ITransaction trans = null;
try {
ISession session = NHibernateHelper.OpenSession();
trans = session.BeginTransaction();
session.Delete(this);
session.Flush();
trans.Commit();
} catch (Exception ex) {
LoggingService.Error(ex.Message);
MessageService.ShowError(ex.Message);
trans.Rollback();
}
}
public static T Read(int id)
{
return NHibernateHelper.OpenSession().Load<T>(id);
}
public static IList<T> FindAll()
{
return
NHibernateHelper.OpenSession().CreateCriteria(typeof(T)).List<T>();
}
}
}
when build it appers every thing is well and no errors , when run the error "NHibernate - Could not compile the mapping document Stock.hbm.xml" appears.
Thanx in advance
I noticed you have a typo in you XML:
<property name="Nots" column="Nots" />
I would suggest that you look into using Fluent NHibernate as well. It is strongly typed (for the most part) and the mapping files are easier to read and use lambda expressions so that you don't have to go the XML route.
Your mapping would instead look like this:
public class StockMap(): ClassMap<Stock>
{
public StockMap(){
Id(x => x.Id).Column("Stock_ID").GeneratedBy.Identity();
Map(x => x.Comp_ID);
Map(x => x.Address);
Map(x => x.Notes);
}
}
You have a typo in your mapping
<property name="Nots" column="Nots" />
should be
<property name="Notes" column="Nots" />
I'm not massively familiar with this design but I am hoping to get some guidance.
I have a backend service that sends out DTOs to a WPF smart client. On the WPF smart client the user will change,delete and modify items and then the changes are sent back (client --> server). As an example, currently I am working on the Customer details form and the user has the ability to add,remove and change categories belonging to a customer in a datagrid. When the DTO is sent back to the server I would like to load in the domain object that is related to the ID in the DTO and apply the changes made on the DTO to the domain object, including all the child collections.
I have made an attempt at doing something similar to this in the code below with the UpdateCustomer method. However, I think I am way off the mark. When the code runs instead of ending up with a list of {Individual,Company,NGO,Government} I end up with a list of {Individual,B2B,Company,NGO,Government} as it has clearly not deleted the B2B entry from the original list.
One option that has occurred to me is to loop through the DTO collection and compare it to the collection from the domain object and add, remove and update dependent on what has been modified. However, this seemed really cumbersome.
What do I need to do to apply the changes from the DTO to the child collections in my domiain object?
Thank you very much for any assistance it will be thoroughly appreciated
Alex
public class Customer
{
public virtual int Id { get; set; }
public virtual IList<Category> Categories { get; private set; }
public virtual string Code { get; set; }
public virtual string Description { get; set; }
public Customer()
{
Categories = new List<Category>();
}
public virtual void AddCategory(string categoryName)
{
Categories.Add(new Category(categoryName));
}
}
public class Category
{
public virtual string CategoryName { get; private set; }
public virtual Customer Customer {get;set;}
public virtual int Id { get; set; }
protected Category(){}
public Category(string name)
{
CategoryName = name;
}
}
}
public void SetUpAutoMapper()
{
Mapper.CreateMap<Category, CategoryDto>();
Mapper.CreateMap<Customer, CustomerDto>();
Mapper.CreateMap<CategoryDto, Category>();
Mapper.CreateMap<CustomerDto, Customer>();
Mapper.AssertConfigurationIsValid();
}
public void SaveCustomer()
{
var customer = new Customer{Code="TESTCUST",Description="TEST CUSTOMER"};
customer.AddCategory("Individual");
customer.AddCategory("B2B");
customer.AddCategory("Healthcare");
customer.AddCategory("NGO");
repository.Save(customer);
}
public CustomerDto GetCustomer(int customerId)
{
var customer = repository.GetCustomer(customerId);
var customerDto = Mapper.Map<Customer,CustomerDto>(customer);
return customerDto;
}
public void UpateCustomer(CustomerDto customerToUpdate)
{
/*imagine that the dto incoming has had the following operations performed on it
-----add new category----
customerToUpdate.Categories.Add(new CategoryDto {CategoryName = "Government"});
---update existing category---
customerToUpdate.Categories[2].CategoryName = "Company";
---remove category---
customerToUpdate.Categories.RemoveAt(1);*/
var customer = repository.GetCustomer(customerToUpdate.Id);
/* How in this bit do I ensure that the child collection changes are
propogated into the underlying customer object retrieved from the database*/
var customer = Mapper.Map<CustomerDto,Customer>(customerToUpdate);
repository.Save(customer);
}
public class CustomerDto
{
public int Id { get; set; }
public string Code { get; set; }
public string Description { get; set; }
public List<CategoryDto> Categories { get; set; }
}
public class CategoryDto
{
public int Id { get; set; }
public string CategoryName { get; set; }
}
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="Customer" table="Customer">
<id name="Id" column="CustomerId">
<generator class="native"/>
</id>
<property name="Code" />
<property name="Description" />
<bag name="Categories" table="Categories" cascade="all" inverse="false">
<key column="FK_CustomerID" />
<one-to-many class="Category"/>
</bag>
</class>
<class name="Category" table="Categories">
<id name="Id" column="CategoryId">
<generator class="native"/>
</id>
<many-to-one name="Customer" column="FK_CustomerId" not-null="true" class="Customer"></many-to-one>
<property name="CategoryName" />
</class>
</hibernate-mapping>
I recently did something similar but with EF as the datatier. I don't know nhibernate to know if the same approach would work.
Basic steps were
Ensure the destination collection is loaded from db and attached to the object graph for change tracking
.ForMember(dest => dest.Categories, opt => opt.UseDestinationValue())
Then create a custom IObjectMapper for mapping IList<> to IList<T> where T : Entity
The custom IObject mapper used some code from http://groups.google.com/group/automapper-users/browse_thread/thread/8c7896fbc3f72514
foreach (var child in source.ChildCollection)
{
var targetChild = target.ChildCollection.SingleOrDefault(c => c.Equals(child)); // overwrite Equals or replace comparison with an Id comparison
if (targetChild == null)
{
target.ChildCollection.Add(Mapper.Map<SourceChildType, TargetChildType>(child));
}
else
{
Mapper.Map(child, targetChild);
}
}
Finally one last piece of logic to check all Id's in targetCollection exist in sourceCollection and delete them if they don't.
It wasn't all that much code in the end and is reusable in other actions.
Mapper.CreateMap<Customer, CustomerDto>()
.ForMember(dest => dest.Categories, opt => opt.MapFrom(src =>src.Categories));
or
Mapper.CreateMap<IList<Category>, IList<CategoryDto>>();
something like this to tell automapper to map the list, too.
This is a problem of unidirectional one-to-one mapping in NHibernate.
Student.cs
public class Student
{
public int ID { get; set; }
public int Roll { get; set; }
public int RegNo { get; set; }
public string Name { get; set; }
public StudentDetail StudentDetail { get; set; }
}
StudentDetail.cs
public class StudentDetail
{
public int ID { get; set; }
public string Father { get; set; }
public string Mother { get; set; }
}
How can I map these classes (how do the hbm mapping files look like) to the following case of one-to-one relationship?
Please have a look at the classes and the table very carefully.
Where can I put the <many-to-one> tag in Student.hbm.xml or StudentDetail.hbm.xml? If I put it in Student.hbm.xml, how can I map the column StudentDetail.StudentID, coz it is in a different table?
So this mapping:
<class name="Student" table="Student">
<id name="ID" column="ID">
<generator class="native"/>
</id>
.......
<many-to-one class="StudentDetail" name="StudentDetail" column="StudentID" unique="true" cascade="all" />
</class>
generates the following exception:
{"Invalid column name 'StudentID'."}
On the other hand <many-to-one> can't be placed in StudentDetail.hbm.xml. Coz, StudentDetail.cs doesn't contain any property of type Student.
Can I use <one-to-one>-tag? If yes where should I place it, in Student.cs or StudentDetail.cs? And how should I configure it?
Case #1:
In Student...
<one-to-one name="StudentDetail"
cascade="save-update,delete"
property-ref="Student" />
In StudentDetail...
<many-to-one name="Student"
column="StudentID"
unique="true"
cascade="none" />
Note that you'll have to have a property in your StudentDetail class that refers to a Student oobject (called Student). Also, your cascades might be different depending on your usage. You most likely want the delete cascade in there, though.
The unique="true" ensures the one-to-one mapping on the StudentDetail side.
Case #2:
Just exchange the two mappings, making sure you change the property names to the opposite class.
Look here for more info:
http://nhforge.org/blogs/nhibernate/archive/2009/04/19/nhibernate-mapping-lt-one-to-one-gt.aspx
You can map it as a one-to-many, with the collection property hidden and only its first element publicly exposed:
public class Student
{
public virtual int ID { get; set; }
public virtual int Roll { get; set; }
public virtual int RegNo { get; set; }
public virtual string Name { get; set; }
protected virtual IList<StudentDetail> StudentDetails { get; set; }
public virtual StudentDetail StudentDetail
{
get
{
if (StudentDetails.Count == 0) return null;
return StudentDetails[0];
}
set
{
if (StudentDetails.Count != 0) throw new Exception();
StudentDetails.Add(value);
value.Student = this;
}
}
}
You could handle the setter better than this - the point is to make sure you don't add multiple rows to the one-to-many. Obviously in this, StudentDetails is mapped but StudentDetail isn't in your .hbm.xml or Fluent mappings.
I'm new to NHibernate and FNH and am trying to map these simple classes by using FluentNHibernate AutoMappings feature:
public class TVShow : Entity
{
public virtual string Title { get; set;}
public virtual ICollection<Season> Seasons { get; protected set; }
public TVShow()
{
Seasons = new HashedSet<Season>();
}
public virtual void AddSeason(Season season)
{
season.TVShow = this;
Seasons.Add(season);
}
public virtual void RemoveSeason(Season season)
{
if (!Seasons.Contains(season))
{
throw new InvalidOperationException("This TV Show does not contain the given season");
}
season.TVShow = null;
Seasons.Remove(season);
}
}
public class Season : Entity
{
public virtual TVShow TVShow { get; set; }
public virtual int Number { get; set; }
public virtual IList<Episode> Episodes { get; set; }
public Season()
{
Episodes = new List<Episode>();
}
public virtual void AddEpisode(Episode episode)
{
episode.Season = this;
Episodes.Add(episode);
}
public virtual void RemoveEpisode(Episode episode)
{
if (!Episodes.Contains(episode))
{
throw new InvalidOperationException("Episode not found on this season");
}
episode.Season = null;
Episodes.Remove(episode);
}
}
I'm also using a couple of conventions:
public class MyForeignKeyConvention : IReferenceConvention
{
#region IConvention<IManyToOneInspector,IManyToOneInstance> Members
public void Apply(FluentNHibernate.Conventions.Instances.IManyToOneInstance instance)
{
instance.Column("fk_" + instance.Property.Name);
}
#endregion
}
The problem is that FNH is generating the section below for the Seasons property mapping:
<bag name="Seasons">
<key>
<column name="TVShow_Id" />
</key>
<one-to-many class="TVShowsManager.Domain.Season, TVShowsManager.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bag>
The column name above should be fk_TVShow rather than TVShow_Id. If amend the hbm files produced by FNH then the code works.
Does anyone know what it's wrong?
Thanks in advance.
Have you stepped through the auto map in the debugger to make sure your convention is being called?
Assuming you have it wired up correctly you may need to implement the Accept interface for ReferenceConvention