简体   繁体   中英

How to get Inner Join using navigation property in Entity Framework Core

I want to generate a query that uses an inner join for a one to one relationship using a navigation property. This is for an existing database that I cannot change.

Here are what my entities look like:

[Table("Info")]
public class Info
{
    [Key]
    public int InfoId { get; set; }
    public string Name { get; set; }   
    public virtual MoreInfo MoreInfo { get; set; }
}

[Table("MoreInfo")]
public class MoreInfo
{
    [Key]
    public int InfoId { get; set; }
    public string OtherName { get; set; }
    public int OtherNumber { get; set; }
}

and here is my query:

var x = await context.Infos
    .Select(info => new
    {
        info.InfoId,
        info.Name,
        info.MoreInfo.OtherName,
        info.MoreInfo.OtherNumber
    })
    .ToListAsync();

this produces this SQL (which uses a left join):

SELECT [info].[InfoId], [info].[Name], [info.MoreInfo].[OtherName], [info.MoreInfo].[OtherNumber]
FROM [Info] AS [info]
LEFT JOIN [MoreInfo] AS [info.MoreInfo] ON [info].[InfoId] = [info.MoreInfo].[InfoId]

This will cause an error if there are records in Info that are not in MoreInfo, which is why I want to do an inner join.

I have tried various option in OnModelCreating such as

modelBuilder.Entity<Info>()
    .HasOne(p => p.MoreInfo)
    .WithOne()
    .IsRequired();

or setting the navigation property as Required

[Table("Info")]
public class Info
{
    [Key]
    public int InfoId { get; set; }
    public string Name { get; set; }
    [Required]
    public virtual MoreInfo MoreInfo { get; set; }
}

or using an include in the query

var x = await context.Infos
    .Include(info => info.MoreInfo)
    .Select(info => new
    {
        info.InfoId,
        info.Name,
        info.MoreInfo.OtherName,
        info.MoreInfo.OtherNumber
    })
    .ToListAsync();

but none of those changed the join.

I am able to get an inner join by using the join function, and not using the navigation property, but I would like to avoid this if possible. The syntax is ugly and requires me to add MoreInfos to the context. If it is possible, I would like to create an inner join using a navigation property.

var x = await context.Infos
    .Join(context.MoreInfos, info => info.InfoId, moreInfo => moreInfo.InfoId, (info, moreInfo) =>
        new
        {
            info.InfoId,
            info.Name,
            moreInfo.OtherName,
            moreInfo.OtherNumber
        })
    .ToListAsync();

produces the correct SQL

SELECT [info].[InfoId], [info].[Name], [moreInfo].[OtherName], [moreInfo].[OtherNumber]
FROM [Info] AS [info]
INNER JOIN [MoreInfo] AS [moreInfo] ON [info].[InfoId] = [moreInfo].[InfoId]

but I'd rather uses the navigation property.

Currently (EF Core 2.1.2) it's not possible with the provided model.

The true one to one relationship technically cannot be enforced, so EF Core IsRequired denotes whether the dependent entity FK is nullable or not. Which for the shared PK association like yours is always true. Note that the Info is the principal while the MoreInfo is the dependent, hence MoreInfo requires Info , bit not vice versa, that's why EF Core generates left outer join when navigating from principal to dependent.

One thing you can do is to include not null criteria:

.Where(info => info.MoreInfo != null)

which when used in EF6 query (with some intermediate projection) was able to let the EF6 query translator use inner join. EF Core is still not smart enough, so all these tricks don't work. It generates left outer join with right side key IS NOT NULL filter. Which is equivalent of a inner join and probably will be processed as such by the SQL query optimizer. But still...

The only way to get inner join currently (except manual join of course) is to navigate from the other end of the relationship, ie from dependent to required principal. There is no need to expose DbSet<MoreInfo> from your context, the Set<TEntity>() method can be used to access any entity set. You'd need an inverse navigation property though:

public class MoreInfo
{
    // ...
    public virtual Info Info { get; set; }
}

and of course updated fluent configuration:

modelBuilder.Entity<Info>()
    .HasOne(e => e.MoreInfo)
    .WithOne(e => e.Info)
    .HasForeignKey<MoreInfo>(e => e.InfoId);

Now the query can be rewritten as:

var x = await context.Set<MoreInfo>()
    .Select(moreInfo => new
    {
        moreInfo.Info.InfoId,
        moreInfo.Info.Name,
        moreInfo.OtherName,
        moreInfo.OtherNumber
    })
    .ToListAsync();

which will generate the desired inner join:

SELECT [moreInfo].[InfoId], [moreInfo.Info].[Name], [moreInfo].[OtherName], [moreInfo].[OtherNumber]
FROM [MoreInfo] AS [moreInfo]
INNER JOIN [Info] AS [moreInfo.Info] ON [moreInfo].[InfoId] = [moreInfo.Info].[InfoId]

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