简体   繁体   中英

Translating a complex SQL query into LINQ

I have plenty of experience with SQL but am fairly new to LINQ and am struggling to convert the following MySQL query into LINQ. Can anyone help convert the following to LINQ for use in an ASP.net MVC project with Entity framework?

SELECT
    S.Submission_ID,
    P.Photo_ID,
    C2.Contract_Name,
    J.Job_Number,
    D.Device_Name,
    A.`Display_Name`,
    S.Submission_Status,
    S.Submission_JobRef,
    S.Created,
    TRUE
FROM
    Submission S
        LEFT JOIN Job J ON S.`Job_ID` = J.`Job_ID`
        LEFT JOIN Contract C2 ON J.`Contract_ID` = C2.`Contract_ID`
        INNER JOIN Submission_Status SS ON S.`Submission_Status` = SS.`ID`
        INNER JOIN Device D ON S.`Device_ID` = D.`Device_ID`
        INNER JOIN ACTION A ON S.`Action_ID` = A.`Action_ID`
        INNER JOIN (
            SELECT
                MIN(P.Photo_ID) AS Photo_ID,
                P.Submission_ID
            FROM
                Photo P
            GROUP BY
                P.`Submission_ID`) P ON S.`Submission_ID` = P.Submission_ID
WHERE
    S.`Submission_Status` <> 3 AND
    (LOCATE(@Criteria, C2.`Contract_Name`) > 0 OR
    LOCATE(@Criteria, J.`Job_Number`) > 0 OR
    LOCATE(@Criteria, D.`Device_Name`) > 0 OR
    LOCATE(@Criteria, A.`Display_Name`) > 0 OR
    LOCATE(@Criteria, SS.`Value`) > 0 OR
    LOCATE(@Criteria, S.`Submission_JobRef`) > 0)
ORDER BY
    S.`Submission_ID` DESC

I have tried to get my head around the multiple joins and subquery but have since got stuck. This is what I have so far...Obviously, it is not working or complete!!

Dim results = From S In db.Submissions
                      Join P In db.Photos On S.Submission_ID Equals P.Submission_ID
                      Group Join J In db.Jobs On S.Job_ID Equals J.Job_ID
                        Into Job = Group
                      Join J In db.Jobs On S.Job_ID Equals J.Job_ID
                      Group By P.Submission_ID
                        Into SubmissionPhotoID = Min(P.Photo_ID)
                      Select New With {.Submission_ID = Submission_ID,
                                       .Photo_ID = SubmissionPhotoID,
                                       .Contract_Name = If(IsNothing(S.Job), "", S.Job.Contract.Contract_Name),
                                       .Job_Number = If(IsNothing(S.Job), "", S.Job.Job_Number),
                                       .Device_Name = S.Device.Device_Name,
                                       .Action_Name = S.Action.Display_Name,
                                       .Submission_Status = S.Submission_Status1.ID,
                                       .Submission_JobRef = S.Submission_JobRef,
                                       .Created = S.Created,
                                       .CanEdit = bolCanEdit}
                      Order By S.Submission_ID
                      Skip param.iDisplayStart
                      Take param.iDisplayLength

Any help or guidance with the above would be greatly appreciated!


Edit

To aid things, here are the classes from the model defining the entities used in the above query. (I have omitted some field which have no relevance to the question).

Partial Public Class Submission
    Public Property Submission_ID As Integer
    Public Property Job_ID As Nullable(Of Integer)
    Public Property Device_ID As Integer
    Public Property Action_ID As Integer
    Public Property Submission_Status As Nullable(Of Integer)
    Public Property Submission_JobRef As String
    Public Property Created As Nullable(Of Date)

    Public Overridable Property Action As Action
    Public Overridable Property Device As Device
    Public Overridable Property Job As Job
    Public Overridable Property Photos As ICollection(Of Photo) = New HashSet(Of Photo)
    Public Overridable Property Submission_Status1 As Submission_Status
End Class

Partial Public Class Job
    Public Property Job_ID As Integer
    Public Property Contract_ID As Nullable(Of Integer)
    Public Property Job_Number As String

    Public Overridable Property Contract As Contract
    Public Overridable Property Submissions As ICollection(Of Submission) = New HashSet(Of Submission)
End Class

Partial Public Class Contract
    Public Property Contract_ID As Integer
    Public Property Contract_Name As String

    Public Overridable Property Jobs As ICollection(Of Job) = New HashSet(Of Job)
End Class

Partial Public Class Submission_Status
    Public Property ID As Integer
    Public Property Value As String

    Public Overridable Property Submissions As ICollection(Of Submission) = New HashSet(Of Submission)

End Class

Partial Public Class Device
    Public Property Device_ID As Integer
    Public Property Device_Name As String

    Public Overridable Property Submissions As ICollection(Of Submission) = New HashSet(Of Submission)
End Class

Partial Public Class Action
    Public Property Action_ID As Integer
    Public Property Display_Name As String

    Public Overridable Property Submissions As ICollection(Of Submission) = New HashSet(Of Submission)
End Class

Partial Public Class Photo
    Public Property Photo_ID As Integer
    Public Property Submission_ID As Integer

    Public Overridable Property Submission As Submission
End Class

That's a fairly complex piece of SQL, with a sub-select and mixture of left and inner joins. Some quick suggestions:

Break it down into a sequence of linq statements, starting with your core objects and adding the related pieces in subsequent steps. If you keep the results as IQueryable, the compiler will put it all together for you and send as one query to the db (ie don't ToList() until the last step).

Personally, I do joins using two from's and a where extension method than using the join operator. I makes it easier to know that you're getting a left join or an inner join, for one thing.

For example:

FROM Submission S LEFT JOIN Job J ON S.`Job_ID` = J.`Job_ID`

I would do this as (sorry I'm c# so the syntax may not be quite correct for VB)

Dim results = from s in db.Submissions
              from j in db.Jobs.Where(j=> j.Job_Id == s.Job_Id).DefaultIfEmpty()

So, the join criteria is inside the .Where() on Jobs and .DefaultIfEmpty() tells it to left-join (essentially, Job will be a default if the join fails).

FURTHER EDIT:

After experimenting, I got this code to return a result (is it the correct result is another question). Again, sorry for the c# syntax.

    [TestMethod]
    public void Query()
    {
        const string conStr = "Data Source=(local);Initial Catalog=ComplexSqlToLinq; Integrated Security=True";
        var db = new MyDbContext(conStr);

        const string criteria = "Contract1";

        var minPhotos = from p in db.Photos
                        group p by p.SubmissionId
                        into g
                        select new {SubmissionId = g.Key, PhotoId = g.Min(p=>p.PhotoId)};

        var query = from s in db.Submissions
                    from j in db.Jobs.Where(j => j.JobId == s.JobId).DefaultIfEmpty()
                    from c in db.Contracts.Where(c => c.ContractId == j.ContractId).DefaultIfEmpty()
                    from ss in db.SubmissionStatuses.Where(ss => ss.Id == s.SubmissionStatus)
                    from d in db.Devices.Where(d => d.DeviceId == s.DeviceId)
                    from a in db.Actions.Where(a => a.ActionId == s.ActionId)
                    from p in minPhotos.Where(p => p.SubmissionId == s.SubmissionId)

                    where s.SubmissionStatus != 3 &&
                       ( c.ContractName.Contains(criteria) ||
                         j.JobNumber.Contains(criteria) ||
                         d.DeviceName.Contains(criteria) ||
                         a.DisplayName.Contains(criteria) ||
                         ss.Value.Contains(criteria) ||
                         s.SubmissionJobRef.Contains(criteria))

                    select new
                               {
                                   s.SubmissionId,
                                   p.PhotoId,
                                   c.ContractName,
                                   j.JobNumber,
                                   d.DeviceName,
                                   a.DisplayName,
                                   s.SubmissionStatus,
                                   s.SubmissionJobRef,
                                   s.Created,
                                   SomeBool = true
                               };

        var result = query.ToList();
        Assert.IsTrue(result.Any());
    }

Obviously, you can vary the criteria constant in the test to apply to different items, I chose to match the Contract - I assume that only one of the tables will strike a match.

This query generates the following SQL, looks a bit hokey but is pretty similar in function to your original.

SELECT 
    [Filter1].[SubmissionId] AS [SubmissionId], 
    [GroupBy1].[A1] AS [C1], 
    [Filter1].[ContractName] AS [ContractName], 
    [Filter1].[JobNumber] AS [JobNumber], 
    [Filter1].[DeviceName] AS [DeviceName], 
    [Filter1].[DisplayName] AS [DisplayName], 
    [Filter1].[SubmissionStatus] AS [SubmissionStatus], 
    [Filter1].[SubmissionJobRef] AS [SubmissionJobRef], 
    [Filter1].[Created] AS [Created], 
    cast(1 as bit) AS [C2]
FROM   
(
    SELECT 
        [Extent1].[SubmissionId] AS [SubmissionId], 
        [Extent1].[SubmissionStatus] AS [SubmissionStatus], 
        [Extent1].[SubmissionJobRef] AS [SubmissionJobRef], 
        [Extent1].[Created] AS [Created], 
        [Extent2].[JobNumber] AS [JobNumber], 
        [Extent3].[ContractName] AS [ContractName], 
        [Extent4].[Value] AS [Value], 
        [Extent5].[DeviceName] AS [DeviceName], 
        [Extent6].[DisplayName] AS [DisplayName]
    FROM      
        [dbo].[Submissions] AS [Extent1]
        LEFT OUTER JOIN [dbo].[Jobs] AS [Extent2] ON [Extent2].[JobId] = [Extent1].[JobId]
        LEFT OUTER JOIN [dbo].[Contracts] AS [Extent3] ON [Extent3].[ContractId] = [Extent2].[ContractId]
        INNER JOIN [dbo].[SubmissionStatus] AS [Extent4] ON [Extent4].[Id] = [Extent1].[SubmissionStatus]
        INNER JOIN [dbo].[Devices] AS [Extent5] ON [Extent5].[DeviceId] = [Extent1].[DeviceId]
        INNER JOIN [dbo].[Actions] AS [Extent6] ON [Extent6].[ActionId] = [Extent1].[ActionId]
    WHERE 
        3 <> [Extent1].[SubmissionStatus] 
) AS [Filter1]

INNER JOIN  (
    SELECT 
        [Extent7].[SubmissionId] AS [K1], 
        MIN([Extent7].[PhotoId]) AS [A1]
    FROM 
        [dbo].[Photos] AS [Extent7]
    GROUP BY 
        [Extent7].[SubmissionId] ) AS [GroupBy1] 
    ON [GroupBy1].[K1] = [Filter1].[SubmissionId]
WHERE 
(
    [Filter1].[ContractName] LIKE @p__linq__0 ESCAPE N'~') OR 
    ([Filter1].[JobNumber] LIKE @p__linq__1 ESCAPE N'~') OR 
    ([Filter1].[DeviceName] LIKE @p__linq__2 ESCAPE N'~') OR 
    ([Filter1].[DisplayName] LIKE @p__linq__3 ESCAPE N'~') OR 
    ([Filter1].[Value] LIKE @p__linq__4 ESCAPE N'~') OR 
    ([Filter1].[SubmissionJobRef] LIKE @p__linq__5 ESCAPE N'~')
)   

To respond to Dave Johnson's comment in a word - scalability.

Recently I was trying to improve performance of an application and my first thought was to add some SQL similar in complexity to John Henry's sample - multiple joins and filters. After all, it performed like a rocket on my dev machine.

The architect flatly prohibited the use of complex SQL on the database server, on the basis that several large applications with 100's of users were hooked in to it. Much as I like building snappy SQL that rocks, I had to agree. Shifting the logic to to machine that consumes it is good architecture.

So for those of us proficient in declarative SQL, learning translation to linq skills is important.

Of course, the solution I gave earlier doesn't achieve this as the same SQL is sent to the server. But having a linq equivalent is a start that can be be further optimised.

After an awful lot of searching and reading various articles I have given up trying to write this query in LINQ query syntax and gone with method syntax instead.

A big thank you to Ackroydd for your suggestions and support with converting complex SQL to LINQ. When you know you can accomplish something in SQL in a matter of minutes but need to use LINQ for scalability and to keep with existing code, it can get rather frustrating!

Here is what I ended up with as I'm sure it will be useful to someone else:

Dim query As IQueryable(Of Submission)

' Initialise the new query
query = db.Submissions.Include(Function(s) s.Action) _
                              .Include(Function(s) s.Photos) _
                              .Include(Function(s) s.Device) _
                              .Include(Function(s) s.Job) _
                              .Include(Function(s) s.Submission_Status1) _
                              .Include(Function(s) s.Job.Contract) _
                              .Include(Function(s) s.Comments) _
                              .AsNoTracking

' Apply initial filters
query = query.Where(Function(S) Not S.Submission_Status1.ID.Equals(3))

' Apply search criteria if passed
If Not String.IsNullOrEmpty(param.sSearch) Then
    query = query.Where(Function(S) S.Job.Contract.Contract_Name.Contains(param.sSearch) OrElse
                                            S.Job.Job_Number.Contains(param.sSearch) OrElse
                                            S.Device.Device_Name.Contains(param.sSearch) OrElse
                                            S.Action.Display_Name.Contains(param.sSearch) OrElse
                                            S.Submission_Status1.Value.Contains(param.sSearch) OrElse
                                            S.Submission_JobRef.Contains(param.sSearch))
End If

' Order the results
query = query.OrderByDescending(Function(S) S.Submission_ID)

' Paginate the results
query = query.Skip(param.iDisplayStart).Take(param.iDisplayLength)

' Return only the required columns
Dim resultData = query.AsEnumerable.Select(Function(S) New AjaxSubmissionOverview With { _
                                                           .Submission_ID = S.Submission_ID,
                                                           .Photo_ID = S.Photos.First.Photo_ID,
                                                           .Contract_Name = If(IsNothing(S.Job), "", S.Job.Contract.Contract_Name),
                                                           .Job_Number = If(IsNothing(S.Job), "", S.Job.Job_Number),
                                                           .Device_Name = S.Device.Device_Name,
                                                           .Action_Name = S.Action.Display_Name,
                                                           .Submission_Status = S.Submission_Status,
                                                           .Submission_JobRef = S.Submission_JobRef,
                                                           .Latest_Comment = If(S.Comments.Count = 0, "", HtmlHelpers.Truncate(S.Comments.Last.Comment1, 100)),
                                                           .Created = S.Created,
                                                           .CanEdit = bolCanEdit})

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM