【问题标题】:C# EF array intersect in LINQ to DBC# EF 数组在 LINQ to DB 中相交
【发布时间】:2020-07-08 20:02:09
【问题描述】:

我有一个代码优先应用程序和一个表“通知”,其中“标签”列将标签存储在一个字符串中,用“;”分隔。在上下文中,我转换为 IEnumerable,反之亦然。插入和获取数据时一切正常,但在一项服务中,我动态构建过滤器,通过添加谓词,一一添加,并添加最终谓词列表进行查询。 现在,我有一种情况,我想按标签过滤,例如,我想要所有带有标签“Tag1”和“Tag2”的通知。我尝试使用 Contains 和 Intersect,但由于无法翻译 LINQ 表达式,我经常遇到异常。有任何想法吗? 谢谢。

上下文:

    builder.Entity<Notification>().Property(x => x.Tags).HasConversion
                    (x => string.Join(';', x),
                    x => x.Split(';', StringSplitOptions.RemoveEmptyEntries)
);

服务:

var filter = PredicateBuilder.True<UserNotification>();
IEnumerable<string> tagsFilter = new List<string>() { "Tag1","Tag2" };
filter = filter.And(x => x.Notification.Tags != null); // this line works

// both these lines fail (they are here as alternatives, should give the same result)
filter = filter.And(x => x.Notification.Tags.Any(r => tagsFilter.Contains(r)));
filter = filter.And(x => x.Notification.Tags.Intersect(tagsFilter).Any());

错误是(在“Where”子句中):

System.InvalidOperationException: The LINQ expression 'DbSet<UserNotification>
    .Join(
        outer: DbSet<Notification>, 
        inner: u => EF.Property<Nullable<long>>(u, "NotificationId"), 
        outerKeySelector: n => EF.Property<Nullable<long>>(n, "Id"), 
        innerKeySelector: (o, i) => new TransparentIdentifier<UserNotification, Notification>(
            Outer = o, 
            Inner = i
        ))
    .Where(u => True && __statuses_0
        .Contains(u.Outer.NotificationStatus) && __types_1
        .Contains(u.Inner.Type) && u.Inner.Tags != null && u.Inner.Tags
        .Any(r => __tags2_2.Contains(r)))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

【问题讨论】:

  • 您必须在客户端的标签上应用过滤器。
  • 您确定在通知表中将标签连接成一个字符串是一个好的设计吗?拥有一个带有标签的单独表格,并在通知和标签之间建立多对多关系,不是更好,而且可能更容易吗?每个通知都有零个或多个标签,每个标签都被零个或多个通知使用。这样更容易更改标签:例如重命名文本,或声明它们已过时。这是代码优先,所以现在您仍然可以决定创建一个适当规范化的数据库结构。
  • 嗨,谢谢大家的 cmets,@David - 我能以某种方式强制查询在客户端执行,而不会将所有未过滤的数据拉到内存中吗?

标签: c# entity-framework linq entity-framework-core


【解决方案1】:

由于您的工作是代码优先,我的建议是将您的标签放在一个单独的表中,并在通知和标签之间建立多对多的关系。这将为您现在节省大量工作,并且将来如果您有一个已填满的大型数据库并且需要进行更改:

public class Notification
{
    public int Id {get; set;}
    ... // other properties

    // every Notification has zero or more Tags (many-to-many)
    public virtual ICollection<Tag> Tags {get; set;}
}

public class Tag
{
    public int Id {get; set;}
    public string Text {get; set; }
    ... // you can add other properties later

    // every Tag is used by zero or more Notifications (many-to-many)
    public virtual ICollection<Notification> Notification {get; set;}
}

因为我使用实体框架代码优先约定,实体框架识别通知和标签之间的多对多关系。不需要流畅的 API 和属性。

要求:给定几个标签的文本,给我所有这些标签的通知:

IEnumerable<string> tagTexts = ...
var notifications = dbContext.Tags.Where(tag => tagTexts.Contains(tag.Text)
    .SelectMany(tag => tag.Notifications)
    .Distinct();

要求删除所有只有带有文本“SQL”标签的通知

string tagText = "SQL";
var notificationsWithOnlyTagSQL = dbContext.Notifications
    .Where(notification => notification.Tags.All(tag => tag.TagText == tagText)
    .ToList();
dbContext.Notifications.RemoveRange(notificationsWithTagSQL);

要求确保所有带有“sql”、“SQL”、“Sql”等的标签都使用相同的文本:“SQL”(假设您有一个不区分大小写的数据库)

const string proposedTagText = "SQL";
var tagsToChange = dbContext.Tags.Where(tag => tag.TagText == proposedTagText).ToList();

foreach (var tag in tagsToChange)
{
    tag.TagText = proposedTagText;
}
dbContext.SaveChanges();

看看如果你有一个单独的标签表,它会变得多么简单!想想如果您必须检查每个通知的字符串,这将是多少工作!

哦,天哪,既然我们已经将标签“sql”更改为“SQL”,我们就有了几个具有相同 TagText 的标签。确保只剩下一个:

var tagsSql = dbContext.Tags
    .Where(tag => tag.TagText == proposedTagText)
    .ToList();
var tagToKeep = tagsSql.FirstOrDefault();
var tagsToRemove = tagsSql.Skip(1).ToList();

var notificationsToChange = dbContext.Tags
    .Where(tag => tagIdsToRemove.Contains(tag))
    .SelectMany(tag => tag.Notifications)
    .Distinct();

foreach (var notification in notificationsToChange)
{
    // remove all tagsToRemove from this notification
    notification.Tags.RemoveRange(tagsToRemove);

    // if this notification does not have tagToKeep, add it:
    if (!notification.Contains(tagToKeep))
    {
        notification.Add(tagToKeep);
    }
}

// now that no one uses TagsToRemove anymore, we can remove the tags:
dbContext.Tags.RemoveRange(tagsToRemove);
dbContext.SaveChanges();

在您的连接字符串标记方法中甚至不可能:

数据库迁移后Tag增加了一个布尔属性:IsObsolete,初始设置为false。

要求给我所有有过时标签的通知:

var notificationsWithObsoleteTags= dbContext.Tags
    .Where(tag => tag.IsObsolete)
    .SelectMany(tag => tag.Notifications);

要求:从通知中删除所有过时的标签

var obsoleteTags = dbContext.Tags.Where(tag => tag.IsObsolete).ToList();
dbContext.RemoveRange(obsoleteTags);
dbContext.SaveChanges();

再次:考虑一下如果您没有单独的表格,您会做多少工作

【讨论】:

    【解决方案2】:

    您不能在服务器端使用 Contains() 或 Intersect(),因为 LINQ 无法在 SQL 中从字符串转换为 IEnumerable。 改为使用按字符串字段过滤:

        var filter = PredicateBuilder.True<UserNotification>();
        IEnumerable<string> tagsFilter = new List<string>() { "Tag1","Tag2" };
        filter = filter.And(x => x.Notification.Tags != null); // this line works
        
        foreach (var tag in tagsFilter) {
            // search for tag with heading and trailing ',' to distinct tags 'ham' and 'hamburger'
            filter = filter.And(x => ("," + x.Notification.Tags + ",").Contains("," + tag + ","));
        }
    

    【讨论】:

    • 您好,注意这里的.Tags属性不是字符串,而是字符串[],所以我有两个枚举要相交。
    猜你喜欢
    • 2013-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多