I’m new to (fluent) nHibernate.
I have 3 tables:
Agency
• AgencyId (pk)
• AgencyAccountNo
AgencyAccount
• AgencyId (pk) (fk -> Agency. AgencyId)
• AgencyAccountNo (pk)
• ChainId (fk -> AgencyChain.ChainId)
AgencyChain
• ChainId (pk)
AgencyAccount is effectively a versioning table. Everytime an Agency changes a new AgencyAccount row with an incremented AgencyAccountNo.
I am trying to fluently map the relationships in Agency and AgencyChain so that only the Current AgencyAccount will be returned, but have had a lot of trouble. I have tried many, many things, too numerous to go into here, and can’t seem to find any examples or documentation on this.
What would your approach be?
the normal/easy way Updated
class AgencyMap : ClassMap<Agency>
{
public AgencyMap()
{
Id(a => a.Id);
Map(a => a.AccountNo);
}
}
class AgencyAccountMap : ClassMap<AgencyAccount>
{
public AgencyAccountMap()
{
CompositeId()
.KeyReference(aa => aa.Agency, "AgencyId")
.KeyProperty(aa => aa.AccountNo, "AgencyAccountNo");
References(a => a.Chain).Column("chainid");
}
}
class AgencyChainMap : ClassMap<AgencyChain>
{
public AgencyChainMap()
{
Id(c => c.Id);
HasMany(c => c.AgencyAccounts)
.KeyColumn("chainid")
// to get only the actual AgencyAccounts
.Where("AgencyAccountNo = (SELECT a.AgencyAccountNo FROM Agency a WHERE a.AgencyId = AgencyId)");
// or if only interested in Agency (using a Set to prevent duplicates coming from the history of agencyaccounts)
HasManyToMany(c => c.Agencys)
.Table("AgencyAccount")
.ParentKeyColumn("chainid")
.ChildKeyColumn("agencyid")
.AsSet();
}
}
var account = session.Get<AgencyAccount>(new AgencyAccount { Agency = agency, AccountNo = agency.AccountNo });
the hackish way when not using Identity but some other id generation
class AgencyMap : ClassMap<Agency>
{
public AgencyMap()
{
Table("AgencyAccount");
Id(a => a.Id, "AgencyId").GeneratedBy.Sequence("agency_id_sequence"); // e.g. sequence
Where("AgencyAccountNo = (SELECT a.AgencyAccountNo FROM Agency a WHERE a.AgencyId = AgencyId)");
Map(a => a.AccountNo);
Join("Agency", join =>
{
join.KeyColumn("AgencyId");
join.Map(<some other prop>);
}
}
}
Note:
this makes inserting new AgencyAccounts for the same Account very difficult
cant show the complete history of AgencyAccounts
possibly has a lot of subtle problems
Related
My application has a requirement that is should be able to filter/search for Pairs by the Number of the related Contact.
A Pair always has a reference to a Contact stored, but the number of the contact is not, and will not, be stored in the reference. So I tried to create a custom index for this, because the Pair and Contact are stored in different collections.
A simplified example of the index looks like this.
public class Pairs_Search : AbstractMultiMapIndexCreationTask<Pairs_Search.Result>
{
public class Result
{
public string Id { get; set; }
public string Workspace { get; set; }
public ContactResult Contact { get; set; }
public bool HasContactDetails { get; set; }
}
public class ContactResult
{
public string Id { get; set; }
public string Name { get; set; }
public int Number { get; set; }
}
public Pairs_Search()
{
AddMap<Pair>(pairs => pairs
.Select(p => new
{
p.Id,
p.Workspace,
Contact = new
{
p.Contact.Id,
p.Contact.Name,
Number = 0
},
// Mark this items as WITHOUT contact details.
HasContactDetails = false,
}
)
);
AddMap<Contact>(contacts => contacts
.Select(c => new
{
Id = (string) null,
Workspace = (string) null,
Contact = new
{
c.Id,
Name = c.DisplayName,
c.Number
},
// Mark this items as WITH contact details.
HasContactDetails = true,
}
)
);
Reduce = results => results
// First group by the contact ID. This will
// create a group with 2 or more items. One with the contact
// details, and one or more with pair details.
// They are all marked by a boolean flag 'HasContactDetails'.
.GroupBy(x => x.Contact.Id)
// We are going to enrich each item in the current group, that is
// marked as 'HasContactDetails = false', with the contact number.
// We need that so that we can filter on it later.
.Select(group =>
group
.Select(i => new
{
i.Id,
i.Workspace,
Contact = new
{
i.Contact.Id,
i.Contact.Name,
// Does the current item have the contact details?
Number = i.HasContactDetails
// Yes, in this case we use the previously set contact number.
? i.Contact.Number
// No, find the item with the contact details and grab the number.
: group.Single(x => x.HasContactDetails).Contact.Number
},
// Pass on the flag that indicates wheter or not
// this item has the contact details. We are going
// to need it later.
i.HasContactDetails
}
)
// We don't need the items with the contact details
// anymore, so filter them out.
.Where(x => !x.HasContactDetails)
)
// Flatten all the small lists to one big list.
.SelectMany(x => x);
// Mark the following fields of the result as searchable.
Index(x => x.Contact.Number, FieldIndexing.Search);
}
}
I've setup a full example that reproduces the issues I am having. You can find the example here.
Creating the index works fine. Querying the index works fine also as it properly matched the pair and contact and enriched the index result with the number of the contact. But when I try to use a .Where() or .Search() on the nested Number property it fails to properly filter the result dataset from the index.
The index without any filtering works as you can see in below code example (also available in the full example).
private static async Task ThisOneWorks()
{
using (var session = Store.OpenAsyncSession())
{
var results = await session
.Query<Pairs_Search.Result, Pairs_Search>()
.ToListAsync();
LogResults("ThisOneWorks()", results);
}
// Output:
// ThisOneWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWorks(): Pair 'Hermione Granger' with number '71'
// ThisOneWorks(): Pair 'Albus Dumbledore' with number '72'
}
Filtering on a non-nested value also works (also available in the full example). As you can see it filters out the one with a different workspace.
private static async Task ThisOneWithWorkspaceFilterWorks()
{
using (var session = Store.OpenAsyncSession())
{
var results = await session
.Query<Pairs_Search.Result, Pairs_Search>()
.Where(x => x.Workspace == "hogwarts")
.ToListAsync();
LogResults("ThisOneWithWorkspaceFilterWorks()", results);
}
// Output:
// ThisOneWithWorkspaceFilterWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWithWorkspaceFilterWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWithWorkspaceFilterWorks(): Pair 'Hermione Granger' with number '71'
}
When I try to filter/search on the Workspace and Number properties I would expect two results that are related to the contact Harry Potter. But instead I just get an empty dataset back.
private static async Task ThisOneWithWorkspaceAndNumberFilterDoesntWork()
{
using (var session = Store.OpenAsyncSession())
{
var results = await session
.Query<Pairs_Search.Result, Pairs_Search>()
.Where(x => x.Workspace == "hogwarts")
.Where(x => x.Contact.Number == 70)
.ToListAsync();
LogResults("ThisOneWithWorkspaceAndNumberFilterDoesntWork()", results);
}
// Output:
// ThisOneWithWorkspaceAndNumberFilterDoesntWork(): EMPTY RESULTS!
}
Can anyone tell me what I am doing wrong here? Any help would be greatly appreciated!
The way to go about it is to store ContactResult in a different collection,
which is what is called a related document in this case,
and when you create the index then you 'Index the Related Document'
Learn from the demo example in:
https://demo.ravendb.net/demos/csharp/related-documents/index-related-documents
The example is for a basic map index but the principle is the same for Multi-Map.
Remove the public class ContactResult from the index class
and define the index with something like:
select new Result
{
....
Number = LoadDocument<Contact>(Pair.Contact).Number
....
}
I have two classes, user and role, defined as:
public class User : Entity
{
// other properties ...
public virtual string Username
public virtual ICollection<Role> Roles { get; set; }
}
public class Role : Entity
{
public virtual string Name { get; set; }
public virtual ICollection<User> Users { get; set; }
}
In my mapping code, I have the following:
mapper.Class<User>(map =>
{
map.Bag(x=>x.Roles,
cm=>
{
cm.Table("UserRole");
cm.Cascade(Cascade.All);
cm.Key(k => k.Column("[User]"));
},
em=>
{
em.ManyToMany(mm =>
{
mm.Column("[Role]");
});
});
});
mapper.Class<Role>(map =>
{
map.Bag(x=>x.Users,
cm=>
{
cm.Inverse(true);
cm.Table("UserRole");
cm.Key(k=>k.Column("[Role]"));
},
em =>
{
em.ManyToMany(mm =>
{
mm.Column("[User]");
});
});
});
The mappings generate the expected schema, but the join table is never populated. Adding a new user with a new Role in its collection persists the role and then the user to the appropriate tables, but the join table is left empty. Why?
Edit: I still have not made any progress on this. I'm absolutely sure the mapping is correct, and the correct schema is generated, but the join table simply isn't populated. For test purposes, I'm generating entities using NBuilder like so:
var roles = new Role[]
{
new Role("Admin"),
new Role("Manager"),
new Role("User")
};
var users = Builder<User>.CreateListOfSize(10)
.TheFirst(1)
.Do(x =>
{
x.Roles.Add(roles[0]);
x.Roles.Add(roles[1]);
roles[0].Users.Add(x);
roles[1].Users.Add(x);
})
.All()
.With(x => x.Id = 0)
.And(x => x.Version = 0)
.And(x => x.Username = "test user")
.And(x => x.Password = "test password")
.Do(x =>
{
x.Roles.Add(roles[2]);
roles[2].Users.Add(x);
}
.Build();
foreach (var u in users) session.Save(u);
The User and Role entities are persisted correctly, but the join table remains empty. This means I cannot effective query the roles for a given user later, which nullifies the point.
Make sure you have both classes referencing each other.
I think that code, similar to one below, should work for you:
role.Users.Add(user);
user.Roles.Add(role);
session.Save(user); // NH only saves user and role, so it can get auto-generated identity fields
session.Flush(); // NH now can save into cross-ref table, because it knows required information (Flush is also called inside of Transaction.Commit())
I found a good answer to a question about many-to-many with lot of explanations and quotes from NH documentation. I think it worth to read it.
[EDIT]
In answer to this somewhat similar question there is discussion in which need for explicit transaction to save into cross-table is mentioned.
I also edited code above with adding session.Flush() to reflect my findings.
I ended up downloading the NHibernate source and referencing that directly so I could step through it. It turns out that it had something to do with the fact that my code for generating the test data was not wrapped in an explicit session transaction. Once I added that, it was fine. I'd love to see some kind of explanation on this, as I wasn't able to follow the code very clearly, but I'm at least satisfied that the problem is solved.
I have a query I am trying to run but I'm not getting desired result.
select * from employee_login e
left join employee_attendance ea on
e.emp_id = ea.EmpId and dated = '2012-01-11'
The Linq query which I tried with Nhibernate is
var attendance = from emp in session.Query<Employee>()
join empatten in session.Query<EmployeeAttendance>()
on emp.emp_id equals empatten.EmpId into atts
from ur in atts.DefaultIfEmpty()
select new { ur };
In the var attendance resultview. How can I achieve these two things?
a left join over employee and employeeattendance ( employee is the left table)
a and condition on the join not over the join result .
I'm pretty new to this situation using Linq or detached criteria; a detached criteria would be a preferable answer.
Here are the models:
public class EmployeeAttendance
{
public virtual string No_ { get; set; }
public virtual Employee Employee { get; set; }
}
public class Employee
{
public virtual string emp_id { get; set; }
public virtual ISet<EmployeeAttendance> Attendances { get; set; }
public Employee()
{
Attendances = new HashedSet<EmployeeAttendance>();
}
}
The Mapping is :
public class EmployeeAttendanceMap:ClassMap<EmployeeAttendance>
{
public EmployeeAttendanceMap()
{
Table("Employee_Attendance");
Id(x => x.No_).GeneratedBy.Assigned();
References(x => x.Employee).Column("emp_id");
}
}
public class EmployeeMap : ClassMap<Employee>
{
public EmployeeMap()
{
Table("employee_login");
Id(x => x.emp_id).Column("emp_id").GeneratedBy.Assigned();
HasMany(x => x.Attendances).KeyColumn("No_").Cascade.All();
}
}
The Employee is the primary table and AttendanceLeave has the foreign key as EmpId from Employee Table
Edit : I tried this also in my last attempt:
ICriteria criteria = session.CreateCriteria(typeof(Employee), "emp")
.CreateAlias("EmployeeAttendance", "Attendance", CriteriaSpecification.LeftJoin
, Restrictions.Eq("Attendance.Dated", DateTime.Parse("2012-1-11")));
but I ended up getting error as :
could not resolve property: EmployeeAttendance of: Royal.Data.Core.Domain.Employee
It looks like you want to get employees on leave as of a certain date. I think this would work, though I've never used the between expression in this way before:
var detached = DetachedCriteria.For<AttendanceLeave>("al")
.Add(Expression.Between('2012-01-11', "LeaveFrom", "LeaveTo")) //this should be a DateTime
.Add(Restrictions.EqProperty ("al.EmpId", "e.emp_id")) //make sure to use "e" for employee criteria alias
.SetProjection (Projections.Count ("al.EmpId"));
var employeesOnLeave = session.CreateCriteria<Employee>("e")
.Add(Restrictions.Gt(Projections.Subquery(detached), 0))
.List();
You'll still get the complete set of leaves on each employee, but it should be the employee you want.
update - looking at your comment, it seems something like this could be what you're after:
DateTime dateInQuestion = new DateTime(2012, 1, 11);
var employeesOnLeaveAsOfDateInQuestion =
session.CreateCriteria<Employee>("e")
.CreateCriteria("e.Attendances", "ea"
, NHibernate.SqlCommand.JoinType.LeftOuterJoin
, Restrictions.Between(dateInQuestion, "ea.LeaveFrom", "ea.LeaveTo"))
.List<Employee>();
This seems to work - but you need to make sure the entities you get back are not cached, otherwise cached copies w/ the full collection will be returned. This is what I tested with - not exactly like your situation because collection is maintained through a link table, but I think it will work the same either way - you may need to evict the collection specifically with a straight one to many though (the EvictCollection method is found on the session factory, not the session). You should need this bit for testing only (in my tests, the database only lives as long as the session). There is also a QueryOver example in the gist if you'd prefer to solve it that way.
I'm trying to do this:
Key key = session.QueryOver<Key>()
.Left.JoinQueryOver<ConfigValue>(x => x.ConfigValues)
.Where(c => c.Id == cfgValue.Id)
.SingleOrDefault();
But I get this exception:
NHibernate.QueryException was unhandled by user code
Message=could not resolve property: ConfigValues of: Domain.Model.Key
I figure it's because the Key object is declared in a way to restrict access to IList and mapped with a non-visible property.
public class Key
{
public virtual int Id { get; protected set; }
public virtual IEnumerable<ConfigValue> ConfigValues { get { return _configValues; } }
private IList<ConfigValue> _configValues = new List<ConfigValue>();
...
And mapped by code as:
Bag<ConfigValue>("_configValues", attr => {
attr.Lazy(CollectionLazy.Lazy);
attr.Inverse(false);
attr.Cascade(Cascade.None);
}, cm => cm.ManyToMany());
The question: How can I do it using NHibernate API?
The only way I managed to do it is with HQL:
IList<Key> keys = session.CreateQuery(#"select K_
from Key as K_
left outer join K_._configValues as KO_
where KO_.Id = :cfgValueId ")
.SetParameter("cfgValueId", cfgValue.Id)
.List<Key>();
I'm not firm with mapping by code but something along the lines
Bag<ConfigValue>("ConfigValues", attr => {
attr.Access("field.camelcase-underscore");
}, cm => cm.ManyToMany());
or Fluent NHibernate (if someones interested)
HasMany(x => x.ConfigValues)
.Access.CamelCaseField(Prefix.Underscore);
Here is the basic situation. In our application, we have Roles and Permissions. There is a many-to-many relationship between roles and permission. The catch is, that a user can be assigned roles directly, or they can have a role that they have acquired by paying for a service. To make things more interesting, if the user cancels the paid service, or it expires, then they should no longer have that role. My issue is that I would like to have a (read-only) collection of the Effective Permissions on any given user, and I'd like to be able to query against it. Some mapping details:
public ApplicationPermissionMap()
{
Table("CLSYS_ApplicationPermissions");
Id(ap => ap.ID)
.Column("ApplicationPermissionID")
.GeneratedBy.Assigned();
//...
}
public ApplicationRoleMap()
{
Table("CLSYS_ApplicationRole");
Id(role => role.ID)
.Column("ApplicationRoleID");
HasManyToMany(r => r.Permissions)
.Table("CLSYS_ApplicationRolePermissions")
.ParentKeyColumn("ApplicationRoleID")
.ChildKeyColumn("PermissionID")
.Cascade.None();
//...
}
public PaidServiceMap()
{
Table("ECOM_PaidService");
Id(ps => ps.ID)
.Column("PaidServiceID");
Component(ps => ps.Status, statusMapping =>
{
statusMapping.Map(status => status.ID)
.Column("StatusID")
.Not.Nullable();
});
HasManyToMany(ps => ps.ApplicationRoles)
.Table("ECOM_PaidServiceRoles")
.ParentKeyColumn("PaidServiceID")
.ChildKeyColumn("RoleID")
.Cascade.None();
//....
}
public UserMap()
{
Table("CLSYS_User");
Id(user => user.ID)
.Column("UserID");
HasManyToMany(user => user.Roles)
.Table("CLSYS_User_ApplicationRoles")
.ParentKeyColumn("UserID")
.ChildKeyColumn("RoleID")
.Cascade.None();
//...
}
What I need is something like this (an addition to UserMap above)
public UserMap()
{
//...
HasMany(user => user.EffectivePermissions)
//Union ( user -> ApplicationRoles -> permissions)
//Union ( user -> PaidServices [with active status] -> permissions)
//Distinct
}
And I would like to be able to query on these permissions like so
_session.Linq<User>().Where(u => u.EffectivePermssions.Contains(somePermission) && user.SomeOtherCriteria == something);
I understand that I wouldn't be able to modify this collection directly, and that is perfectly acceptable. Can anyone help me out with this mapping?
As a short term solution, I have created a view that handles the union logic, and have the following mapping:
HasManyToMany(user => user.EffectivePermissions)
.Table("VW_CLSYS_User_EffectiveApplicationPermissions")
.ParentKeyColumn("UserID")
.ChildKeyColumn("ApplicationPermissionID")
.Inverse();
This is not ideal, as I'd prefer to have the logic in the mapping for the user. Any other suggestions are welcome.