Entity Framework, LINQ: ไม่สามารถใช้ Any กับ StartsWith?
ฉันมีแบบสอบถาม Entity Framework LINQ ง่ายๆ เป้าหมายคือค้นหาผู้ให้บริการทั้งหมดที่ขึ้นต้นด้วย A, B หรือ C:
var letters = new List<string>() { "A", "B", "C" }; // Dynamic, can be many
var results = db.Carriers.AsNoTracking()
.Where(c => letters.Any(val => c.Name.StartsWith(val)))
.ToList();
ฉันเข้าใจ
System.InvalidOperationException: 'The LINQ expression' DbSet .Where (c => __letters_0 .Any (val => val == "" || c.Name! = null && val! = null && c.Name.StartsWith (val) )) 'ไม่สามารถแปลได้ เขียนแบบสอบถามใหม่ในรูปแบบที่สามารถแปลได้หรือเปลี่ยนไปใช้การประเมินผลไคลเอ็นต์อย่างชัดเจนโดยการแทรกการเรียกไปยัง AsEnumerable (), AsAsyncEnumerable (), ToList () หรือ ToListAsync () ดูhttps://go.microsoft.com/fwlink/?linkid=2101038 สำหรับข้อมูลเพิ่มเติม.'
ไม่มีทางทำได้หรือ?
คำตอบ
ใน Entity Framework 6 แบบสอบถามนี้จะทำงานได้ดี EF6 รองรับการแปล SQL StartsWithและรองรับการใช้ลำดับโลคัล ( letters) ในนิพจน์แบบสอบถาม
EF core 3 (เวอร์ชันในคำถามตามที่ระบุข้อความยกเว้น) ยังรองรับการแปล SQL ของStartsWith. ปัญหาที่นี่ (ซึ่งคำตอบอื่นขาดไปโดยสิ้นเชิง) คือลำดับโลคัลถูกใช้ในลักษณะที่ไม่รองรับ แบบสอบถามเช่น ...
var results = db.Carriers.AsNoTracking()
.Where(c => letters.Contains(c.Name))
.ToList();
... จะได้รับการสนับสนุนเพราะlettersสามารถแปลเป็นINอนุประโยคได้ แต่แน่นอนว่าเป็นแบบสอบถามที่แตกต่างกันอย่างสิ้นเชิง
การletters.Anyใช้ EF ต้องแปลงlettersเป็น "บางสิ่ง" ที่สามารถเข้าร่วมกับ SQL ได้ EF6 ทำได้โดยสร้างชุดผลลัพธ์ในแบบสอบถาม SQL:
WHERE EXISTS (SELECT
1 AS [C1]
FROM (SELECT
N'A' AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
UNION ALL
SELECT
N'B' AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable2]
UNION ALL
SELECT
N'C' AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable3]) AS [UnionAll2]
WHERE ( CAST(CHARINDEX([UnionAll2].[C1], [Extent1].[Name]) AS int)) = 1
ซึ่งใช้งานได้ แต่ไม่สามารถปรับขนาดได้เลย EF core 3 ไม่รองรับและไม่มีวิธีแก้ไขง่ายๆตามที่แนะนำในคำตอบอื่น ๆ
วิธีแก้ไขที่เป็นไปได้คือการสร้างเพรดิเคตด้วยเพรดิเคต||(Or) เพรดิเคตตัวอย่างเช่น:
var pred = letters.Aggregate(PredicateBuilder.False<Carrier>(),
(p,x) => p = p.Or(c => c.Name.StartsWith(x)));
var results = db.Carriers.AsNoTracking()
.Where(pred)
.ToList();
ในกรณีที่PredicateBuilderเป็นผู้สร้างคำกริยาเช่น Linqkit หรืออย่างใดอย่างหนึ่ง แต่วิธีนี้ไม่สามารถปรับขนาดได้เช่นกัน EF สร้างพารามิเตอร์สำหรับแต่ละรายการในlettersดังนั้นคุณอาจถึงเกณฑ์ 2100 พารามิเตอร์ใน Sql Server
Entity Framework ไม่อนุญาตให้ใช้เพรดิเคตที่กำหนดเอง นั่นเป็นสาเหตุที่ทำให้ข้อความค้นหาของคุณล้มเหลว:
... ไม่สามารถแปลได้ เขียนแบบสอบถามใหม่ในรูปแบบที่สามารถแปลได้ ...
ดังนั้นนี่อาจหมายความว่าเวอร์ชัน EF ของคุณไม่รองรับStartsWithหรือมีชุดค่าผสมบางอย่างเช่นStartsWithและAnyไม่สามารถแปลได้
วิธีแก้ปัญหาอาจเป็น:
- ขั้นแรกคุณอาจกรองแบบสอบถามของคุณด้วยวิธีที่ใกล้เคียงที่สุดโดยใช้Containsวิธีการ
var results = students.Where(s => letters.Any(l => s.name.Contains(l)));
นี่คือตัวอย่างที่คล้ายกันที่ฉันเขียนสำหรับซอ. NET
- ต่อไปในกรณีที่คุณยังต้องการใช้
StartsWithแต่ EF ของคุณไม่รองรับคุณสามารถแปลงแบบสอบถามของคุณเป็นคอลเล็กชัน IN-MEMORY:
var results = filter_by_contains_query
.ToList() // convert filtered result to in-memory collection
.Where(i => letters.Any(l => i.StudentName.StartsWith(l)));
ที่filter_by_contains_queryเป็นแบบสอบถามจากส่วนที่ 1
มากกว่า:
- เกี่ยวกับ EF สนับสนุนวิธีการตรวจสอบที่นี่
- การสนทนารอบ
StartsWith,EndsWithและContainsที่นี่