Saya telah mencari cukup lama untuk solusi yang baik untuk masalah yang disajikan oleh pola Repositori khas (daftar metode yang berkembang untuk kueri khusus, dll .. lihat: http://ayende.com/blog/3955/repository- is-the-new-singleton ).
Saya sangat menyukai gagasan menggunakan kueri Perintah, terutama melalui penggunaan pola Spesifikasi. Namun, masalah saya dengan spesifikasi adalah bahwa ini hanya berkaitan dengan kriteria pilihan sederhana (pada dasarnya, klausa where), dan tidak berurusan dengan masalah kueri lainnya, seperti bergabung, pengelompokan, pemilihan subset atau proyeksi, dll. pada dasarnya, semua rintangan ekstra yang harus dilalui banyak kueri untuk mendapatkan kumpulan data yang benar.
(catatan: Saya menggunakan istilah "perintah" seperti dalam pola Perintah, juga dikenal sebagai objek kueri. Saya tidak berbicara tentang perintah seperti dalam pemisahan perintah / kueri di mana ada perbedaan yang dibuat antara kueri dan perintah (perbarui, hapus, memasukkan))
Jadi saya mencari alternatif yang merangkum seluruh kueri, tetapi masih cukup fleksibel sehingga Anda tidak hanya menukar spaghetti Repositories untuk ledakan kelas perintah.
Saya telah menggunakan, misalnya Linqspecs, dan sementara saya menemukan beberapa nilai dalam dapat menetapkan nama yang bermakna ke kriteria pemilihan, itu tidak cukup. Mungkin saya sedang mencari solusi campuran yang menggabungkan berbagai pendekatan.
Saya mencari solusi yang mungkin telah dikembangkan orang lain untuk mengatasi masalah ini, atau mengatasi masalah yang berbeda tetapi masih memenuhi persyaratan ini. Dalam artikel yang ditautkan, Ayende menyarankan untuk menggunakan konteks nHibernate secara langsung, tetapi saya merasa itu sebagian besar memperumit lapisan bisnis Anda karena sekarang juga harus berisi informasi kueri.
Saya akan menawarkan hadiah untuk ini, segera setelah masa tunggu berlalu. Jadi tolong buat solusi Anda layak, dengan penjelasan yang baik dan saya akan memilih solusi terbaik, dan memberi suara positif kepada runner up.
CATATAN: Saya mencari sesuatu yang berbasis ORM. Tidak harus EF atau nHibernate secara eksplisit, tetapi itu adalah yang paling umum dan paling cocok. Jika dapat dengan mudah diadaptasi ke ORM lain itu akan menjadi bonus. Kompatibel dengan Linq juga akan menyenangkan.
PEMBARUAN: Saya sangat terkejut bahwa tidak banyak saran bagus di sini. Sepertinya orang-orang benar-benar CQRS, atau mereka sepenuhnya berada di kamp Repositori. Sebagian besar aplikasi saya tidak cukup rumit untuk menjamin CQRS (sesuatu dengan sebagian besar pendukung CQRS dengan mudah mengatakan bahwa Anda tidak boleh menggunakannya untuk).
UPDATE: Sepertinya ada sedikit kebingungan di sini. Saya tidak mencari teknologi akses data baru, melainkan antarmuka yang dirancang dengan cukup baik antara bisnis dan data.
Idealnya, yang saya cari adalah semacam persilangan antara objek Query, pola spesifikasi, dan repositori. Seperti yang saya katakan di atas, pola spesifikasi hanya berurusan dengan aspek klausa di mana, dan bukan aspek lain dari kueri, seperti gabungan, sub-pilihan, dll. Repositori menangani seluruh kueri, tetapi lepas kendali setelah beberapa saat . Objek kueri juga menangani seluruh kueri, tetapi saya tidak ingin hanya mengganti repositori dengan ledakan objek kueri.
sumber
Jawaban:
Penafian: Karena belum ada jawaban yang bagus, saya memutuskan untuk memposting bagian dari posting blog hebat yang saya baca beberapa waktu yang lalu, disalin hampir kata demi kata. Anda dapat menemukan postingan blog lengkapnya di sini . Jadi begini:
Kita dapat mendefinisikan dua antarmuka berikut:
public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); }
The
IQuery<TResult>
Menentukan pesan yang mendefinisikan query tertentu dengan data itu kembali menggunakanTResult
jenis generik. Dengan antarmuka yang ditentukan sebelumnya, kita dapat mendefinisikan pesan kueri seperti ini:public class FindUsersBySearchTextQuery : IQuery<User[]> { public string SearchText { get; set; } public bool IncludeInactiveUsers { get; set; } }
Kelas ini mendefinisikan operasi kueri dengan dua parameter, yang akan menghasilkan larik
User
objek. Kelas yang menangani pesan ini dapat didefinisikan sebagai berikut:public class FindUsersBySearchTextQueryHandler : IQueryHandler<FindUsersBySearchTextQuery, User[]> { private readonly NorthwindUnitOfWork db; public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db) { this.db = db; } public User[] Handle(FindUsersBySearchTextQuery query) { return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray(); } }
Sekarang kita dapat membiarkan konsumen bergantung pada
IQueryHandler
antarmuka generik :public class UserController : Controller { IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler; public UserController( IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler) { this.findUsersBySearchTextHandler = findUsersBySearchTextHandler; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; User[] users = this.findUsersBySearchTextHandler.Handle(query); return View(users); } }
Model ini segera memberi kami banyak fleksibilitas, karena sekarang kami dapat memutuskan apa yang akan dimasukkan ke dalam
UserController
. Kami dapat memasukkan implementasi yang sama sekali berbeda, atau implementasi yang membungkus implementasi nyata, tanpa harus melakukan perubahan padaUserController
(dan semua konsumen lain dari antarmuka tersebut).The
IQuery<TResult>
antarmuka memberi kita waktu kompilasi dukungan ketika menentukan atau suntikIQueryHandlers
dalam kode kita. Ketika kita mengubahFindUsersBySearchTextQuery
kembaliUserInfo[]
bukan (dengan menerapkanIQuery<UserInfo[]>
), yangUserController
akan gagal dikompilasi, karena jenis kendala generik padaIQueryHandler<TQuery, TResult>
tidak akan mampu memetakanFindUsersBySearchTextQuery
keUser[]
.Menyuntikkan
IQueryHandler
antarmuka ke konsumen Namun, memiliki beberapa masalah kurang jelas yang masih perlu dibenahi. Jumlah ketergantungan konsumen kita mungkin menjadi terlalu besar dan dapat menyebabkan konstruktor kelebihan injeksi - ketika konstruktor mengambil terlalu banyak argumen. Jumlah kueri yang dijalankan kelas bisa sering berubah, yang akan membutuhkan perubahan konstan ke dalam jumlah argumen konstruktor.Kami dapat memperbaiki masalah karena harus menyuntikkan terlalu banyak
IQueryHandlers
dengan lapisan abstraksi ekstra. Kami membuat mediator yang berada di antara konsumen dan penangan kueri:public interface IQueryProcessor { TResult Process<TResult>(IQuery<TResult> query); }
Ini
IQueryProcessor
adalah antarmuka non-generik dengan satu metode umum. Seperti yang Anda lihat dalam definisi antarmuka,IQueryProcessor
tergantung padaIQuery<TResult>
antarmuka. Hal ini memungkinkan kami untuk memiliki dukungan waktu kompilasi pada konsumen kami yang bergantung padaIQueryProcessor
. Mari tulis ulangUserController
untuk menggunakan yang baruIQueryProcessor
:public class UserController : Controller { private IQueryProcessor queryProcessor; public UserController(IQueryProcessor queryProcessor) { this.queryProcessor = queryProcessor; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; // Note how we omit the generic type argument, // but still have type safety. User[] users = this.queryProcessor.Process(query); return this.View(users); } }
The
UserController
sekarang tergantung padaIQueryProcessor
yang dapat menangani semua pertanyaan kita. TheUserController
'sSearchUsers
metode memanggilIQueryProcessor.Process
metode yang lewat di sebuah objek query diinisialisasi. KarenaFindUsersBySearchTextQuery
mengimplementasikanIQuery<User[]>
antarmuka, kita dapat meneruskannya keExecute<TResult>(IQuery<TResult> query)
metode umum . Berkat inferensi tipe C #, kompilator dapat menentukan tipe generik dan ini membuat kita tidak perlu menyatakan tipe secara eksplisit. Jenis kembalian dariProcess
metode ini juga dikenal.Sekarang menjadi tanggung jawab implementasi
IQueryProcessor
untuk menemukan hakIQueryHandler
. Ini memerlukan beberapa pengetikan dinamis, dan secara opsional menggunakan kerangka kerja Dependency Injection, dan semuanya dapat dilakukan hanya dengan beberapa baris kode:sealed class QueryProcessor : IQueryProcessor { private readonly Container container; public QueryProcessor(Container container) { this.container = container; } [DebuggerStepThrough] public TResult Process<TResult>(IQuery<TResult> query) { var handlerType = typeof(IQueryHandler<,>) .MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = container.GetInstance(handlerType); return handler.Handle((dynamic)query); } }
The
QueryProcessor
kelas membangun sebuah tertentuIQueryHandler<TQuery, TResult>
jenis berdasarkan pada jenis contoh permintaan disediakan. Tipe ini digunakan untuk meminta kelas kontainer yang disediakan untuk mendapatkan sebuah instance dari tipe itu. Sayangnya kita perlu memanggilHandle
metode menggunakan refleksi (dengan menggunakan kata kunci dymamic C # 4.0 dalam kasus ini), karena pada titik ini tidak mungkin untuk mentransmisikan contoh handler, karenaTQuery
argumen generik tidak tersedia pada waktu kompilasi. Namun, kecualiHandle
metode tersebut diubah namanya atau mendapat argumen lain, panggilan ini tidak akan pernah gagal dan jika Anda mau, sangat mudah untuk menulis pengujian unit untuk kelas ini. Menggunakan refleksi akan memberikan sedikit penurunan, tetapi tidak ada yang perlu dikhawatirkan.Untuk menjawab salah satu kekhawatiran Anda:
Konsekuensi dari penggunaan desain ini adalah akan ada banyak kelas kecil dalam sistem, tetapi memiliki banyak kelas kecil / terfokus (dengan nama yang jelas) adalah hal yang baik. Pendekatan ini jelas jauh lebih baik daripada memiliki banyak kelebihan beban dengan parameter berbeda untuk metode yang sama dalam repositori, karena Anda dapat mengelompokkannya dalam satu kelas kueri. Jadi, Anda masih mendapatkan kelas kueri yang jauh lebih sedikit daripada metode dalam repositori.
sumber
TResult
parameter umumIQuery
antarmuka tidak berguna. Namun, dalam tanggapan saya yang diperbarui,TResult
parameter digunakan olehProcess
metodeIQueryProcessor
untuk menyelesaikan padaIQueryHandler
saat runtime.IQueryable
dan memastikan untuk tidak menghitung koleksi, lalu dariQueryHandler
saya baru saja memanggil / merangkai kueri. Ini memberi saya fleksibilitas untuk menguji kueri saya dan merangkainya. Saya memiliki layanan aplikasi di atas sayaQueryHandler
, dan pengontrol saya bertanggung jawab untuk berbicara langsung dengan layanan alih-alih penanganCara saya mengatasinya sebenarnya sederhana dan ORM agnostik. Pandangan saya untuk repositori adalah ini: Tugas repositori adalah menyediakan aplikasi dengan model yang diperlukan untuk konteksnya, jadi aplikasi hanya menanyakan repo untuk apa yang diinginkannya tetapi tidak memberi tahu cara mendapatkannya.
Saya menyediakan metode repositori dengan Kriteria (ya, gaya DDD), yang akan digunakan oleh repo untuk membuat kueri (atau apa pun yang diperlukan - ini mungkin permintaan layanan web). Gabungan dan kelompok imho adalah rincian bagaimana, bukan apa dan kriteria seharusnya hanya menjadi dasar untuk membangun klausa where.
Model = objek akhir atau struktur data yang dibutuhkan oleh aplikasi.
public class MyCriteria { public Guid Id {get;set;} public string Name {get;set;} //etc } public interface Repository { MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria); }
Mungkin Anda dapat menggunakan kriteria ORM (Nhibernate) secara langsung jika Anda menginginkannya. Implementasi repositori harus mengetahui cara menggunakan Kriteria dengan penyimpanan yang mendasari atau DAO.
Saya tidak tahu domain Anda dan persyaratan modelnya, tetapi akan aneh jika cara terbaik adalah aplikasi tersebut membuat kueri itu sendiri. Modelnya berubah begitu banyak sehingga Anda tidak dapat mendefinisikan sesuatu yang stabil?
Solusi ini jelas memerlukan beberapa kode tambahan tetapi tidak memasangkan sisanya ke ORM atau apa pun yang Anda gunakan untuk mengakses penyimpanan. Repositori melakukan tugasnya untuk bertindak sebagai fasad dan IMO bersih dan kode 'terjemahan kriteria' dapat digunakan kembali
sumber
Saya telah melakukan ini, mendukung ini dan membatalkan ini.
Masalah utamanya adalah ini: tidak peduli bagaimana Anda melakukannya, abstraksi yang ditambahkan tidak membuat Anda mandiri. Ini akan bocor menurut definisi. Intinya, Anda menciptakan seluruh lapisan hanya untuk membuat kode Anda terlihat lucu ... tetapi tidak mengurangi pemeliharaan, meningkatkan keterbacaan, atau memberi Anda semua jenis model agnostisisme.
Bagian yang menyenangkan adalah Anda menjawab pertanyaan Anda sendiri sebagai tanggapan atas tanggapan Olivier: "ini pada dasarnya menduplikasi fungsionalitas LINQ tanpa semua manfaat yang Anda peroleh dari LINQ".
Tanyakan pada diri Anda: bagaimana mungkin tidak?
sumber
Anda dapat menggunakan antarmuka yang lancar. Ide dasarnya adalah bahwa metode kelas mengembalikan instance saat ini kelas ini setelah melakukan beberapa tindakan. Ini memungkinkan Anda untuk menyambung panggilan metode.
Dengan membuat hierarki kelas yang sesuai, Anda dapat membuat alur logis dari metode yang dapat diakses.
public class FinalQuery { protected string _table; protected string[] _selectFields; protected string _where; protected string[] _groupBy; protected string _having; protected string[] _orderByDescending; protected string[] _orderBy; protected FinalQuery() { } public override string ToString() { var sb = new StringBuilder("SELECT "); AppendFields(sb, _selectFields); sb.AppendLine(); sb.Append("FROM "); sb.Append("[").Append(_table).AppendLine("]"); if (_where != null) { sb.Append("WHERE").AppendLine(_where); } if (_groupBy != null) { sb.Append("GROUP BY "); AppendFields(sb, _groupBy); sb.AppendLine(); } if (_having != null) { sb.Append("HAVING").AppendLine(_having); } if (_orderBy != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderBy); sb.AppendLine(); } else if (_orderByDescending != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderByDescending); sb.Append(" DESC").AppendLine(); } return sb.ToString(); } private static void AppendFields(StringBuilder sb, string[] fields) { foreach (string field in fields) { sb.Append(field).Append(", "); } sb.Length -= 2; } } public class GroupedQuery : FinalQuery { protected GroupedQuery() { } public GroupedQuery Having(string condition) { if (_groupBy == null) { throw new InvalidOperationException("HAVING clause without GROUP BY clause"); } if (_having == null) { _having = " (" + condition + ")"; } else { _having += " AND (" + condition + ")"; } return this; } public FinalQuery OrderBy(params string[] fields) { _orderBy = fields; return this; } public FinalQuery OrderByDescending(params string[] fields) { _orderByDescending = fields; return this; } } public class Query : GroupedQuery { public Query(string table, params string[] selectFields) { _table = table; _selectFields = selectFields; } public Query Where(string condition) { if (_where == null) { _where = " (" + condition + ")"; } else { _where += " AND (" + condition + ")"; } return this; } public GroupedQuery GroupBy(params string[] fields) { _groupBy = fields; return this; } }
Anda akan menyebutnya seperti ini
string query = new Query("myTable", "name", "SUM(amount) AS total") .Where("name LIKE 'A%'") .GroupBy("name") .Having("COUNT(*) > 2") .OrderBy("name") .ToString();
Anda hanya dapat membuat contoh baru dari
Query
. Kelas-kelas lain memiliki konstruktor yang dilindungi. Inti dari hierarki ini adalah untuk "menonaktifkan" metode. Misalnya, fileGroupBy
metode tersebut mengembalikanGroupedQuery
yang merupakan kelas dasarQuery
dan tidak memilikiWhere
metode (metode tempat dideklarasikanQuery
). Oleh karena itu tidak mungkin untuk meneleponWhere
setelahnyaGroupBy
.Namun itu tidak sempurna. Dengan hierarki kelas ini, Anda dapat menyembunyikan anggota secara berturut-turut, tetapi tidak menampilkan yang baru. Oleh karena itu
Having
melontarkan pengecualian saat dipanggil sebelumnyaGroupBy
.Perhatikan bahwa panggilan dapat dilakukan
Where
beberapa kali. Ini menambahkan kondisi baru dengan kondisiAND
yang ada. Hal ini mempermudah pembuatan filter secara terprogram dari satu kondisi. Hal yang sama dimungkinkan denganHaving
.Metode yang menerima daftar bidang memiliki parameter
params string[] fields
. Ini memungkinkan Anda untuk melewatkan nama bidang tunggal atau larik string.Antarmuka yang lancar sangat fleksibel dan tidak mengharuskan Anda membuat banyak metode berlebih dengan kombinasi parameter yang berbeda. Contoh saya bekerja dengan string, namun pendekatannya dapat diperluas ke jenis lain. Anda juga bisa mendeklarasikan metode yang telah ditentukan untuk kasus khusus atau metode yang menerima tipe kustom. Anda juga bisa menambahkan metode seperti
ExecuteReader
atauExceuteScalar<T>
. Ini akan memungkinkan Anda untuk menentukan kueri seperti inivar reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true }) .Where(new CurrentMonthCondition()) .Where(new DivisionCondition{ DivisionType = DivisionType.Production}) .OrderBy(new StandardMonthlyReportSorting()) .ExecuteReader();
Bahkan perintah SQL yang dibangun dengan cara ini dapat memiliki parameter perintah dan dengan demikian menghindari masalah injeksi SQL dan pada saat yang sama memungkinkan perintah untuk di-cache oleh server database. Ini bukan pengganti untuk O / R-mapper tetapi dapat membantu dalam situasi di mana Anda akan membuat perintah menggunakan penggabungan string sederhana sebaliknya.
sumber