Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Menggunakan Ekspresi untuk Memfilter Data Database

Saya ingin memulai dengan deskripsi masalah yang saya temui. Ada entitas dalam database yang perlu ditampilkan sebagai tabel di UI. Entity Framework digunakan untuk mengakses database. Ada filter untuk kolom tabel ini.

Anda perlu menulis kode untuk memfilter entitas berdasarkan parameter.

Misalnya, ada dua entitas:Pengguna dan Produk.

Pengguna kelas publik{ public int Id { get; mengatur; } Nama string publik { dapatkan; mengatur; }}Produk kelas publik{ public int Id { dapatkan; mengatur; } Nama string publik { dapatkan; mengatur; }}

Asumsikan kita perlu memfilter pengguna dan produk berdasarkan nama. Kami membuat metode untuk memfilter setiap entitas.

public IQueryable FilterUsersByName(IQueryable users, string text){ pengguna kembali.Where(user => user.Name.Contains(text));}public IQueryable FilterProductsByName(IQueryable produk, teks string){ mengembalikan produk.Di mana(produk => produk.Nama.Berisi(teks));}

Seperti yang Anda lihat, kedua metode ini hampir identik dan hanya berbeda dalam properti entitas, yang digunakan untuk memfilter data.

Mungkin menjadi tantangan jika kita memiliki lusinan entitas dengan lusinan bidang yang memerlukan pemfilteran. Kompleksitas terletak pada dukungan kode, penyalinan tanpa berpikir, dan akibatnya, pengembangan lambat dan kemungkinan kesalahan tinggi.

Mengutip Fowler, itu mulai berbau. Saya ingin menulis sesuatu yang standar daripada duplikasi kode. Misalnya:

public IQueryable FilterUsersByName(IQueryable users, string text){ return FilterContainsText(users, user => user.Name, text);}public IQueryable FilterProductsByName(IQueryable produk, string text){ return FilterContainsText(products, propduct => propduct.Name, text);}public IQueryable FilterContainsText(entity IQueryable, Func getProperty, string text){ mengembalikan entitas. Where(entity => getProperty(entity).Contains(text));}

Sayangnya, jika kami mencoba memfilter:

public void TestFilter(){ using (var context =new Context()) { var filteredProducts =FilterProductsByName(context.Products, "name").ToArray(); }}

Kami akan mendapatkan kesalahan «Test method ExpressionTests.ExpressionTest.TestFilter melemparkan pengecualian:
System.NotSupportedException :Jenis simpul ekspresi LINQ 'Invoke' tidak didukung di LINQ ke Entitas.

Ekspresi

Mari kita periksa apa yang salah.

Metode Where menerima parameter tipe Expression>. Jadi, Linq bekerja dengan pohon ekspresi, yang digunakan untuk membangun kueri SQL, bukan dengan delegasi.

Ekspresi menjelaskan pohon sintaks. Untuk lebih memahami bagaimana strukturnya, pertimbangkan ekspresi, yang memeriksa bahwa nama sama dengan baris.

Ekspresi> yang diharapkan =produk => produk.Nama =="target";

Saat debugging, kita dapat melihat struktur ekspresi ini (properti kunci ditandai dengan warna merah).

Kami memiliki pohon berikut:

Saat kita meneruskan delegasi sebagai parameter, pohon berbeda akan dibuat, yang memanggil metode Invoke pada parameter (delegasi) alih-alih memanggil properti entitas.

Saat Linq mencoba membuat kueri SQL dengan pohon ini, Linq tidak tahu cara menginterpretasikan metode Invoke dan melempar NotSupportedException.

Jadi, tugas kita adalah mengganti cast ke properti entitas (bagian pohon yang ditandai dengan warna merah) dengan ekspresi yang diteruskan melalui parameter ini.

Mari kita coba:

Expression> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter(product) =="target"

Sekarang, kita dapat melihat kesalahan «Nama metode yang diharapkan» pada tahap kompilasi.

Masalahnya adalah bahwa ekspresi adalah kelas yang mewakili node dari pohon sintaks, bukan delegasi dan tidak dapat dipanggil secara langsung. Sekarang, tugas utamanya adalah menemukan cara untuk membuat ekspresi yang meneruskan parameter lain ke dalamnya.

Pengunjung

Setelah pencarian Google singkat, saya menemukan solusi untuk masalah serupa di StackOverflow.

Untuk bekerja dengan ekspresi, ada kelas ExpressionVisitor, yang menggunakan pola Pengunjung. Hal ini dirancang untuk melintasi semua node dari pohon ekspresi dalam urutan parsing pohon sintaks dan memungkinkan memodifikasi mereka atau mengembalikan node lain sebagai gantinya. Jika baik node maupun node anaknya tidak diubah, ekspresi aslinya akan dikembalikan.

Saat mewarisi dari kelas ExpressionVisitor, kita dapat mengganti simpul pohon apa pun dengan ekspresi, yang kita lewati melalui parameter. Jadi, kita perlu meletakkan beberapa label-simpul, yang akan kita ganti dengan parameter, ke dalam pohon. Untuk melakukannya, tulis metode ekstensi yang akan mensimulasikan panggilan ekspresi dan akan menjadi penanda.

public static class ExpressionExtension{ public static TFunc Call(ekspresi ini) { throw new InvalidOperationException("Metode ini tidak boleh dipanggil. Ini adalah penanda untuk mengganti."); }}

Sekarang, kita dapat mengganti satu ekspresi dengan yang lain

Expression> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter.Call()(product) =="target"; 

Pengunjung perlu ditulis, yang akan menggantikan metode Panggilan dengan parameternya di pohon ekspresi:

kelas publik SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition(); } protected override Expression VisitMethodCall(MethodCallExpression node) { if (IsMarker(node)) { kembali Visit(ExtractExpression(node)); } kembalikan basis.VisitMethodCall(simpul); } private LambdaExpression ExtractExpression(MethodCallExpression node) { var target =node.Arguments[0]; kembali (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { kembali node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Kami dapat mengganti penanda kami:

ekspresi statis publik SubstituteMarker(ekspresi ini ekspresi){ var visitor =new SubstituteExpressionCallVisitor(); return (Expression)visitor.Visit(expression);}Ekspresi> propertyGetter =product => product.Name;Expression> filter =product => propertyGetter.Call ()(produk).Contains("123");Ekspresi> finalFilter =filter.SubstituteMarker();

Dalam debugging, kita dapat melihat bahwa ekspresi tidak seperti yang kita harapkan. Filter masih berisi metode Invoke.

Faktanya adalah bahwa ekspresi parameterGetter dan finalFilter menggunakan dua argumen yang berbeda. Jadi, kita perlu mengganti argumen di parameterGetter dengan argumen di finalFilter. Untuk melakukan ini, kami membuat pengunjung lain:

Hasilnya adalah sebagai berikut:

kelas publik SubstituteParameterVisitor :ExpressionVisitor{ private readonly LambdaExpression _expressionToVisit; Kamus hanya-baca pribadi _substitutionByParameter; public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit =expressionToVisit; _substitutionByParameter =expressionToVisit .Parameters .Select((parameter, index) => new {Parameter =parameter, Index =index}) .ToDictionary(pair => pair.Parameter, pair => parameterSubstitutions[pair.Index]); } public Expression Replace() { kembali Visit(_expressionToVisit.Body); } protected override Expression VisitParameter(ParameterExpression node) { Penggantian ekspresi; if (_substitutionByParameter.TryGetValue(simpul, substitusi keluar)) { kunjungan kembali(substitusi); } kembalikan basis.VisitParameter(simpul); }}kelas publik SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtensions) .GetMethod(nameof(ExpressionExtensions.Call)) .GetGenericMethodDefinition(); } protected override Expression VisitInvocation(InvocationExpression node) { var isMarkerCall =node.Expression.NodeType ==ExpressionType.Call &&IsMarker((MethodCallExpression) node.Expression); if (isMarkerCall) { var parameterReplacer =new SubstituteParameterVisitor(node.Arguments.ToArray(), Unwrap((MethodCallExpression) node.Expression))); var target =parameterReplacer.Replace(); Kunjungan kembali (target); } kembalikan base.VisitInvocation(simpul); } private LambdaExpression Unwrap(MethodCallExpression node) { var target =node.Arguments[0]; kembali (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { kembali node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}

Sekarang, semuanya berfungsi sebagaimana mestinya dan kami, akhirnya, dapat menulis metode penyaringan kami

public IQueryable FilterContainsText(IQueryable entitas, Ekspresi> getProperty, teks string){ Expression> filter =entitas => getProperty. Panggilan()(entitas).Berisi(teks); kembalikan entitas.Where(filter.SubstituteMarker());}

Kesimpulan

Pendekatan dengan penggantian ekspresi dapat digunakan tidak hanya untuk pemfilteran tetapi juga untuk pengurutan dan kueri apa pun ke database.

Selain itu, metode ini memungkinkan penyimpanan ekspresi bersama dengan logika bisnis secara terpisah dari kueri ke database.

Anda dapat melihat kodenya di GitHub.

Artikel ini didasarkan pada balasan StackOverflow.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Fragmentasi Indeks Clustered yang Tak Terduga

  2. Cara Memeriksa apakah UDF T-SQL Terikat Skema (Bahkan Saat Dienkripsi)

  3. Tindak lanjut #1 pada pencarian wildcard terkemuka

  4. Bekerja dengan Data Java di Sisense

  5. Menerapkan Pencadangan dan Pemulihan Basis Data Otomatis dengan Cara Default