-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for Global Query Filters on derived types. #27978
Conversation
@zbarnett Please sign the CLA so we can review the changes |
@AndriySvyryd unfortunately I'm still waiting on our legal dept to approve it. I'll sign as soon as I can. |
@zbarnett The Limitation of the Global Queries that it cannot go into the hierarchy is blocking us from delivering a major Feature to our customers and this PR could might be a solution to it. |
Very cool @zbarnett! This is a very nice feature we have been looking for already for a long time. Hope you can sign the CLA soon 🥇 |
Will this make it to EF7? |
CLA is not signed. We cannot even look at the code unless the CLA is signed. |
Sorry about this taking so long. I just checked with management and we're still waiting on legal to approve it. This is the first time I've made any open source contributions at my current job and wasn't aware the process was so lengthy. Will sign as soon as I can. |
Finally got this approved with my employer and I just signed the CLA. I'll be updating this fork and fixing tests over the next few days. Thank y'all for being patient. |
@zbarnett any ETA? |
Unfortunately had some unexpected things come up which caused this to be put on the back burner for a bit. Still definitely planning to circle back to it, though. |
Now that a CLA has been signed, @ajcvickers can anyone on the EF Core team help @zbarnett with the outstanding fixes to unit tests? |
We decided that we will not accept this PR, sorry. The problem is that this approach doesn't work great for TPT/TPC. Entities are spread around several tables that we would join multiple times. The resulting query is nasty because we are unable to prune unnecessary joins efficiently. Also, this functionality is mostly available in current EF by declaring a filter based on individual subtypes on the root. The PR is a convenient/useful sugar over the base functionality and moreover allows for better encapsulation, but we don't think it's enough value to lock ourselves into a particular design before we actually figure out what to do in case of TPT/TPC. |
@maumar I'm curious how y'all arrived at that conclusion. I've been using these changes in production for about a year in a schema of a little over 500 tables that uses solely TPT inheritance with a max depth of 6 without any problems. It was my understanding that the joins for the the base types were necessary regardless in the case of TPT/TPC inheritance in order to construct a fully populated entity so I didn't think my changes should cause any additional joins. Though, it's very possible I'm misunderstanding something. I will say that I haven't used TPC inheritance at all so I'm not familiar with the implications there. Thanks for taking a look at this. |
Here is the example that illustrates the problem: [ConditionalFact]
public void Query_filters_test()
{
using var ctx = new MyContext();
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
var query = ctx.Entities.OfType<BranchEntity2>().ToList();
}
public class RootEntity
{
public int Id { get; set; }
public string RootName { get; set; }
}
public class BranchEntity1 : RootEntity
{
public string BranchName1 { get; set; }
}
public class BranchEntity2 : RootEntity
{
public string BranchName2 { get; set; }
}
public class LeafEntity11 : BranchEntity1
{
public string LeafName11 { get; set; }
}
public class LeafEntity12 : BranchEntity1
{
public string LeafName12 { get; set; }
}
public class LeafEntity21 : BranchEntity2
{
public string LeafName21 { get; set; }
}
public class LeafEntity22 : BranchEntity2
{
public string LeafName22 { get; set; }
}
public class MyContext : DbContext
{
public DbSet<RootEntity> Entities { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RootEntity>().UseTptMappingStrategy().HasQueryFilter(x => x.RootName != "bad root");
modelBuilder.Entity<BranchEntity1>().HasQueryFilter(x => x.BranchName1 != "bad branch 1");
modelBuilder.Entity<BranchEntity2>().HasQueryFilter(x => x.BranchName2 != "bad branch 2");
modelBuilder.Entity<LeafEntity11>().HasQueryFilter(x => x.LeafName11 != "bad leaf 11");
modelBuilder.Entity<LeafEntity12>().HasQueryFilter(x => x.LeafName12 != "bad leaf 12");
modelBuilder.Entity<LeafEntity21>().HasQueryFilter(x => x.LeafName21 != "bad leaf 21");
modelBuilder.Entity<LeafEntity22>().HasQueryFilter(x => x.LeafName22 != "bad leaf 22");
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=QueryFiltersSample;Trusted_Connection=True;MultipleActiveResultSets=true");
}
} it's a binary tree-like structure, with root entity having two branches and each branch having two leaves. Each entity in hierarchy has it's own query filter defined. Now, when we query using SELECT [e].[Id], [e].[RootName], [b0].[BranchName2], [l1].[LeafName21], [l2].[LeafName22], CASE
WHEN [l2].[Id] IS NOT NULL THEN N'LeafEntity22'
WHEN [l1].[Id] IS NOT NULL THEN N'LeafEntity21'
WHEN [b0].[Id] IS NOT NULL THEN N'BranchEntity2'
END AS [Discriminator]
FROM [Entities] AS [e]
LEFT JOIN [BranchEntity1] AS [b] ON [e].[Id] = [b].[Id]
LEFT JOIN [BranchEntity2] AS [b0] ON [e].[Id] = [b0].[Id]
LEFT JOIN [LeafEntity11] AS [l] ON [e].[Id] = [l].[Id]
LEFT JOIN [LeafEntity12] AS [l0] ON [e].[Id] = [l0].[Id]
LEFT JOIN [LeafEntity21] AS [l1] ON [e].[Id] = [l1].[Id]
LEFT JOIN [LeafEntity22] AS [l2] ON [e].[Id] = [l2].[Id]
WHERE ([e].[RootName] <> N'bad root' OR ([e].[RootName] IS NULL)) AND ((([l0].[Id] IS NULL) AND ([l].[Id] IS NULL) AND ([b].[Id] IS NULL)) OR ((([l0].[Id] IS NOT NULL) OR ([l].[Id] IS NOT NULL) OR ([b].[Id] IS NOT NULL)) AND ([b].[BranchName1] <> N'bad branch 1' OR ([b].[BranchName1] IS NULL)))) AND ((([l2].[Id] IS NULL) AND ([l1].[Id] IS NULL) AND ([b0].[Id] IS NULL)) OR ((([l2].[Id] IS NOT NULL) OR ([l1].[Id] IS NOT NULL) OR ([b0].[Id] IS NOT NULL)) AND ([b0].[BranchName2] <> N'bad branch 2' OR ([b0].[BranchName2] IS NULL)))) AND (([l].[Id] IS NULL) OR (([l].[Id] IS NOT NULL) AND ([l].[LeafName11] <> N'bad leaf 11' OR ([l].[LeafName11] IS NULL)))) AND (([l0].[Id] IS NULL) OR (([l0].[Id] IS NOT NULL) AND ([l0].[LeafName12] <> N'bad leaf 12' OR ([l0].[LeafName12] IS NULL)))) AND (([l1].[Id] IS NULL) OR (([l1].[Id] IS NOT NULL) AND ([l1].[LeafName21] <> N'bad leaf 21' OR ([l1].[LeafName21] IS NULL)))) AND (([l2].[Id] IS NULL) OR (([l2].[Id] IS NOT NULL) AND ([l2].[LeafName22] <> N'bad leaf 22' OR ([l2].[LeafName22] IS NULL)))) AND (([l2].[Id] IS NOT NULL) OR ([l1].[Id] IS NOT NULL) OR ([b0].[Id] IS NOT NULL)) note that join for If we remove query filters on all the derived entities we get this sql: SELECT [e].[Id], [e].[RootName], [b0].[BranchName2], [l1].[LeafName21], [l2].[LeafName22], CASE
WHEN [l2].[Id] IS NOT NULL THEN N'LeafEntity22'
WHEN [l1].[Id] IS NOT NULL THEN N'LeafEntity21'
WHEN [b0].[Id] IS NOT NULL THEN N'BranchEntity2'
END AS [Discriminator]
FROM [Entities] AS [e]
LEFT JOIN [BranchEntity2] AS [b0] ON [e].[Id] = [b0].[Id]
LEFT JOIN [LeafEntity21] AS [l1] ON [e].[Id] = [l1].[Id]
LEFT JOIN [LeafEntity22] AS [l2] ON [e].[Id] = [l2].[Id]
WHERE ([e].[RootName] <> N'bad root' OR ([e].[RootName] IS NULL)) AND (([l2].[Id] IS NOT NULL) OR ([l1].[Id] IS NOT NULL) OR ([b0].[Id] IS NOT NULL)) where joins for Users can write the entire filter predicate on the root entity and will get the same sql as that first one, so it may seem like not a big issue. Our thinking however is that when users themselves write code/query that produces bad sql it's nowhere near as bad as when some EF feature generates bad code for them. |
@maumar thanks for that great example! I see exactly what you mean now and I definitely agree that we don't want EF to generate less than optimal queries. If I reworked this a bit to play nicely with the type pruning could the decision be reconsidered? |
@zbarnett yes, if pruning works we would reconsider - this was the major problem we had with the change and we are not fundamentally opposed to it. Fair warning though, this might be a significant amount of work. One other thing to consider from design stand point - my understanding is that this change expects that each level of the hierarchy filters only itself. This is somewhat different than before, where root was responsible for providing filter for all it's derived types. What should happen when we encounter mix of the two approaches? (i.e root has some filter based on a property in branch, and branch has it's own separate filter). We shouldn't block this (breaking change), but perhaps perform some de-duplication? Or maybe leave it be, because it's the user who wrote bad code, not us. This shouldn't be a big deal, but we will discuss it in the EF team and get back to you with what/if we have a strong opinion here. edit: EF Team decision: in case of mixing - leave as is |
Closing as there are significant changes needed and the code is diverging. |
Global Query Filters can now be added to entities at any level of an inheritance hierarchy, not just the root level. Removed the limitation mentioned here: https://docs.microsoft.com/en-us/ef/core/querying/filters#limitations
Fixes #10259
I implemented this for my employer and am awaiting official permission before I sign the CLA (I have verbal permission but we're waiting on the legal dept.), but wanted to get this PR out there in the meantime to get feedback.
I modified the InheritanceQueryFixtureBase context for the specification tests but only noticed test failures for SQL Server. I'm not sure how to test the other providers. Any guidance would be appreciated.