Select latest records and add results as new columns - c#

I have the following database
my StudentRecords for specific sutdent is
How can i get the latest 3 studentRecords based on the latest Records.RecordDate
so for each sutdent, row will have ReportItems.ReportItemName ReportItems.TotalPSR,, Students.FullName, + add the following columns:
The latest studentRecords based on RecordDate and display the psr + reordsNames + recordDate so that one row look like this
Is this even possible?
And how can i do this using EF6 also what will be the SQL select statment

Providing your EF entities are set up with their mappings correctly to associate the Students, StudentRecords, and Records then you should be able to accomplish this in EF using something like the following:
var results = testContext.Students
.SelectMany(s => s.StudentRecords.OrderByDescending(sr => sr.Record.RecordDate).Take(3))
.GroupBy(x => x.Student)
.ToList();
What this will give you is a result per-student in a grouped structure where the grouping is on the student, with the grouped values are StudentResults. From there you can extract the student details from the "Key", while each result is a collection of (up to) 3 latest StudentRecords in descending order. You will need to handle the scenario where the # of results is < 3.
As a simple example extraction...
foreach (var studentGrouping in results)
{
studentName = studentGrouping.Key.FullName;
psr1 = studentGrouping[0].PSR;
recordName1 = studentGrouping[0].Record.RecordName;
recordDate1 = studentGrouping[0].Record.RecordDate;
// ... etc.
}
Now a caveat to the above solution is that it will invoke lazy-loading on the Records, so up to 3 hits per student. Curiously I tried to mitigate this with .Include(s=>s.StudentRecords.Select(r=>r.Record)) or trying the .Include(r=> r.Record) after the SelectMany() but while the initial query looked to include the columns for the Records, it still triggered the lazy loads.
If anyone can offer a correction to address the lazy loads, I'd love to hear it. I was a bit surprised to see them after adding the Include statements.
If your object model is more complex, such as lots of fields in Student/Record, etc. then I would recommend altering the above to return an anonymous type structure to retrieve just the values you're going to need to populate your results. The above is Ok for simple structures.
For example if Student had 30 columns, but we only care about the ID and Name:
var results = testContext.Students
.SelectMany(s => s.StudentRecords.OrderByDescending(sr => sr.Record.RecordDate).Take(3))
.Select( sr=> new {sr.Student.StudentId, sr.Student.FullName, sr.PSR, sr.Record.RecordId, RecordName = sr.Record.Name})
.GroupBy(x => new{StudentId, FullName})
.ToList();
This would return a structure where the Key was an anonymous type containing just the StudentId and Name, with the grouped values being the type containing the record details for that student. This has the benefit of only returning the data you will need in a single query to the database.

You can use Row_Number and Pivot as below:
;With Cte as (
Select RI.ReportItemName, RI.TotalPSR, S.FullName, SR.PSR, R.[Name], R.[Date],
RowNPSR = Row_Number() over(Partition by SR.StudentRecord order by R.RecordDate DESC),
RowName = Row_Number() over(Partition by SR.StudentRecord order by R.RecordDate DESC) + 500,
RowDate = Row_Number() over(Partition by SR.StudentRecord order by R.RecordDate DESC) + 1000
from
StudentRecords SR
Join Students s
on SR.StudentId = S.StudentId
Join Records R
On SR.RecordId = R.recordId
Left Join ReportItems RI
On S.ReportItemId = RI.ReportItemId
)
Select a.*, p1.[1] as PSR1, p1.[2] as PSR2, p1.[3] as PSR3
, p2.[501] as Name1, p2.[502] as Name2, p2.[503] as Name3 from (
Select * from CTE RowNPSR <= 3 ) a
Pivot (max(PSR) for RowNPSR in ([1],[2],[3]) ) p1
Pivot (max([Name]) for RowName in ([501],[502],[503]) ) p2
Pivot (max([Date]) for RowDate in ([1001],[1002],[1003]) ) p3

Related

string.Join in Entity framwork, LINQ to SQL. Without Client-side evaluation

If you have a table, similar to here:
DataTypeID, DataValue
1,"Value1"
1,"Value2"
2,"Value3"
3,"Value4"
and want output like this:
DataTypeID,DataValues
1,"Value1,Value2"
2,"Value3"
3,"Value4"
Most questions suggest like this to use toList() or AsEnumerable() and then, string.Join(", ", DataValues) on client-side. This might work if the data is not huge but it defeats the purpose of using EF. How can I do this without loading all the data in-memory?
UPDATE: As of EF7 preview 7, now you simply use string.Join normally for example:
_context.MyTable
.GroupBy(keySelector => keySelector.MyKey, elemSelector => elemSelector.StringProp)
.Select(elem => string.Join(',', elem))
//.FirstOrDefaultAsync(cancellationToken), if (keyselector => 1) i.e. only 1 group so you get all rows
Old answer
Well, as per this this issue, string.Join() is yet to be implemented(as of now) and IEnumerable.Aggregate will not translate either.
In the meanwhile, you can create a view and write your SQL there.
For example, to group by id and string.Join(", ", Names);
CREATE VIEW V_Name AS
SELECT ID,
Names=STUFF
(
(
SELECT DISTINCT ' || '+ CAST(Child.Name AS VARCHAR(MAX))
FROM Child,MainTable
WHERE Main.ID= t1.ID --this line is imp...
AND Child.ID=MainTable.ID
FOR XMl PATH('')
),1,1,''
)
FROM MainTable t1
GROUP BY t1.IDReview
OR
CREATE VIEW V_Name AS
SELECT ID, STRING_AGG(Name, ', ') AS Names
FROM MainTable
LEFT JOIN ChildTable ON MainTable.ID = ChildTable.ID
GROUP BY ID
Now, in your C# you can simply join this with your ID, just like you normally would with an IQueryable:
from data in _dbcontext.sometable
join groupedAndJoinedNames in _dbcontext.viewname
on data.ID equals groupedAndJoinedNames.ID
select new
{
Names = groupedAndJoinedNames.Names
}

Get only rows with the latest date for each name

I'm trying to write a query that returns only those rows that contain the latest date for each name.
So for example, this data:
Name
Date Sold
More Columns...
Bob
2021-01-05
Mike
2021-01-18
Susan
2021-01-23
Bob
2021-02-04
Susan
2021-02-16
Mike
2021-03-02
Would produce this result:
Name
Date Sold
More Columns...
Bob
2021-02-04
Susan
2021-02-16
Mike
2021-03-02
It's sort of like a GROUP BY, but I'm not aggregating anything. I only want to filter the original rows.
How could I write such a query?
NOTE: In the end, this will be a SQL Server query but I need to write it using Entity Framework.
UPDATE: In reality, this is part of a much more complex query. It would be extremely difficult for me to implement this as a raw SQL query. If at all possible, I need to implement using Entity Framework.
Two options
Select top 1 with ties *
From YourTable
Order by row_number() over (partition by Name order by Sold_Date desc)
or slightly more performant
with cte as (
Select *
,RN = row_number() over (partition by Name order by Sold_Date desc)
From YourTable
)
Select *
From cte
Where RN=1
Adapted from
Error while flattening the IQueryable<T> after GroupBy()
var names = _context.Items.Select(row => row.Name).Distinct();
var items =
from name in names
from item in _context.Items
.Where(row => row.Name == name)
.OrderByDescending(row => row.DateSold)
.Take(1)
select item;
var results = items.ToArrayAsync();
Let's break this down:
A query expression which establishes the keys for our next query. Will eventually be run as a subquery.
var names = _context.Items.Select(row => row.Name).Distinct();
Another query, starting with the keys...
var items =
from name in names
... and for each key, let's find the matching row ...
from item in _context.Items
.Where(row => row.Name == name)
.OrderByDescending(row => row.DateSold)
.Take(1)
... and we want that row.
select item;
Run the combined query.
var results = items.ToArrayAsync();
try this
;with Groups as
(
Select [Name], max([Date Sold]) as [Date Sold]
From Table
Group By [Name]
)
Select Table.* From Groups
Inner Join Table on Table.[Name] = Groups.Name And Table.[Date Sold] = Groups.[Date Sold]

Get the id of the latest record in group by with linq

I'm trying to get the latest record for a group in linq but I want the id, not the date as sometimes the dates can be exactly the same as other records.
The following code gives me the key and the last date
var latestPositions = from bs in Scan
group bs by bs.Asset into op
select new
{
Asset = op.Key,
LastSeen = op.Max(x => x.LastSeen)
};
Returns something like this...
Asset LastSeen
BS1 2020-05-10
BS2 2020-07-10
Which is what I need, but I then need to get to the rest of the data from that table row, but if I join it on the two columns I can get multiple rows, is there a way for me to return the id column in the group by, so I can join on that?
Thanks
GroupBy cannot help here because of SQL limitation. But you can write workaround
var latestPositions = from bs in Scan
where !Scan.Any(s => s.Asset == bs.Asset && s.LastSeen > bs.LastSeen)
select bs;
But I have to mention that fastest way is using window functions which are not available in EF Core:
SELET
sc.Id
FROM (
SELECT
s.Id,
ROW_NUMBER() OVER (PARTITION BY s.Asset ORDER BY s.LastSeen DESC) AS RN
FROM Scan s
) sc
WHERE sc.RN == 1
But there is exists EF Core extension linq2db.EntityFrameworkCore which makes it possible via LINQ (I assume that Asset is just ID, not a navigation property)
var queryRn = from bs in Scan
select new
{
Entity = bs,
RN = Sql.Ext.RowNumber().Over()
.PartitionBy(bs.Asset).OrderByDesc(bs.LastSeen).ToValue()
};
// switch to alternative translator
queryRn = queryRn.ToLinqToDB();
var latestPositions = from q in queryRn
where q.RN == 1
select q.Entity;
I think I did what you wanted above and I wrote the full code on this link
If it's not what you want, can you write what you want a little more clearly.
var temp = from l in list
group l by l.Asset into lg
orderby lg.Key
select new { Asset = lg.Key, LastSeen = lg.Max(x => x.LastSeen), ID = lg.Where(x => x.LastSeen == lg.Max(y => y.LastSeen)).Single().ID };
So every Scan has a property Asset, a DateTime property LastSeen, and zero or more other properties.
Requirement: given a sequence of Scans, give me per Asset (all or some of the ) properties of the LastSeen Scan.
The following will do the trick:
var lastSeenScan = dbContext.Scans.GroupBy(scan => scan.Asset,
// parameter resultSelector: take every Asset, and all Scans that have this Asset value
// and order these Scans by descending value of lastSeen:
(asset, scansWithThisAssetValue) => scansWithThisAssetValue
.OrderByDescending(scan => scan.LastSeen)
// Select the scan properties that you plan to use.
// Not needed if you want the complete Scan
.Select(scan => new
{
Id = scan.Id,
Operator = scan.Operator,
...
})
// and keep only the First one, which is the Last seen one:
.FirstOrDefault();
In words: divide your table of of Scans into groups of scans that have the same value for property Asset. Order all scans in each group by descending value of LastSeen. This will make the Scan that has last been seen the first one.
From all scans in the group select only the properties that you plan to use, and take the first one.
Result: for every used Asset, you get the (selected properties of the) scan that has the highest value of LastSeen.

LINQ: Group by aggregate but still get information from the most recent row?

Let's say I have a table that holds shipping history. I'd like to write a query that counts the amount of shipments per user and gets the shipping name from the most recent entry in the table for that user.
Table structure for simplicity:
ShipmentID
MemberID
ShippingName
ShippingDate
How do I write a LINQ C# query to do this?
It sounds like might want something like:
var query = from shipment in context.ShippingHistory
group shipment by shipment.MemberID into g
select new { Count = g.Count(),
MemberID = g.Key,
MostRecentName = g.OrderByDescending(x => x.ShipmentDate)
.First()
.ShipmentName };
Not really a LINQ answer, but personally, I'd be dropping to SQL for that, to make sure it isn't doing any N+1 etc; for example:
select s1.MemberID, COUNT(1) as [Count],
(select top 1 ShippingName from Shipping s2 where s2.MemberID = s1.MemberID
order by s2.ShippingDate desc) as [LastShippingName]
from Shipping s1
group by s1.MemberID
You can probably do LINQ something like (untested):
var qry = from row in data
group row by row.MemberId into grp
select new {
MemberId = grp.Key,
Count = grp.Count(),
LastShippingName =
grp.OrderByDescending(x => x.ShippingDate).First().ShippingName
};

Advice on removing multiple sub-queries (which contains a join) by optimizing Linq To SQL query

I'm working on adding globalization to my product cataloge and I have made it work. However, I feel that the underlaying SQL query isn't performing as well as it could and I could need some advice on how to change my Linq To SQL query to make it more efficient.
The tables that are used
Product contains a unique id for each product. This column is called EntityID
TextTranslation contains the globalized text. The columns in this table are CultureID (a string), TextID (a reference to the text), Value (the actuall globalized text).
Text contains the mapping between a globalized text and a product. There is also a column which indicates which type of text it is (like name, description and so on)
TextType contains the definition (id, name and description) for a text type.
var culturedTexts =
from translation in ctx.TextTranslations
join text in ctx.Texts on translation.TextId equals text.TextId
where translation.CultureId == "en-EN"
select new
{
text.EntityId,
text.TextTypeId,
translation.Value,
};
var products =
from p in ctx.Products
let texts = culturedTexts.Where(i => i.EntityId == p.EntityId)
select new Model.Product
{
Description = texts.Where(c => c.TextTypeId == (int)TextType.Description).SingleOrDefault().Value,
Name = texts.Where(c => c.TextTypeId == (int)TextType.Name).SingleOrDefault().Value
};
When this is executed I get a query which looks like
SELECT (
SELECT [t1].[Value]
FROM [Common].[TextTranslation] AS [t1]
INNER JOIN [Common].[Text] AS [t2] ON [t1].[TextId] = [t2].[TextId]
WHERE ([t2].[TextTypeId] = 2) AND ([t2].[EntityId] = [t0].[EntityId]) AND ([t1].[CultureId] = 'sv-SE')
) AS [Description], (
SELECT [t3].[Value]
FROM [Common].[TextTranslation] AS [t3]
INNER JOIN [Common].[Text] AS [t4] ON [t3].[TextId] = [t4].[TextId]
WHERE ([t4].[TextTypeId] = 1) AND ([t4].[EntityId] = [t0].[EntityId]) AND ([t3].[CultureId] = 'sv-SE')
) AS [Name]
FROM [Catalog].[Product] AS [t0]
So each globalized text (Name, Description) in the LINQ query gets its own sub query and associated join. Is it possible to streamline this a bit and remove each text type getting its own join and subquery?
Well, given that they are getting different TextTypeId values, how would you prefer the TSQL to look? If you do a single JOIN, you'll have to put in a messy SELECT CASE or similar to discriminate between type "1" and type "2".
One option would be to to simply bring back all the suitable rows and do the final projection in-memory at the client, but to be honest I expect that the SQL optimizer will make light work of that TSQL anyway... especially if that query hits a good spanning index.

Categories