EF Core 3.1中如何通过值转换属性查询User实体的ExternalIds?
你的问题很典型——EF Core的值转换器虽然能帮你把int[]转成逗号分隔字符串存到数据库,但查询时EF Core没法自动把int[]的Contains操作翻译成对应的SQL逻辑,这就是为什么你看到生成的SQL是WHERE CAST(1 AS bit) = CAST(0 AS bit)(恒假条件,自然返回空)。下面给你几个可行的解决方案,适配你的需求:
方案1:直接操作数据库存储的字符串列(用EF.Property访问)
既然数据库里存的是逗号分隔字符串,我们可以绕过实体类的int[]属性,直接访问数据库列的字符串值,再用EF.Functions.Like做匹配。为了避免部分匹配(比如查1时匹配到11、101),最好给查询值和存储值都加上前后逗号:
var targetId = externalId.ToString(); // 构造带前后逗号的匹配模式,确保匹配完整的ID var searchPattern = $",{targetId},"; var user = context.Users .FirstOrDefault(u => EF.Functions.Like( // 给数据库存储的字符串前后加逗号,统一匹配格式 EF.Functions.Concat(",", EF.Property<string>(u, "ExternalIds"), ","), searchPattern));
这里EF.Property<string>(u, "ExternalIds")是关键,它让EF Core直接引用数据库里的字符串列,而不是使用实体类的int[]属性。
方案2:使用原生SQL查询
如果觉得LINQ写法不够直观,直接用原生SQL是更直接的选择,同样处理边界匹配问题:
var user = context.Users .FromSqlRaw(@" SELECT * FROM Users WHERE ',' + ExternalIds + ',' LIKE '%,' + {0} + ',%'", externalId) .FirstOrDefault();
方案3:改用JSON格式存储数组(更优雅的替代方案)
你提到不喜欢逗号分隔字符串,那可以把数组序列化为JSON字符串存储,这样结构更清晰,查询也更准确(避免部分匹配的风险)。首先修改值转换器:
// 记得引用System.Text.Json protected override void OnModelCreating(ModelBuilder modelBuilder) { var valueComparer = new ValueComparer<int[]>( (c1, c2) => c1.SequenceEqual(c2), c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), c => c.ToArray()); modelBuilder.Entity<User>() .Property(user => user.ExternalIds) .HasConversion( v => JsonSerializer.Serialize(v, null), v => JsonSerializer.Deserialize<int[]>(v, null)) .Metadata.SetValueComparer(valueComparer); }
然后针对JSON存储,用数据库的JSON函数查询(以SQL Server为例,需要SQL Server 2016+):
var user = context.Users .FromSqlRaw(@" SELECT * FROM Users WHERE EXISTS ( SELECT 1 FROM OPENJSON(ExternalIds) WHERE value = {0})", externalId) .FirstOrDefault();
这种方法比逗号分隔字符串更可靠,也更贴近数组的原生结构。
为什么原来的Contains查询失效?
EF Core的值转换器只负责数据读写时的双向转换,但查询翻译阶段,EF Core无法理解u.ExternalIds.Contains(externalId)这个针对int[]的操作,应该对应到数据库里的逗号字符串做什么SQL逻辑。因为它没法自动把数组的Contains转换为字符串的模糊匹配,所以只能生成一个恒假的条件来表示“无法翻译的查询”。
内容的提问来源于stack exchange,提问作者Michael

