I have a unidirectional one-to-one relation. what I want to achieve is either
create a join on clause using CASE and sending variables to it so that I can change the join column (any other suggestions to change join column are also welcomed)
ignore the first clause created by hibernate and create specific ON clause using criteria as in the bottom of the question.
My query that works in SQL Manager:
SELECT d.Description, d.AccountId, d.PartnerId, a.FormattedName FROM
InvoiceDesigns d
INNER JOIN UserAccount a
ON
a.id =
CASE
WHEN d.PartnerId != -1 AND d.AccountId = 1 THEN d.PartnerId
WHEN d.PartnerId = 1 THEN d.AccountId
ELSE 1
END
WHERE
d.AccountId = 1 OR PartnerId = 1
The working query runs in SQL Manager, not in hibernate. it is the thing I want to achieve, I want hibernate to create a query like this.
When an account, lets say ID = 1 to stick with example, displays designs they will see
A) PartnerId of design is NOT -1 AND AccountId is 1, which means it is created by a partner for that account and will display partner's formattedName.
This is for normal accounts when viewing their avaiable designs. If a partner created a design for them, they will see that partner's name.
B) PartnerId equals to ID of the account, which means it is a Partner design and formattedName of the account related design is created for will be displayed.
This is for partner accounts to view designs they created.
C) Design is created by the account directly, so will display its own name. For normal accounts and self created designs.
Normal join is working fine in hibernate when I want to use only the AccountId or PartnerId but I need to change the column to get the related FormattedName. I tried #Where, #Filter, #JoinColumnOrFormula but I wasn't able to achieve this query.
My classes with #Where basically look like this:
Design class:
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id")
int id;
#Column(name="AccountId")
int accountId;
#Column(name="PartnerId")
int partnerId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "AccountId", referencedColumnName = "id", insertable = false, updatable = false, nullable = true)
#Where(clause = "CASE " +
"WHEN PartnerId != -1 AND AccountId = :accountId THEN PartnerId " +
"WHEN PartnerId = :accountId THEN AccountId " +
"ELSE :accountId " +
"END")
AccountData account;
Account class:
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id")
int id;
#Column(name="FormattedName")
String formattedName;
Here you can see that I want to send an ID to query as parameter using ":accountId" which will be used to define the join column.
this question: custom join clause actually does something similar. That is why I focused on where clause
Addition: I also found something with criteria
Criteria criteria = session.createCriteria(InvoiceDesignData.class, "design");
criteria.createCriteria("design.account", "acc", JoinType.LEFT_OUTER_JOIN, Restrictions.eqProperty("acc.id", "design.partnerId"));
invoiceDesignList = criteria.list();
But this does not ignore the first condition and adds an additional condition to ON clause using AND. I removed join columns from #one-to-many hoping to ignore first condition but it does not. Produced query:
from InvoiceDesigns this_ left outer join UserAccount acc1_ on this_.account_id=acc1_.id and ( acc1_.id=this_.PartnerId )
Thanks in advance
You can try moving the case statement:
SELECT d.Description, d.AccountId, d.PartnerId, a.FormattedName
FROM (SELECT *, CASE
WHEN PartnerId != -1 AND AccountId = 1 THEN PartnerId
WHEN PartnerId = 1 THEN AccountId
ELSE 1
END AS SmartID
FROM InvoiceDesigns) d
INNER JOIN UserAccount a
ON a.id = d.SmartID
WHERE d.AccountId = 1 OR PartnerId = 1
EDIT: This basically leaves you with a straightforward join with an in-line query without a conditional ON statement.
If you only want to map each Design to a single AccountData object, with a single join, but you want to join on either the accountId or partnerId depending on their values, then you should be able to do this by using a join formula. You'll want something like:
#ManyToOne
#JoinColumnsOrFormulas(
{ #JoinColumnOrFormula(
formula = #JoinFormula(
value = "case " +
"when partnerId != -1 and accountId = 1 then partnerId" +
"when partnerId = accountId then accountId " +
"else 1" +
"end",
referencedColumnName="id")) }
)
Note that you need to use the #ManyToOne annotation, even though this is really a one to one association. The join formula won't work if you try to use it on a one to one.
Then you can simply retrieve the data with a normal HQL join:
SELECT d.description, d.accountId, d.partnerId, a.formattedName FROM Design d
inner join d.account a
Related
I have following tables:
User - userId, userName, ...
Settings - settingId, userId, settingKey, settingValue
for example for userId = 123, I might have:
settingId: 1
userId: 123
settingKey: "allowClient"
settingValue: "0"
settingId: 2
userId: 123
settingKey: "allowAccess"
settingValue: "1"
Then for example how can I query for all users that have settingValue of "0" corresponding to settingKey of "allowClient" and settingValue of "1" corresponding to settingKey of "allowAccess"? Sometimes the settingKey and settingValue that I'm looking for might not even be there for a particular user, in which case, I would just want to ignore those users.
My "attempt":
select * from User u inner join Settings s on u.userid = s.userid
where s.settingKey = 'allowClient and s.settingValue = '0'
and s.settingKey = 'allowAccess' and s.settingValue = '1'
this doesn't work for obvious reason because it's putting AND on all the conditions. I'm not aware of any sql construct that can get around this and allow me to just say what I actually want.
Your first attempt doesn't work because the WHERE clause check each row one at a time. And no single row fulfils all of those conditions at once.
So, you could use an EXISTS() check on each of the two keys, for a very literal expression of your problem...
SELECT
user.*
FROM
user
WHERE
EXISTS (
SELECT *
FROM settings
WHERE userId = user.userId
AND settingKey = 'allowClient'
AND settingValue = '0'
)
AND
EXISTS (
SELECT *
FROM settings
WHERE userId = user.userId
AND settingKey = 'allowAccess'
AND settingValue = '1'
)
Depending on data characteristics, you may benefit from a single sub-query instead of two EXISTS() checks.
This is closer to what you were trying to do.
Filter to get two rows per user (using OR instead of AND)
Aggregate back down to a single row and check if both conditions were met
(But I'd go with two EXISTS() first, and let the optimiser do its work.)
WITH
matching_user
(
SELECT
userId
FROM
settings
WHERE
(settingKey = 'allowClient' AND settingValue = '0')
OR
(settingKey = 'allowAccess' AND settingValue = '1')
GROUP BY
userId
HAVING
COUNT(DISTINCT settingKey) = 2 -- DISTINCT only needed if one user has the same key set twice
)
SELECT
user.*
FROM
user
INNER JOIN
matching_user
ON user.userId = matching_user.userId
Finally, you could just join twice, which is functionally similar to the double-exists check, but shorter code, though not always as performant.
SELECT
user.*
FROM
user
INNER JOIN
settings AS s0
ON s0.userId = user.userId
AND s0.settingKey = 'allowClient'
AND s0.settingValue = '0'
INNER JOIN
settings AS s1
ON s1.userId = user.userId
AND s1.settingKey = 'allowAccess'
AND s1.settingValue = '1'
Using the two different aliases prevents ambiguity (which would cause an error).
It does assume that the joins will only ever find 0 or 1 rows, if they can find many, you get duplication. EXISTS() doesn't have that problem.
I have to show result from the dbo.mail_Messages table, but the problem is that Messages should be shown which Ids are not in dbo.mail_Reply. For that I am using the query below but it is showing nothing.
I have also attached a screen shot:
How can I fix this?
My code:
SELECT
dbo.mail_Messages.MessageID,
dbo.mail_Users_Messages_Mapped.PlaceHolderID,
IsRead,
SenderId,
dbo.mail_Messages.Subject,
dbo.mail_Messages.Body,
dbo.mail_Messages.Date,UserEmail
FROM
dbo.mail_Users_Messages_Mapped
JOIN
dbo.mail_Messages ON dbo.mail_Messages.MessageID = dbo.mail_Users_Messages_Mapped.MessageID
JOIN
dbo.mail_Users ON dbo.mail_Users.UserID = dbo.mail_Users_Messages_Mapped.UserId
JOIN
dbo.mail_Reply ON dbo.mail_Reply.MessageID = dbo.mail_Messages.MessageID
WHERE
UserEmail = 'user1'
AND dbo.mail_Users_Messages_Mapped.PlaceHolderId = 1
AND dbo.mail_Messages.MessageID != dbo.mail_Reply.MessageID
Your query will produce an empty result as it has two contradicting conditions. In the join clause you demand that:
dbo.mail_Reply.MessageID = dbo.mail_Messages.MessageID
And in the where clause you demand that
dbo.mail_Messages.MessageID != dbo.mail_Reply.MessageID
Since a value cannot be both equal and not equal to another value, the combination of these two conditions is an empty result.
One way to solve this, if I understand the requirements correctly, is to stop joining mail_Reply, and use the in operator instead:
SELECT
dbo.mail_Messages.MessageID,
dbo.mail_Users_Messages_Mapped.PlaceHolderID,
IsRead,
SenderId,
dbo.mail_Messages.Subject,
dbo.mail_Messages.Body,
dbo.mail_Messages.Date,UserEmail
FROM
dbo.mail_Users_Messages_Mapped
join
dbo.mail_Messages on dbo.mail_Messages.MessageID = dbo.mail_Users_Messages_Mapped.MessageID
join
dbo.mail_Users on dbo.mail_Users.UserID = dbo.mail_Users_Messages_Mapped.UserId
where
UserEmail = 'user1'
and
dbo.mail_Users_Messages_Mapped.PlaceHolderId = 1
and
dbo.mail_Messages.MessageID NOT IN (SELECT MessageID FROM dbo.mail_Reply)
I was searching for similar problem on google and stackoverflow for almost 2 hours but did not find any solution.
I have 2 tables with relation 1 to many.
1) [Accounts]
PK Account_Id
int User_ID
2) [Temporary_Accounts]
Fk Account_Id
char IsAccepted {'1','0',null}
varchar name
And 2 mapped classes
1) Acc
int Id;
User user;
TempAcc Temp; //cause each Account can have 0 or one TempAcc (with IsAccepted == null)
2)TempAcc
int Id;
bool IsAccepted;
string name;
I want to display all accounts for given user_id with additional information(f.e name) for Accounts which has record in [Temporary_Accounts] and IsAccepted == null.
so the SQL should look like:
select acc.Account_Id, acc.User_Id, tempacc.Name
from Account acc left join Temporary_Account tempacc
on (acc.Account_ID = tempacc.Account_Id and tempacc.IsAccepted is null)
where (acc.User_Id = 65);
but my IQueryOverquery:
IQueryOver<Acc> query = (...)
query.JoinAlias(f => f.Temp,
() => Temp,
JoinType.LeftOuterJoin)
.Where(f => f.Temp.IsAccepted == null)
.And(f => f.user.id == userid);
generates such sql:
select acc.Account_Id, acc.User_Id, tempacc.Name
from Accounts acc left join Temporary_Accounts tempacc
on (acc.Account_ID = tempacc.Account_Id)
where (acc.User_Id = 65 and tempacc.IsAccepted is null);
so I am getting less results than in first correct query.
Do you have any Idea what should I change or what could I do to obtain results from first query ? My Idea was to leftjoin Accounts table with subquery which selects all IsAccepted=null accounts from Temporary_Accounts table ,but I am not sure how to do it in Iqueryover or Icriteria.
I will be grateful for any advices
Since you have a 1-Many between Acc and Temp your sample sql will produce a Cartesian product.
The Queryover you will need uses a Subquery and looks something like the following:
Acc accountAlias = null;
var subQuery = QueryOver.Of<Temp>()
.Where(x=>x.IsAccepted==null)
.And(x=>x.Account.Id==accountAlias.Id);
var results = session.QueryOver<Acc>(()=>accountAlias)
.Where(x=>x.User.Id==65)
.WithSubquery.WhereExists(subQuery);
Producing SQL like this:
select *
from Accounts a
where a.User_Id=65
and exists (
select t.Account_Id
from Temporary_Accounts t
where t.IsAccepted is null and t.Account_Id=a.Account_Id
)
This article on nhibernate.info is very helpful for figuring out complex queries with QueryOver.
UPDATE:
If you need to also find Accounts which do not have any corresponding rows in Temporary_Accounts then you need two subqueries and a Disjunction.
Acc accountAlias = null;
var hasTempAccount = QueryOver.Of<Temp>()
.Where(x=>x.IsAccepted==null)
.And(x=>x.Account.Id==accountAlias.Id);
var doesNotHaveTempAccount = QueryOver.Of<Temp>()
.And(x=>x.Account.Id==accountAlias.Id);
var results = session.QueryOver<Acc>(()=>accountAlias)
.Where(x=>x.User.Id==65)
.Where(Restrictions.Disjunction()
.Add(Subqueries.WhereExists(hasTempAccount))
.Add(Subqueries.WhereNotExists(doesNotHaveTempAccount))
);
UPDATE 2:
Since NH 3.2 you can add extra conditions to a JOIN. See this answer for further details: Adding conditions to outer joins with NHibernate ICriteria/QueryOver query
Temp tempAlias = null;
Account accountAlias = null;
dto dto = null;
var results = Session.QueryOver<Account>(()=>accountAlias)
.JoinAlias(x=>x.TempAccounts,()=>tempAlias,JoinType.LeftOuterJoin,
Restrictions.IsNull(Projections.Property(()=>tempAlias.IsAccepted))
)
.Where(x=>x.Account.Id==65)
.SelectList(list=>list
.Select(()=>accountAlias.Id).WithAlias(()=>dto.AccountId)
.Select(()=>accountAlias.User.Id).WithAlias(()=>dto.UserId)
.Select(()=>tempAlias.Name).WithAlias(()=>dto.TempAccName)
)
.SetResultTransformer(Transformers.AliasToBean<dto>())
.List<dto>();
I've been using .Skip() and .Take() extension methods with LINQ To SQL for a while now with no problems, but in all the situations I've used them it has always been for a single table - such as:
database.Users.Select(c => c).Skip(10).Take(10);
My problem is that I am now projecting a set of results from multiple tables and I want to page on the overall set (and still get the benefit of paging at the DB).
My entity model looks like this:
A campaign [has many] groups, a group [has many] contacts
this is modelled through a relationship in the database like
Campaign -> CampaignToGroupMapping -> Group -> GroupToContactMapping -> Contact
I need to generate a data structure holding the details of a campaign and also a list of each contact associated to the campaign through the CampaignToGroupMapping, i.e.
Campaign
CampaignName
CampaignFrom
CampaignDate
Recipients
Recipient 1
Recipient 2
Recipient n...
I had tried to write a LINQ query using .SelectMany to project the set of contacts from each group into one linear data set, in the hope I could .Skip() .Take() from that.
My attempt was:
var schedule = (from c in database.Campaigns
where c.ID == highestPriority.CampaignID
select new PieceOfCampaignSchedule
{
ID = c.ID,
UserID = c.UserID,
Name = c.Name,
Recipients = c.CampaignGroupsMappings.SelectMany(d => d.ContactGroup.ContactGroupMappings.Select(e => new ContactData() { /*Contact Data*/ }).Skip(c.TotalSent).Take(totalRequired)).ToList()
}).SingleOrDefault();
The problem is that the paging (with regards to Skip() and Take()) is happening for each group, not the entire data set.
This means if I use the value 200 for the parameter totalRequired (passed to .Take()) and I have 3 groups associated with this campaign, it will take 200 from each group - not 200 from the total data from each group associated with the campaign.
In SQL, I could achieve this with a query such as:
select * from
(
select [t1].EmailAddress, ROW_NUMBER() over(order by CampaignID desc) as [RowNumber] from contacts as [t1]
inner join contactgroupmapping as [t2] on [t1].ID = [t2].ContactID
inner join campaigngroupsmapping as [t3] on [t3].ContactGroupID = [t2].GroupID
where [t3].CampaignID = #HighestPriorityCampaignID
) as [Results] where [Results].[RowNumber] between 500 and 3000
With this query, I'm paging over the combined set of contacts from each group associated with the particular campaign. So my question is, how can I achieve this using LINQ To SQL syntax instead?
To mimic the SQL query you provided you would do this:
var schedule = (from t1 in contacts
join t2 in contactgroupmapping on t1.ID equals t2.GroupID
join t3 in campaigngroupsmapping on t3.ContactGroupID = t2.GroupID
where t3.CampaignID = highestPriority.CampaignID
select new PieceOfCampaignSchedule
{
Email = t1.EmailAddress
}).Skip(500).Take(2500).ToList()
Are you trying to page over campaigns, recipients, or both?
Use a view to aggregate the results from the multiple tables and then use LINQ over the view
I think your attempt is really close; Maybe I'm missing something, but I think you just need to close your SelectMany() before the Skip/Take:
Recipients = c.CampaignGroupsMappings.SelectMany(d => d.ContactGroup.ContactGroupMappings.Select(e => new ContactData() { /*Contact Data*/ })).Skip(c.TotalSent).Take(totalRequired).ToList()
Note: added ")" after "/* Contact Data */ })" and removed ")" from after ".Take(totalRequired)"
I am trying to add the following custom sql to a finder condition and there is something not quite right.. I am not an sql expert but had this worked out with a friend who is..(yet they are not familiar with rubyonrails or activerecord or finder)
status_search = "select p.*
from policies p
where exists
(select 0 from status_changes sc
where sc.policy_id = p.id
and sc.status_id = '"+search[:status_id].to_s+"'
and sc.created_at between "+status_date_start.to_s+" and "+status_date_end.to_s+")
or exists
(select 0 from status_changes sc
where sc.created_at =
(select max(sc2.created_at)
from status_changes sc2
where sc2.policy_id = p.id
and sc2.created_at < "+status_date_start.to_s+")
and sc.status_id = '"+search[:status_id].to_s+"'
and sc.policy_id = p.id)" unless search[:status_id].blank?
My find statement:
Policy.find(:all,:include=>[{:client=>[:agent,:source_id,:source_code]},{:status_changes=>:status}],
:conditions=>[status_search])
and I am getting this error message in my log:
ActiveRecord::StatementInvalid (Mysql::Error: Operand should contain 1 column(s): SELECT DISTINCT `policies`.id FROM `policies` LEFT OUTER JOIN `clients` ON `clients`.id = `policies`.client_id WHERE ((((policies.created_at BETWEEN '2009-01-01' AND '2009-03-10' OR policies.created_at = '2009-01-01' OR policies.created_at = '2009-03-10')))) AND (select p.*
from policies p
where exists
(select 0 from status_changes sc
where sc.policy_id = p.id
and sc.status_id = '2'
and sc.created_at between 2009-03-10 and 2009-03-10)
or exists
(select 0 from status_changes sc
where sc.created_at =
(select max(sc2.created_at)
from status_changes sc2
where sc2.policy_id = p.id
and sc2.created_at < 2009-03-10)
and sc.status_id = '2'
and sc.policy_id = p.id)) ORDER BY clients.created_at DESC LIMIT 0, 25):
what is the major malfunction here - why is it complaining about the columns?
The conditions modifier is expecting a condition (e.g. a boolean expression that could go in a where clause) and you are passing it an entire query (a select statement).
It looks as if you are trying to do too much in one go here, and should break it down into smaller steps. A few suggestions:
use the query with find_by_sql and don't mess with the conditions.
use the rails finders and filter the records in the rails code
Also, note that constructing a query this way isn't secure if the values like status_date_start can come from users. Look up "sql injection attacks" to see what the problem is, and read the rails documentation & examples for find_by_sql to see how to avoid them.
Ok, I've managed to retool this so it is more friendly to a conditions modifier and I think it is doing the sql query correctly.. however, it is returning policies that when I try to list the current status (the policy.status_change.last.status) it is set to the same status used in the query - which is not correct
here is my updated condition string..
status_search = "status_changes.created_at between ? and ? and status_changes.status_id = ?) or
(status_changes.created_at = (SELECT MAX(sc2.created_at) FROM status_changes sc2
WHERE sc2.policy_id = policies.id and sc2.created_at < ?) and status_changes.status_id = ?"
is there something obvious to this that is not returning all of the remaining associated status changes once it finds the one in the query?
here is the updated find..
Policy.find(:all,:include=>[{:client=>[:agent,:source_id,:source_code]},:status_changes],
:conditions=>[status_search,status_date_start,status_date_end,search[:status_id].to_s,status_date_start,search[:status_id].to_s])