Entity Framework with multiple LEFT OUTER JOINs where one "joins" on to the other

Viewed 46

I'm relatively new to EF, so my apologies if this is an EF 101 type of question. I have a SQL query that I'm trying to convert to C#. The following SQL query returns 290,903 records:

SELECT clcl.CLCL_ID, clcl.CLST_MCTR_REAS, clcl.CLCL_LOW_SVC_DT, clcl.CLCL_ME_AGE, grgr.CICI_ID, clcl.NWNW_ID
FROM [FACETS].[dbo].[CMC_CLCL_CLAIM] clcl WITH(NOLOCK)
INNER JOIN [FACETS].[dbo].[CMC_CLED_EDI_DATA] cled WITH(NOLOCK)ON clcl.CLCL_ID = cled.CLCL_ID
INNER JOIN [FACETS].[dbo].[CMC_PRPR_PROV] prpr WITH(NOLOCK)ON clcl.PRPR_ID = prpr.PRPR_ID
INNER JOIN [FACETS].[dbo].[CMC_SBSB_SUBSC] sbsb WITH(NOLOCK)ON clcl.SBSB_CK = sbsb.SBSB_CK
INNER JOIN [FACETS].[dbo].[CMC_SBAD_ADDR] sbad WITH(NOLOCK)ON clcl.SBSB_CK = sbad.SBSB_CK
    AND sbsb.SBAD_TYPE_HOME = sbad.SBAD_TYPE
INNER JOIN FACETS.dbo.CMC_GRGR_GROUP grgr WITH(NOLOCK)ON grgr.GRGR_CK = clcl.GRGR_CK
LEFT OUTER JOIN [FACETS].[dbo].[CMC_CLHP_HOSP] clhp WITH(NOLOCK)ON clcl.CLCL_ID = clhp.CLCL_ID
LEFT OUTER JOIN [FACETS].[dbo].[CMC_HBCD_BILL_DESC] hbcd WITH(NOLOCK)ON hbcd.HBCD_ID = clhp.CLHP_FAC_TYPE + clhp.CLHP_BILL_CLASS
WHERE clcl.CLCL_CUR_STS IN ('11', '15')

I'm trying to convert it to C# with the following code:

 ppoClaims = (from clcl in _context.CMC_CLCL_CLAIM
                 join cled in _context.CMC_CLED_EDI_DATA on clcl.CLCL_ID equals cled.CLCL_ID
                 join prpr in _context.CMC_PRPR_PROV on clcl.PRPR_ID equals prpr.PRPR_ID
                 join sbsb in _context.CMC_SBSB_SUBSC on clcl.SBSB_CK equals sbsb.SBSB_CK
                 join sbad in _context.CMC_SBAD_ADDR on clcl.SBSB_CK equals sbad.SBSB_CK
                 join grgr in _context.CMC_GRGR_GROUP on clcl.GRGR_CK equals grgr.GRGR_CK
                 join clhp in _context.CMC_CLHP_HOSP on clcl.CLCL_ID equals clhp.CLCL_ID into SUBclhp
                 from z in SUBclhp.DefaultIfEmpty()
                 join hbcd in _context.CMC_HBCD_BILL_DESC on z.CLHP_FAC_TYPE + z.CLHP_BILL_CLASS equals hbcd.HBCD_ID
                 where sbsb.SBAD_TYPE_HOME == sbad.SBAD_TYPE
                 && staticVars.CLCL_CUR_STS.Contains(clcl.CLCL_CUR_STS)

However it only return 48,930 records, so clearly I'm doing something wrong. I think my problem could lie in one of two spots. Either the

AND sbsb.SBAD_TYPE_HOME = sbad.SBAD_TYPE

being put in my where clause in the C#. Or my attempt at the left outer joins here

join clhp in _context.CMC_CLHP_HOSP on clcl.CLCL_ID equals clhp.CLCL_ID into SUBclhp
from z in SUBclhp.DefaultIfEmpty()
join hbcd in _context.CMC_HBCD_BILL_DESC on z.CLHP_FAC_TYPE + z.CLHP_BILL_CLASS equals hbcd.HBCD_ID

Or, maybe it's a combination of both? I feel like I'm pretty close, just need one or two small alterations. Any help is much appreciated.

Edit: The staticVars.CLCL_CUR_STS in the C# is an array that contains "11" and "15"

1 Answers

Firstly, EF is an ORM not merely a replacement for ADO. You map entities to tables, but more importantly, you map the relations between these entities so that EF can work out the joins for you automatically. The fact that EF's Linq queries have a join operation is merely for exceptional scenarios. The goal with EF isn't to just give you a different way to write SQL queries, it is to avoid writing SQL-like queries all-together.

To that end, each entity that contains relations to other entities should contain references and collections for the various many-to-one or one-to-many relationships, or many-to-many or one-to-one relationships respectively.

Your job with EF starts with mapping out these relationships and creating the appropriate navigation properties. This also gives you the opportunity to give your entities a more friendly, readable name where the database table conforms to legacy naming conventions/requirements. For example, looking briefly at your schema:

[Table("CMC_CLCL_CLAIM")]
public class Claim
{
   [Key, Column("CLCL_ID")]
   public int Id { get; set; }
   
   [Column("CLCL_CUR_STS")]
   public string CurrentStatus { get; set; }

   // ... other columns ...

   public virtual ICollection<Group> Groups { get; set; }

   // ... and other references, collections, etc.
}

[Table("CMC_GRGR_Group")]
public class Group
{
    [Key, Column("GRGR_ID")]
    public int Id { get; set; }

    //... Other group fields...

    [ForeignKey("Claim"), Column("CLCL_ID")]
    public int ClaimId { get; set; }

    public virtual Claim Claim { get; set; }

    // Probaby a FK to another table?
    public int CiCiId { get; set; }
}

EF can automatically work out many relationships, but there can be cases where it helps to do it a bit more explicitly with attributes and/or EntityTypeConfiguration / DbContext.OnModelCreating which can help streamline entities to avoid bi-directional references if not needed, or declaring FK properties. (That's a bit more advanced material to research, but for a start focus on setting up the relations in the first place)

Once these navigation properties are set up, then you build your Linq expressions around the entities and let EF build your SQL. From that you can compare the results and determine if all of the relationships are working properly.

For instance you can load the entity and its related data, or project the data from this entity graph (the entity and related entities) into a desired view model or anonymous type.

So for instance in your example query, it doesn't really make sense to be joining all of those tables when the SELECT is only pulling data from the CLCL Claim and one column from the GRGR Group which joins off the Claim, none of the other tables.

The query would look more like:

var data = _context.Claims
    .Where(x => staticVars.ActiveStatuses.Contains(x.CurrentStatus))
    .SelectMany(x => x.Groups.Select( g => new 
        {
            x.Reason,
            x.LowServiceDate,
            x.MedianAge,
            g.CiCiId,
            x.NwnwId
         }).ToList();

Based on the joins you want a list of these GroupIds with the relevant details from the claim where one claim can have multiple Groups. Again the entity property names are guesses at whatever the original schema fields mean. It's more for demonstration that you can use more meaningful names in your entities to make your code easier to understand rather than propagating archaic naming conventions.

Ultimately the Joins in the original tables can possibly be resulting in further Cartesian products. If the goal in the original query with the unused INNER JOINS was just to enforce limiting data for Null-able FKs to return data where those FKs are populated (rather than using non-nullable FKs) then the Where clause above can be updated to AND (&&) in conditions where the associated related entities are != null.

In any case, when working with EF, it is better to set up the relationships between the entities, then approach the requirements as "I want to return this data from the entity graph in this format with these filters etc." rather than "I want to reproduce this SQL query".

If instead you just want to port SQL into C# then I'd recommend just writing Stored Procedures or Views and mapping read-only entities to those outputs rather than trying to map tables and trying to build Linq versions of those queries.

Related