Spring JPA Specification Not Exists and Self Join - spring-data-jpa

How to build the following SQL query in Spring JPA Specification?
SELECT col1, col2, MAX(col3)
FROM table_name t1
WHERE col4 IN (1,2,3) AND status IN ('STATUS_1','STATUS_2') AND
NOT EXISTS
(
SELECT 1 FROM table_name t2 WHERE t1.id = t2.parent_id
AND t2.status IN ('STATUS_3','STATUS_4')
)
GROUP BY col1, col2;
Java code:
return new Specification<TableEntity>() {
#Override
public Predicate toPredicate(Root<TableEntity> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// how to build Predicates for the above query which has self-join and not exists.
}
};

try this
return new Specification<TableEntity>() {
#Override
public Predicate toPredicate(Root<TableEntity> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
Subquery<TableEntity> subquery = query.subquery(TableEntity.class);
Root<TableEntity> subqueryRoot = subquery.from(TableEntity.class);
subquery.select(subqueryRoot);
subquery.where(builder.and(builder.equal(root, subqueryRoot.get("parent")),
subqueryRoot.get("status").in("STATUS_3","STATUS_4"))
);
return builder.not(builder.exists(subquery));
}
};

Related

Improve performance in LINQ To Entities query (aggregation)

I have this EF model:
class Reception
{
public string Code { get; set; }
public virtual List<Row> { get; set; }
}
class Row
{
public string Item { get; set; }
public int Quantity { get; set; }
public float Weight { get; set; }
}
Is there a way to improve the following LINQ To Entities query?
dbContext.Receptions.Select(r => new
{
code = r.Code,
quantitySum = r.Rows.Sum(e => e.Quantity),
weightSum = r.Rows.Sum(e => e.Weight),
});
I'm worried about doing twice the "r.Rows" part.
Should I not be worried?
Aggregate queries (and especially these containing multiple aggregates) are better translated to SQL if based on GroupBy, because that's the natural SQL construct for aggregates.
So if you want better translation and suffer the code readability, the query in question can be turned into left outer join + group by like this:
var query = dbContext.Receptions
.SelectMany(r => r.Rows.DefaultIfEmpty(), (r, e) => new
{
r.Code,
Quantity = (int?)e.Quantity ?? 0,
Weight = (float?)e.Weight ?? 0,
})
.GroupBy(e => e.Code, (key, g) => new
{
code = key,
quantitySum = g.Sum(e => e.Quantity),
weightSum = g.Sum(e => e.Weight),
});
which translates to something like this
SELECT
1 AS [C1],
[GroupBy1].[K1] AS [Code],
[GroupBy1].[A1] AS [C2],
CAST( [GroupBy1].[A2] AS real) AS [C3]
FROM ( SELECT
[Join1].[K1] AS [K1],
SUM([Join1].[A1]) AS [A1],
SUM([Join1].[A2]) AS [A2]
FROM ( SELECT
[Extent1].[Code] AS [K1],
CASE WHEN ([Extent2].[Quantity] IS NULL) THEN 0 ELSE [Extent2].[Quantity] END AS [A1],
CASE WHEN ([Extent2].[Weight] IS NULL) THEN cast(0 as real) ELSE [Extent2].[Weight] END AS [A2]
FROM [dbo].[Receptions] AS [Extent1]
LEFT OUTER JOIN [dbo].[Rows] AS [Extent2] ON [Extent1].[Code] = [Extent2].[Reception_Code]
) AS [Join1]
GROUP BY [K1]
) AS [GroupBy1]
which is the best you can get from EF6 for this particular query.
Normally, EF should convert this to a fairly efficient DB query, using SUM functions on the DB side as well. But if you want to be sure, use your SQL Server Profiler to analyze the query.
I ran this code quickly for you and this is what EF does:
SELECT
[Project2].[Id] AS [Id],
[Project2].[Code] AS [Code],
[Project2].[C1] AS [C1],
CAST( [Project2].[C2] AS real) AS [C2]
FROM
(SELECT
[Project1].[Id] AS [Id],
[Project1].[Code] AS [Code],
[Project1].[C1] AS [C1],
(SELECT SUM([Extent3].[Weight]) AS [A1] FROM [dbo].[Rows] AS [Extent3] WHERE [Project1].[Id] = [Extent3].[Reception_Id]) AS [C2]
FROM
(SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Code] AS [Code],
(SELECT SUM([Extent2].[Quantity]) AS [A1] FROM [dbo].[Rows] AS [Extent2] WHERE [Extent1].[Id] = [Extent2].[Reception_Id]) AS [C1]
FROM
[dbo].[Receptions] AS [Extent1]
)AS [Project1]
)AS [Project2]
Surely, we can write a better query, with less subselects, but in reality, this query is not something SQL server will trip over.

Entity Framework generates datetime2 parameters for comparing to a date column -- how to match the data type of the column

Is there anyway to force Entity Framework into sending date parameters to match the column data type? This is causing a very costly key lookup in some of our queries.
An example generated query runs in minutes, I grabbed from SQL Server profiler:
exec sp_executesql N'SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
COUNT(1) AS [A1]
FROM [table] AS [Extent1]
WHERE ( EXISTS (SELECT
1 AS [C1]
FROM [search] AS [Extent2]
WHERE ([Extent1].[Number] = [Extent2].[Number]) AND ([Extent2].[SearchInstance] = #p__linq__0)
)) AND ([Extent1].[Date] >= #p__linq__1) AND ([Extent1].[Date] <= #p__linq__2)
) AS [GroupBy1]',N'#p__linq__0 uniqueidentifier,#p__linq__1 datetime2(7),#p__linq__2 datetime2(7)',#p__linq__0='1A530478-17F8-442E-B718-32049086717F',#p__linq__1='2012-07-24',#p__linq__2='2015-07-24'
If I manually change the types of the parameters to date, this query executes in 0 seconds.
exec sp_executesql N'SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
COUNT(1) AS [A1]
FROM [table] AS [Extent1]
WHERE ( EXISTS (SELECT
1 AS [C1]
FROM [search] AS [Extent2]
WHERE ([Extent1].[Number] = [Extent2].[Number]) AND ([Extent2].[SearchInstance] = #p__linq__0)
)) AND ([Extent1].[Date] >= #p__linq__1) AND ([Extent1].[Date] <= #p__linq__2)
) AS [GroupBy1]',N'#p__linq__0 uniqueidentifier,#p__linq__1 date,#p__linq__2 date',#p__linq__0='1A530478-17F8-442E-B718-32049086717F',#p__linq__1='2012-07-24',#p__linq__2='2015-07-24'
Using ef codefirst, I specifed .HasColumType("date") but did not change the parameter type being sent.
** names of tables & columns have been changed to protect the innocent.
Crudely, using a DbInterceptor.
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Data.SqlClient;
using System.Linq;
public static class SomewhereDoThis
{
public static void Register()
{
DbInterception.Add(new DateInterceptor());
}
}
public class DateInterceptor : IDbCommandInterceptor
{
/* .. */
public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
ShrinkDates(command);
}
public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
ShrinkDates(command);
}
public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext)
{
ShrinkDates(command);
}
public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
ShrinkDates(command);
}
private static void ShrinkDates(DbCommand command)
{
command.Parameters
.OfType<SqlParameter>()
.Where(p => p.SqlDbType == SqlDbType.DateTime2)
.Where(p => p.Value != DBNull.Value)
.Where(p => p.Value is DateTime)
.Where(p => ((DateTime)p.Value).TimeOfDay == TimeSpan.Zero)
.ToList()
.ForEach(p => p.SqlDbType = SqlDbType.Date);
}
}

LINQ-to-Entity Joined Table Return

I have two tables as follows:
Departments:
DeptID (PK)
DeptName
...
Employees:
EmpID (PK)
DeptID (FK)
EmpName
...
and I have a query using LINQ as follows:
public List<Employee> GetEmployee(int deptID)
{
var query = from e in mdc.Employees
join d in mdc.Departments on e.DeptID equals d.DeptID
where e.DeptID == deptID
select new { e.EmpID, e.EmpName, d.DeptName };
return query.ToList();
}
Now my question is this. I would like to select the fields EmpID, EmpName, and DeptName.
But what will my return type be? This one returns an error because this query returns a GenericList as opposed to my List<Employee>.
You need to create another class with required properties like this,
public class NewType{
public EmpID{get;set;}
//other fields here
}
and then select ,
public List<NewType> GetEmployee(int deptID)
{
var query = from e in mdc.Employees
join d in mdc.Departments on e.DeptID equals d.DeptID
where e.DeptID == deptID
select new NewType{ e.EmpID, e.EmpName, d.DeptName };
return query.ToList();
}

Concatenation in Linq-to-Entities / Entity Framework

I want to perform the following task in Linq-to-Entities that can be done easily in SQL. Please help in this regard.
select 0 as eid, 'Select' as eName
union
select eid , empcode + '' + empname as eName
from tblemp
This is even easier than the link coined by Meysam.
CoreUtil.ToEnumerable(new Employee { Name = "Select" })
.Concat(context.Employees.AsEnumerable())
where ToEnumerable is a very useful little function you'll use more often:
public static class CoreUtil
{
public static IEnumerable<T> ToEnumerable<T>(params T[] items)
{
return items;
}
}

Entity Framework code first - many to many filtering

Small EF question.
I have a many to many relationship mapped in EF. X..Y
So when I have one X there is a property X.Ys.
Now what I want to do is use a Linq Query to get several X's but I don't want to have all Y's inside the selected X's.
I want the Y's filtered on Y.RegistrationDate > Date.Today.
So when I have one X and itterate through .Y's I will only get future Y's.
UPDATE
This works, resulting in S having distinct ug's with it's relationship only containing upcoming events.
But don't tell me this cant be simplified??!!
var t = (from ug in uof.Ugs.All().ToList()
from upcomingEvent in ug.Events
where upcomingEvent.Date >= DateTime.Today
select new
{
ug,
upcomingEvent
}).ToList();
var s = (from ug in t.Select(x => x.ug).Distinct()
select new UG
{
Id = ug.Id,
Name = ug.Name,
Description = ug.Description,
WebSite = ug.WebSite,
Events = ug.Events.Where(x => x.Date >= DateTime.Today).ToList()
}).ToList();
UPDATE2
Added image to show that even with basic context manipulation I'm still getting 2 events, event when I take 1!
exampledebugimage
EF does not support this scenario as you want it, what you can do however is this:
var date = DateTime.Date;
var query = from x in Xs
select new
{
X = x
Ys = x.Ys.Where(i = > i.RegistrationDate > date)
}
Which will give you a collection of X's with their corresponding Y's that match your criteria.
Have you tried?:
var query = Xs
.Select(x => new { x, yCol = x.YCol.Where(y => y.Date >= DateTime.Today) })
.AsEnumerable()
.Select(x => x.x)
.ToList();
See: http://blogs.msdn.com/b/alexj/archive/2009/10/13/tip-37-how-to-do-a-conditional-include.aspx
All those .ToList you use will mean you load the whole table from the db before filtering. So watch out for that.
UPDATE: As fixup doesn't work with Many-To-Many
As Slauma mentioned in the comments make sure you don't use this technique if you are going to submit the changes as the changetracking will think you altered the collection. Or even better make sure you use .AsNoTracking() which will improve performance anyway.
We can use the same solution as above but slightly different for many-to-many. See this example:
[TestClass]
public class ContextTest
{
[TestMethod]
public void FixupTest()
{
Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
using (var db = new Context())
{
db.Groups.Add(new Group
{
Name = "G1",
Users = new List<User>{
new User{ Name = "M"},
new User{Name = "S"}
}
});
db.SaveChanges();
}
using (var db = new Context())
{
var group = db.Groups
.Select(g => new { g, Users = g.Users.Where(u => u.Name == "M") })
.AsEnumerable()
.Select(g => {
g.g.Users = g.Users.ToList();
return g.g;
})
.First();
Assert.AreEqual(1, group.Users.Count);
}
}
}
public class User
{
public int ID { get; set; }
public string Name { get; set; }
public ICollection<Group> Groups { get; set; }
}
public class Group
{
public int ID { get; set; }
public string Name { get; set; }
public ICollection<User> Users { get; set; }
}
The test pass and the generated sql is:
SELECT
[Project1].[ID] AS [ID],
[Project1].[Name] AS [Name],
[Project1].[C1] AS [C1],
[Project1].[ID1] AS [ID1],
[Project1].[Name1] AS [Name1]
FROM ( SELECT
[Extent1].[ID] AS [ID],
[Extent1].[Name] AS [Name],
[Join1].[ID] AS [ID1],
[Join1].[Name] AS [Name1],
CASE WHEN ([Join1].[Group_ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM [dbo].[Groups] AS [Extent1]
LEFT OUTER JOIN (SELECT [Extent2].[Group_ID] AS [Group_ID], [Extent3].[ID] AS [ID], [Extent3].[Name] AS [Name]
FROM [dbo].[GroupUsers] AS [Extent2]
INNER JOIN [dbo].[Users] AS [Extent3] ON [Extent3].[ID] = [Extent2].[User_ID] ) AS [Join1] ON ([Extent1].[ID] = [Join1].[Group_ID]) AND (N'Mikael' = [Join1].[Name])
) AS [Project1]
ORDER BY [Project1].[ID] ASC, [Project1].[C1] ASC