Take the following NHibernate scenario:
I have two tables:
Header
HeaderID (int),
ForeignKey1 (int),
ForeignKey2 (int)
Child
ForeignKey1 (int),
ForeignKey2 (int)
Description (varchar)
...
Metric Shed Ton Of Other Columns That Im Not InterestedIn(various types)
I'm using Fluent NHibernate - I'd like to project the value of Description on the Child object into the parent (is that the correct terminology?!) - but obviously Child contains a lot of data in its columns - I don't want all that extra data, just the description...how do I get NH to produce the equivalent of the following query:
select
Header.*, Child.Description
from
Header
inner join
Child ON Header.ForeignKey1 = Child.ForeignKey1 AND Header.ForeignKey2 = Child.ForeignKey2
The only way I've got this working up to now is to use a Reference mapping to reference the child from the Header entity and then just created a non-mapped property on Header which pointed to Child.Description. Obviously this means that NH fetches the whole child object before it can query the value of Description
(I don't think the composite key is a problem here, the join seems to work fine - it's just how to get the data without getting all the non-interesting data)
At the moment my header entity looks like this:
public virtual int HeaderID { get; set; }
public virtual int KeyColumn1 { get; set; }
public virtual int KeyColumn2 { get; set; }
public virtual Child Child { get; set; }
public virtual string Description { get { return Child.Description; } }
The mappings for it:
Id(x => x.HeaderID);
Map(x => x.KeyColumn1);
Map(x => x.KeyColumn2);
References<Child>(x => x.Child).Fetch.Join().Columns("KeyColumn1", "KeyColumn2").ReadOnly();
Basically, I can't change the schema, I'm not interested in the rest of the data in the Child table, and I can't create views (can't change schema)
Anyone have any ideas if this is possible? I'm going to be querying a big list of Header objects, and I need that field from Child but I don't want the query to take forever!
Is this something I would have to do at the query level instead using hql or crit API?
Edit:
Trying to get this working using query API
Header.Session.QueryOver<Header>(() => parent)
.Where(h => h.HeaderID == 1)
.Inner.JoinQueryOver(x => x.Child, () => child)
.Select(x => x.HeaderID, x => x.ForeignKey1, x => x.ForeignKey2, x => child.Description);
Checking the SQL shows the query is exactly what I want - but I get an exception System.Object[] is not of type Header and cannot be used in this generic collection
I assume this is because what I'm getting is just an array of values from the Select() - any idea how I can transform this into a Header object?
Edit: I ended up with
Header.Session.QueryOver<Header>(() => parent)
.Where(h => h.HeaderID == 1)
.Inner.JoinQueryOver(x => x.Child, () => child)
.Select(x => x.HeaderID, x => x.ForeignKey1, x => x.ForeignKey2, x => child.Description)
.TransformUsing(new GenericResultTransformer(typeof(Header), "HeaderID", "ForeignKey1", "ForeignKey2", "Description"));
Which works just how I want it - if anyone has a better suggestion I'm open to it, but like I said, I can't touch the schema at all
There is the concept of lazy properties, but that is really for the opposite situation, i.e. there is a small number of properties that you want to exclude.
If this is for a presentation scenario, use can use any of NHibernate's query methods, to project just the columns you like - the query result need not be a complete entity. See for instance the select clause in HQL and LINQ, and the SetProjection() family in Criteria/QueryOver.
Related
I have an m:n relationship between 2 entities:
Person (id, name, ...), PersonJob (personid, jobid), Job (id, jobname, ...)
I can easly read my data from EF, map it via Automapper and return it as a REST api result like this (Dto being my Data Transfer Obects for the api calls):
public class MappingProfile : Profile {
public MappingProfile() {
CreateMap<Person, PersonDto>();
CreateMap<PersonJob , PersonJobDto>();
CreateMap<Job, JobDto>();
...
public async Task<PersonDto?> GetPersonDetailsAsync(long id)
{
var person = await context.Persons
.Include(x => x.PersonJob ).ThenInclude(x => x.Job)
.SingleOrDefaultAsync(p => p.Id == id);
return person == null ? null : mapper.Map<Person, PersonDto>(person );
}
public async Task<JobDto?> GetJobDetailsAsync(long id)
{
var job = await context.Jobs
.Include(x => x.PersonJob ).ThenInclude(x => x.Person)
.SingleOrDefaultAsync(p => p.Id == id);
return job == null ? null : mapper.Map<Job, JobDto>(job);
}
The Mapper works pretty nice, handling the cyclic nature of the EF objects pretty well. A Person object contains multiple PersonJob objects. These have a reference back to the object and a reference to the Job, which itself has a reference back to the PersonJob object.
But when I pass this data back via the REST interface this all gets serialized in a HUGE json string. So the Person contains a PersonJob that contains the original Person that contains the same PersonJob, ...
I cannot add the fields as Ignore fields in the mapping as I need both "mapping directions" - coming from Person to Job and from Job to Person.
At the moment I am deleting the "back references" after the Mapping, but this is obviosly not a good solution and hard to maintain if the object structures are more complex.
Is there an elegant way?
regards,
Christoph
I'm using by code mappings and trying to map a manytomany. This works fine but I need OrderBy for the child collection items. I noticed this has been omitted (it does exist in the HBM mappings). e.g.
public class Mapping : EntityMapper<Category>
{
public Mapping()
{
Set(x => x.Items, m =>
{
m.Table("ItemCategories");
m.Key(k => k.Column("CategoryId"));
m.Inverse(true);
m.Cascade(Cascade.None);
}, col => col.ManyToMany(m =>
{
m.Columns(x => x.Name("ItemId"));
//m.OrderBy("Score desc"); // missing in Nh4.x?
}));
}
}
Is there a workaround for this? I tried following the suggestion in this article whereby I can set the property before the session factory is built but it has no effect. e.g.
cfg.GetCollectionMapping(typeof(Category).FullName + ".Items").ManyToManyOrdering = "Score desc";
cfg.BuildSessionFactory();
Am I doing something wrong or is OrderBy on manytomany not supported in Nh4?
Also, is it possible to restrict the maximum number of items retrieved in the collection?
Replaced the many to many with one to many and introduced an entity that represents the relationship (followed advice from this article).
This has the upside of allowing you to map the order-by column as well as other columns, and also solved the issue of restricting the number of items in the collection by using the one-to-many's Where() and Filter() clauses.
I have parent and child objects BOOKLET and DEMOGRAPHICS_INFO in my Oracle database mapped as follows in my data layer:
public BookletMapping()
{
Table("BOOKLET");
Id(x => x.Id, m => m.Column("ID");
...
ManyToOne(x => x.DemographicsInfo, m => m.Column("DEMOGRAPHICS_INFO_ID"));
}
public DemographicsInfoMapping()
{
Table("DEMOGRAPHICS_INFO");
Id(x => x.Id, m => m.Column("ID");
...
}
I have intentionally left out the DemographicsInfo relationship to the Booklet because I don't need to traverse my entities that direction. The ManyToOne relationship will actually be a one-to-one.
I have written a test to ensure I can create a DemographicsInfo and immediately assign it to its parent Booklet, and that looks like this:
[Test]
public void ShouldSaveCorrectEntity()
{
var booklet = _unitOfWork.Get<Booklet>(4);
var demInfo = new DemographicsInfo();
_unitOfWork.Insert(demInfo);
booklet.DemographicsInfo = demInfo;
_unitOfWork.Save();
demInfo.Id.ShouldNotEqual(0);
}
When I call Save(), I get the following exception:
{"ORA-02291: integrity constraint (<schema>.BOOKLET_DEMOGRAPHICS_INFO_FK1) violated - parent key not found\n"}
This is because the demInfo object is not given an Id upon Insert(). My Insert implementation looks like this:
public void Insert<T>(T entity)
{
using (var transaction = _session.BeginTransaction())
{
_session.Save(objectToSave);
_session.Flush();
transaction.Commit();
}
}
Where _session is an NHibernate ISession. Because I have both Saved the new entity (it persists successfully) and Flushed my session, I would expect my demInfo variable to have an Id, but it remains 0, which is a foreign key violation when I try to save my parent object. Am I overlooking a step here? Should I rethink my pattern for adding a new child to an existing parent?
I have solved my issue. As it turns out, the Oracle convention of using Sequences for Id column values and populating them using Triggers on row insert disallows NHibernate from learning the Id upon persisting an entity. I added a Sequence Generator to my Id maps and Insert() now updates the Id on my transient entity upon save so I can assign the DemographicsInfo to my Booklet.DemographicsInfo.
Id(x => x.Id, m =>
{
m.Column("ID");
m.Generator(Generators.Sequence, a => a.Params(new { sequence = "SEQ_TABLE_ID" }));
});
Now my test passes with no exceptions.
I want to make a kind of "news feed" in the application of my company.
In the scenario, User actions will generate "Activity" of different kinds, and other users will see in their "news feed".
However, an "Activity" is not related to all users, and to determine the relation, we have a complex piece of code.
Here is my Activity class
public class Activity: IActivity
{
public virtual int Id { get; set; }
public virtual ActivityType Type { get; set; }
public virtual User User { get; set; }
public virtual bool IsVisibleToUser(User userLook)
{
// Complex business calculation etc.
return true;
}
}
I want to get latest 10 news that is visible to User. But since the Activity table will be quite huge, and performance is an issue, I want to do the best practice about it.
What i am about to do, is get 25 last Activity, and check if we fill the list to show to user. For example, if only 5 Activity is visible to user, i will get another 25 Activities and so on.
IList<Activity> resultList = session.CreateCriteria(typeof(Activity))
.SetMaxResults(25)
.AddOrder(Order.Desc("Id"))
.List<Activity>();
I want to learn, if I get the whole list ordered by Id, and check one by one if it is visible to User, would NHibernate only loads the objects that i use for me or not?
IList<Activity> resultList = session.CreateCriteria(typeof(Activity))
.AddOrder(Order.Desc("Id"))
.List<Activity>();
int count = 0;
foreach( Activity act in resultList){
if (act.IsVisible(CurrentUser)){
count++;
// Do something with act
if (count == 10)
break;
}
}
EDIT:
Here is ActivityMapping for Activity model.
public class ActivityMap : ClassMap<Activity>
{
public ActivityMap()
{
Id(x => x.Id);
Map(x => x.Type).CustomType(typeof(Int32));
References(x => x.User).Nullable();
}
}
If your question is about how the generated SQL would look like, my guess would be :
SELECT
this_.Id as Id0_0_,
this_.ActivityTypeas ActivityType_0_0_,
--Other fields
FROM dbo.ActivityType this_
WHERE
--condition
ORDER BY
--condition
Since you have mentioned that the Activity count is huge, you can make use of
ICriteria's SetFirstResult and SetMaxResult.
SetFirstResult(int) indicates the index of the first item that you wish to obtain and SetMaxResult(int) indicates the number of rows you wish to get, 25 in your case.
The ToList would load all the records in memory at once.
[UPDATE] If you need the records to be returned one by one, make use of Enumerable() -
If you expect your query to return a very large number of objects, but you don't expect to use them all, you might get better performance from the Enumerable() methods, which return a System.Collections.IEnumerable. The iterator will load objects on demand, using the identifiers returned by an initial SQL query (n+1 selects total).
Source - Link
No, the List() method pulls everything into memory at once.
I try to map NHibernate to an existing database structure. Unfortunately I can not change the existing structure.
First I'll give some background and then explain the problem itself
Relational analysis is quite simple:
Log is the main Entity. he has one-to-one relationship with Form.
the foreign key is FormID.
Buyer & Seller are collection of the form entity.
The application should save the form entity together with the buyers and the sellers.
The problem is the primary key consisting of the Buyer & Seller entity.
The key composite from ForegineKey- FormID and an intenal ordering int number- tdNum
This is reflected in the following form FNH mapping
public class FormLogMap : ClassMap<FormLog>
{
public FormLogMap()
{
Table("BillOfSaleLog");
Id(x => x.FormId).Column("FormID").GeneratedBy.Native();
....
....
References<Form>(x => x.Form, "FormID").LazyLoad().ReadOnly();
}
}
public class FormMap : ClassMap<Form>
{
public FormMap()
{
Table("BillOfSaleForm");
Id(x => x.Id).Column("FormID").GeneratedBy.Foreign("Log");
...
...
HasOne<FormLog>(x => x.Log).Cascade.All();
HasMany(x => x.Buyers).KeyColumn("FormID").Inverse().Cascade.All();
HasMany(x => x.Sellers).KeyColumn("FormID").Inverse().Cascade.All();
}
}
public class BuyerMap : ClassMap<Buyer>
{
public BuyerMap()
{
Table("BillOfSaleBuyer");
CompositeId()
.KeyReference(x => x.Form, "FormID")
.KeyProperty(x => x.InnerOrderId, "tdNum1");
....
....
}
}
Seller is exectly the same
The problem occurs when I try to save the entity by the Edit Action
I'm using MVC to get the user data and make it an object.
The binding works fine and makes the object along with the collections.
But when I save an entity receive the following error:
I expect NHibernate be smart enough to set the Buyer & Seller foreign keys.
But in fact they remain without value.
The problem can be solved by setting foreign keys manually
as the following code:
//i have to set the form proerty in the childs
//without of the lines NH will try to save the childs it with FormID = null
foreach (var buyer in form.Buyers) { buyer.Form = form; }
foreach (var seller in form.Sellers) { seller.Form = form; }
But I'm looking for an elegant and correct solution
Thank you for reading
You don't show the code for adding a Buyer or Seller to a Form but it should look like this:
form.Buyers.Add(buyer);
buyer.Form = form;
Because Form is the inverse side of the relationships, you have to set the reference to Form on the many side. You should do this anyway so that the in-memory objects are correct. If the Buyer and Seller objects are already persistent in the same ISession than that should work.
According to this related question, HHibernate does not support cascade working with a composite key (as you have in BuyerMap). One of the answers does contain a hack to workaround this, but you will end up with a redundant column.