LINQ To SQL Paging - sql

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)"

Related

SQL Query to get specific result. It can be Lambda or LINQ

It is not a general question. It is all about my 3 tables and I couldn't figure out where to start.
Basically I have got 3 tables. ClientModels, DolsMcaItemModels and DolsMcaClientModels.
ClientModels - All the clients and their ClientID
DolsMcaItemModels - List of documents, there is a mandatory column.
DolsMcaClientModels - This table keeps all client DolsMcaItemModels.
I am trying to get list of clients who don't have mandatory documents in DolsMcaClientModels.
For example, if I have got a Mandatory "Passport" Document in DolsMcaItemModels and I want to get a list of clients who don't have a passport.
CLIENT TABLE
SELECT C.[ClientID]
,[ClientName]
,[ClientDOB]
FROM [dbo].[ClientModels]
DOCUMENT LIST
SELECT [DolsMcaItemID]
,[DolsMcaItemName]
,[DolsMcaItemMandatory]
,[DolsMcaItemStatus]
FROM [dbo].[DolsMcaItemModels]
WHERE [DolsMcaItemStatus] = true
Client Document Table
SELECT [DolsMcaClientID]
,[DolsMcaItemID]
,[ClientID]
,[DolsMcaClientItemStatus]
FROM [dbo].[DolsMcaClientModels]
So far what i did
List of clients who don't have any documents
SELECT C.[ClientID]
,[ClientName]
,[ClientDOB]
FROM [dbo].[ClientModels] C
LEFT JOIN [DolsMcaClientModels] CI ON C.ClientID = CI.ClientID
WHERE CI.ClientID IS NULL
and list mandatory is missing.
SELECT I.[DolsMcaItemID]
,[DolsMcaItemName]
,[DolsMcaItemLevel]
,[DolsMcaItemMandatory]
,[DolsMcaItemStatus]
FROM [dbo].[DolsMcaItemModels] I
LEFT JOIN [DolsMcaClientModels] CI2 ON I.DolsMcaItemID = CI2.DolsMcaItemID
WHERE CI2.DolsMcaItemID IS NULL AND [DolsMcaItemMandatory] = 1
I don't know how to combine this together.

LINQ: How can I Join to child table using data from group join?

I have to expand upon an existing query in one of the applications I'm working on.
Table relationships are as follows:
tableName relationship tableName2 Join columns
title 1:1 parcel p_id:p_id
title 1:many transaction title_no:new_title_no
transactions many:1 title old_title_no:title_no
The query is essentially a data loader to reduce the number of round trips to the database.
The existing query looks like this:
From title In context.Title
Join parcel In context.Parcel
On title.p_id Equals parcel.P_Id
Group Join transaction In context.Transaction
On title.title_no Equals transaction.New_Title_No And title.transfer_date Equals transaction.Transfer_Date Into transactions = Group
Select New TitleSearchResult() With {.Title = title, .Pc_Parcel = parcel, .Transactions = transactions}
Which returns a custom data type to store the different values.
The application also maps these values(the Title entity contains unmapped properties to store the data from the other entities) :
For Each result In query
result.Title.Parcel = result.Parcel
result.Title.Transactions = result.Transactions.ToList()
Next
I need to return the title associated with each transaction based on old_title_no.
I've tried to break the queries up as follows:
Dim query1 = From title In context.Title
Join parcel In context.Parcel
On title.p_id Equals parcel.P_Id
Select title, parcel
Dim query2 = From title_old In context.Title
Join trans As Transaction In context.Transaction
On title_old.title_no Equals trans.Old_Title_No
Select trans, title_old
Dim query3 = From b In query1
Group Join c In query2
On b.title.title_no Equals c.trans.New_Title_No And b.title.transfer_date Equals c.trans.Transfer_Date
Into transactions = Group
Select b.title, b.parcel, transactions
Which seems to do the right querying, but doesn't allow me to create a new TitleSearchResult object like the following (because I can't break out the data from the transaction object):
From t In query3.ToList()
Select New TitleSearchResult() With {.Title = t.title, .Pc_Parcel = t.parcel, _
.Transactions = t.transactions.trans, .Old_Titles = t.transactions.title_old}
How can I still do the group join on transaction (because I need the IEnumerable returned so that I can use it in another query on the database) and be able to separate the trans and title_old objects?
Assuming the TitleSearchResult has List types for Old_Titles and Transactions, you can do:
var ans = From t In query3.ToList() _
Select New TitleSearchResult() With {
.Title = t.title,
.Pc_Parcel = t.parcel,
.Transactions = t.transactions.Select(Function(t1) t1.trans).ToList(),
.Old_Titles = t.transactions.Select(Function(t1) t1.title_old).ToList()
}

Select Count of one table into another

I have one SQL statement as:
SELECT ARTICLES.NEWS_ARTCL_ID, ARTICLES.NEWS_ARTCL_TTL_DES,
ARTICLES.NEWS_ARTCL_CNTNT_T, ARTICLES.NEWS_ARTCL_PUB_DT,
ARTICLES.NEWS_ARTCL_AUTH_NM, ARTICLES.NEWS_ARTCL_URL, ARTICLES.MEDIA_URL,
ARTICLES.ARTCL_SRC_ID, SOURCES.ARTCL_SRC_NM, MEDIA.MEDIA_TYPE_DESCRIP
FROM
RSKLMOBILEB2E.NEWS_ARTICLE ARTICLES,
RSKLMOBILEB2E.MEDIA_TYPE MEDIA,
RSKLMOBILEB2E.ARTICLE_SOURCE SOURCES
WHERE ARTICLES.MEDIA_TYPE_IDENTIF = MEDIA.MEDIA_TYPE_IDENTIF
AND ARTICLES.ARTCL_SRC_ID = SOURCES.ARTCL_SRC_ID
AND ARTICLES.ARTCL_SRC_ID = 1
ORDER BY ARTICLES.NEWS_ARTCL_PUB_DT
Now I need to combine another SQL statement into one which is:
SELECT COUNT ( * )
FROM RSKLMOBILEB2E.NEWS_LIKES LIKES, RSKLMOBILEB2E.NEWS_ARTICLE ARTICLES
WHERE LIKES.NEWS_ARTCL_ID = ARTICLES.NEWS_ARTCL_ID
Basically I have one table which contains articles and I need to include the user likes which is in another table.
Use a subquery to add the likescount in your first query like this:
SELECT ARTICLES.NEWS_ARTCL_ID
,ARTICLES.NEWS_ARTCL_TTL_DES
,ARTICLES.NEWS_ARTCL_CNTNT_T
,ARTICLES.NEWS_ARTCL_PUB_DT
,ARTICLES.NEWS_ARTCL_AUTH_NM
,ARTICLES.NEWS_ARTCL_URL
,ARTICLES.MEDIA_URL
,ARTICLES.ARTCL_SRC_ID
,SOURCES.ARTCL_SRC_NM
,MEDIA.MEDIA_TYPE_DESCRIP
,(
SELECT COUNT(*)
FROM RSKLMOBILEB2E.NEWS_LIKES LIKES
WHERE LIKES.NEWS_ARTCL_ID = ARTICLES.NEWS_ARTCL_ID
) AS LikesCount
FROM RSKLMOBILEB2E.NEWS_ARTICLE ARTICLES
,RSKLMOBILEB2E.MEDIA_TYPE MEDIA
,RSKLMOBILEB2E.ARTICLE_SOURCE SOURCES
WHERE ARTICLES.MEDIA_TYPE_IDENTIF = MEDIA.MEDIA_TYPE_IDENTIF
AND ARTICLES.ARTCL_SRC_ID = SOURCES.ARTCL_SRC_ID
AND ARTICLES.ARTCL_SRC_ID = 1
ORDER BY ARTICLES.NEWS_ARTCL_PUB_DT;
I'm not sure what you are trying to achieve but it seems you want to count all the data from 2 tables. You can edit your query to something like this.
SELECT COUNT (ARTICLES.*) FROM RSKLMOBILEB2E.NEWS_LIKES LIKES
JOIN RSKLMOBILEB2E.NEWS_ARTICLE ARTICLES
ON LIKES.NEWS_ARTCL_ID = ARTICLES.NEWS_ARTCL_ID
I think that solution is in using Analytic Functions. Please have a look on https://oracle-base.com/articles/misc/analytic-functions
Please check following query (keep in mind I have no idea about your table structures). Due to left join records might be duplicated, this is why grouping is added.
SELECT ARTICLES.NEWS_ARTCL_ID, ARTICLES.NEWS_ARTCL_TTL_DES,
ARTICLES.NEWS_ARTCL_CNTNT_T, ARTICLES.NEWS_ARTCL_PUB_DT,
ARTICLES.NEWS_ARTCL_AUTH_NM, ARTICLES.NEWS_ARTCL_URL, ARTICLES.MEDIA_URL,
ARTICLES.ARTCL_SRC_ID, SOURCES.ARTCL_SRC_NM, MEDIA.MEDIA_TYPE_DESCRIP,
count(LIKES.ID) over ( partition by ARTICLES.NEWS_ARTCL_ID ) as num_likes
FROM RSKLMOBILEB2E.NEWS_ARTICLE ARTICLES
join RSKLMOBILEB2E.MEDIA_TYPE MEDIA
on ARTICLES.MEDIA_TYPE_IDENTIF = MEDIA.MEDIA_TYPE_IDENTIF
join RSKLMOBILEB2E.ARTICLE_SOURCE SOURCES
on ARTICLES.ARTCL_SRC_ID = SOURCES.ARTCL_SRC_ID
LEFT JOIN RSKLMOBILEB2E.NEWS_LIKES LIKES
ON LIKES.NEWS_ARTCL_ID = ARTICLES.NEWS_ARTCL_ID
WHERE
ARTICLES.ARTCL_SRC_ID = 1
group by ARTICLES.NEWS_ARTCL_ID, ARTICLES.NEWS_ARTCL_TTL_DES,
ARTICLES.NEWS_ARTCL_CNTNT_T, ARTICLES.NEWS_ARTCL_PUB_DT,
ARTICLES.NEWS_ARTCL_AUTH_NM, ARTICLES.NEWS_ARTCL_URL, ARTICLES.MEDIA_URL,
ARTICLES.ARTCL_SRC_ID, SOURCES.ARTCL_SRC_NM, MEDIA.MEDIA_TYPE_DESCRIP
ORDER BY ARTICLES.NEWS_ARTCL_PUB_DT
I also changed coma-separated list of tables from where condition to joins. I think this is more readable since table join conditions are separated from result filtering in where clause.

NHibernate Return Values

I am currently working on a project using NHiberate as the DAL with .NET 2.0 and NHibernate 2.2.
Today I came to a point where I had to join a bunch of entities/collections to get what I want. That is fine.
What got me was that I do not want the query to return a list of objects of a certain entity type but rather the result would include various properties from different entities.
The following query is not what I am doing but it is kind of query that I am talking about here.
select order.id, sum(price.amount), count(item)
from Order as order
join order.lineItems as item
join item.product as product,
Catalog as catalog
join catalog.prices as price
where order.paid = false
and order.customer = :customer
and price.product = product
and catalog.effectiveDate < sysdate
and catalog.effectiveDate >= all (
select cat.effectiveDate
from Catalog as cat
where cat.effectiveDate < sysdate
)
group by order
having sum(price.amount) > :minAmount
order by sum(price.amount) desc
My question is, in this case what type result is supposed to be returned? It is certainly not of type Order, neither is of type LineItems.
Thanks for your help!
John
you can always use List of object[] for returning data and it will work fine.
This is called a projection, and it happens any time you specify an explicit select clause that contains rows from various tables (or even aggregate / summary data from a single table).
Using LINQ you can create anonymous objects to store these rows of data, like this:
var crunchies = (from foo in bar
where foo.baz == quux
select new { foo.corge, foo.grault }).ToList();
Then you can do crunchies[0].corge for example to pull out the rows & columns.
If you are using NHibernate.Linq this will "just work".
If you're using HQL or Criteria API, then what Fahad mentioned will work. You'll get a List<object[]> as a result, and the index of the array references the order of the columns that you returned in your select clause.

Problem with adding custom sql to finder condition

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])