Các lệnh và / hoặc thông số kỹ thuật truy vấn được thiết kế tốt


90

Tôi đã khá lâu tìm kiếm giải pháp tốt cho các vấn đề được trình bày bởi mẫu Kho lưu trữ điển hình (danh sách các phương pháp ngày càng tăng cho các truy vấn chuyên biệt, v.v. xem: http://ayende.com/blog/3955/repository- is-the-new-singleton ).

Tôi thực sự thích ý tưởng sử dụng các truy vấn Lệnh, đặc biệt là thông qua việc sử dụng mẫu Đặc tả. Tuy nhiên, vấn đề của tôi với đặc tả là nó chỉ liên quan đến tiêu chí của các lựa chọn đơn giản (về cơ bản, mệnh đề where) và không giải quyết các vấn đề khác của truy vấn, chẳng hạn như nối, nhóm, lựa chọn tập hợp con hoặc phép chiếu, v.v. về cơ bản, tất cả các vòng bổ sung mà nhiều truy vấn phải trải qua để có được tập dữ liệu chính xác.

(lưu ý: Tôi sử dụng thuật ngữ "lệnh" như trong Mẫu lệnh, còn được gọi là đối tượng truy vấn. Tôi không nói về lệnh như trong phân tách lệnh / truy vấn, nơi có sự phân biệt giữa truy vấn và lệnh (cập nhật, xóa, chèn))

Vì vậy, tôi đang tìm kiếm các giải pháp thay thế gói gọn toàn bộ truy vấn, nhưng vẫn đủ linh hoạt để bạn không chỉ hoán đổi Kho chứa spaghetti cho sự bùng nổ của các lớp lệnh.

Tôi đã sử dụng, ví dụ như Linqspecs, và mặc dù tôi tìm thấy một số giá trị trong việc có thể gán các tên có ý nghĩa cho các tiêu chí lựa chọn, nhưng điều đó là chưa đủ. Có lẽ tôi đang tìm kiếm một giải pháp kết hợp kết hợp nhiều cách tiếp cận.

Tôi đang tìm kiếm các giải pháp mà những người khác có thể đã phát triển để giải quyết vấn đề này hoặc giải quyết một vấn đề khác nhưng vẫn đáp ứng các yêu cầu này. Trong bài viết được liên kết, Ayende đề xuất sử dụng ngữ cảnh nHibernate trực tiếp, nhưng tôi cảm thấy điều đó phần lớn làm phức tạp lớp nghiệp vụ của bạn vì bây giờ nó cũng phải chứa thông tin truy vấn.

Tôi sẽ cung cấp tiền thưởng cho điều này, ngay sau khi thời gian chờ kết thúc. Vì vậy, hãy làm cho các giải pháp của bạn trở nên xứng đáng, với những lời giải thích tốt và tôi sẽ chọn giải pháp tốt nhất và ủng hộ những người chạy nhất.

LƯU Ý: Tôi đang tìm thứ gì đó dựa trên ORM. Không nhất thiết phải là EF hoặc nHibernate rõ ràng, nhưng đó là những thứ phổ biến nhất và sẽ phù hợp nhất. Nếu nó có thể dễ dàng thích nghi với ORM khác thì đó sẽ là một phần thưởng. Linq tương thích cũng sẽ tốt.

CẬP NHẬT: Tôi thực sự ngạc nhiên rằng không có nhiều gợi ý hay ở đây. Có vẻ như mọi người hoàn toàn là CQRS hoặc họ hoàn toàn ở trong trại Kho lưu trữ. Hầu hết các ứng dụng của tôi không đủ phức tạp để đảm bảo CQRS (điều mà hầu hết những người ủng hộ CQRS đều sẵn sàng nói rằng bạn không nên sử dụng nó).

CẬP NHẬT: Có vẻ như có một chút nhầm lẫn ở đây. Tôi không tìm kiếm một công nghệ truy cập dữ liệu mới mà là một giao diện được thiết kế hợp lý giữa doanh nghiệp và dữ liệu.

Lý tưởng nhất, những gì tôi đang tìm kiếm là một số loại giao nhau giữa các đối tượng Truy vấn, mẫu Đặc tả và kho lưu trữ. Như tôi đã nói ở trên, mẫu Đặc tả chỉ xử lý khía cạnh mệnh đề where, chứ không phải các khía cạnh khác của truy vấn, chẳng hạn như kết hợp, lựa chọn con, v.v. . Các đối tượng truy vấn cũng xử lý toàn bộ truy vấn, nhưng tôi không muốn chỉ thay thế các kho lưu trữ bằng các vụ nổ các đối tượng truy vấn.


5
Câu hỏi tuyệt vời. Tôi cũng muốn xem những người có nhiều kinh nghiệm hơn tôi đề xuất. Tôi đang làm việc trên một cơ sở mã tại thời điểm này, nơi kho lưu trữ chung cũng chứa quá tải cho các đối tượng Lệnh hoặc đối tượng Truy vấn, cấu trúc của đối tượng này tương tự như những gì Ayende mô tả trong blog của anh ấy. Tái bút: Điều này cũng có thể thu hút sự chú ý của các lập trình viên.
Simon Whitehead

Tại sao không chỉ sử dụng một kho lưu trữ hiển thị IQueryable nếu bạn không ngại sự phụ thuộc vào LINQ? Cách tiếp cận phổ biến là một kho lưu trữ chung và sau đó khi bạn cần logic có thể sử dụng lại ở trên, bạn tạo một loại kho lưu trữ dẫn xuất với các phương thức bổ sung của mình.
devdigital

@devdigital - Sự phụ thuộc vào Linq không giống như sự phụ thuộc vào việc triển khai dữ liệu. Tôi muốn sử dụng Linq cho các đối tượng, vì vậy tôi có thể sắp xếp hoặc thực hiện các chức năng lớp nghiệp vụ khác. Nhưng điều đó không có nghĩa là tôi muốn phụ thuộc vào việc triển khai mô hình dữ liệu. Điều tôi thực sự đang nói ở đây là giao diện lớp / tầng. Ví dụ: tôi muốn có thể thay đổi một truy vấn và không phải thay đổi nó ở 200 vị trí, đó là điều sẽ xảy ra nếu bạn đẩy IQueryable trực tiếp vào mô hình kinh doanh.
Erik Funkenbusch

1
@devdigital - về cơ bản chỉ chuyển các vấn đề với kho lưu trữ vào lớp nghiệp vụ của bạn. Bạn chỉ đang xáo trộn vấn đề xung quanh.
Erik Funkenbusch

Câu trả lời:


94

Tuyên bố từ chối trách nhiệm: Vì chưa có bất kỳ câu trả lời tuyệt vời nào, tôi quyết định đăng một phần từ một bài đăng blog tuyệt vời mà tôi đã đọc cách đây một thời gian, được sao chép gần như nguyên văn. Bạn có thể tìm thấy toàn bộ bài đăng trên blog ở đây . Vì vậy, đây là:


Chúng ta có thể xác định hai giao diện sau:

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

Chỉ IQuery<TResult>định một thông báo xác định một truy vấn cụ thể với dữ liệu mà nó trả về bằng cách sử dụng TResultkiểu chung. Với giao diện đã xác định trước đó, chúng ta có thể xác định một thông báo truy vấn như sau:

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}

Lớp này định nghĩa một thao tác truy vấn với hai tham số, điều này sẽ dẫn đến một mảng các Userđối tượng. Lớp xử lý thông báo này có thể được định nghĩa như sau:

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();
    }
}

Giờ đây, chúng tôi có thể để người tiêu dùng phụ thuộc vào IQueryHandlergiao diện chung :

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);
    }
}

Ngay lập tức, mô hình này cung cấp cho chúng tôi rất nhiều sự linh hoạt, bởi vì bây giờ chúng tôi có thể quyết định những gì sẽ tiêm vào UserController. Chúng tôi có thể đưa vào một triển khai hoàn toàn khác hoặc một triển khai kết thúc triển khai thực sự mà không cần phải thực hiện thay đổi đối với UserController(và tất cả những người tiêu dùng khác của giao diện đó).

Các IQuery<TResult>giao diện cho chúng ta thời gian biên dịch hỗ trợ khi xác định hoặc tiêm IQueryHandlerstrong mã của chúng tôi. Khi chúng tôi thay đổi FindUsersBySearchTextQuerythành trả về UserInfo[]thay thế (bằng cách triển khai IQuery<UserInfo[]>), UserControllersẽ không biên dịch được, vì ràng buộc kiểu chung trên IQueryHandler<TQuery, TResult>sẽ không thể ánh xạ FindUsersBySearchTextQuerytới User[].

IQueryHandlerTuy nhiên, việc đưa giao diện vào người tiêu dùng có một số vấn đề ít rõ ràng hơn vẫn cần được giải quyết. Số lượng phụ thuộc của người tiêu dùng của chúng ta có thể quá lớn và có thể dẫn đến việc chèn quá nhiều hàm tạo - khi một hàm tạo nhận quá nhiều đối số. Số lượng truy vấn mà một lớp thực thi có thể thay đổi thường xuyên, điều này sẽ yêu cầu thay đổi liên tục đối với số lượng đối số của phương thức khởi tạo.

Chúng tôi có thể khắc phục vấn đề phải tiêm quá nhiều IQueryHandlersvới một lớp trừu tượng bổ sung. Chúng tôi tạo một người hòa giải nằm giữa người tiêu dùng và người xử lý truy vấn:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

Đây IQueryProcessorlà một giao diện không chung chung với một phương pháp chung. Như bạn có thể thấy trong định nghĩa giao diện, IQueryProcessortùy thuộc vào IQuery<TResult>giao diện. Điều này cho phép chúng tôi hỗ trợ thời gian biên dịch trong những người tiêu dùng phụ thuộc vào IQueryProcessor. Hãy viết lại UserControllerđể sử dụng mới IQueryProcessor:

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);
    }
}

Hiện UserControllertại phụ thuộc vào a IQueryProcessorcó thể xử lý tất cả các truy vấn của chúng tôi. Các UserController's SearchUsersphương pháp gọi là IQueryProcessor.Processphương pháp đi qua trong một đối tượng truy vấn khởi tạo. Vì giao diện FindUsersBySearchTextQuerytriển khai IQuery<User[]>, chúng ta có thể chuyển nó sang Execute<TResult>(IQuery<TResult> query)phương thức chung . Nhờ suy luận kiểu C #, trình biên dịch có thể xác định kiểu chung và điều này giúp chúng ta tiết kiệm được kiểu phải khai báo rõ ràng. Kiểu trả về của Processphương thức cũng được biết đến.

Bây giờ nó là trách nhiệm của việc thực hiện IQueryProcessorđể tìm ra quyền IQueryHandler. Điều này yêu cầu một số thao tác nhập động và tùy chọn sử dụng khung Phụ thuộc Injection và tất cả có thể được thực hiện chỉ với một vài dòng mã:

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);
    }
}

Các QueryProcessorlớp xây dựng một cụ thể IQueryHandler<TQuery, TResult>loại dựa trên các loại của các ví dụ truy vấn đã cung cấp. Kiểu này được sử dụng để yêu cầu lớp vùng chứa được cung cấp lấy một thể hiện của kiểu đó. Thật không may, chúng ta cần gọi Handlephương thức bằng cách sử dụng phản chiếu (bằng cách sử dụng từ khóa dymamic C # 4.0 trong trường hợp này), bởi vì tại thời điểm này, không thể ép kiểu trình xử lý, vì TQueryđối số chung không có sẵn tại thời điểm biên dịch. Tuy nhiên, trừ khi Handlephương thức được đổi tên hoặc nhận các đối số khác, cuộc gọi này sẽ không bao giờ thất bại và nếu bạn muốn, rất dễ dàng để viết một bài kiểm tra đơn vị cho lớp này. Sử dụng phản chiếu sẽ giảm nhẹ, nhưng không có gì đáng lo ngại.


Để trả lời một trong những mối quan tâm của bạn:

Vì vậy, tôi đang tìm kiếm các giải pháp thay thế gói gọn toàn bộ truy vấn, nhưng vẫn đủ linh hoạt để bạn không chỉ hoán đổi Kho chứa spaghetti cho sự bùng nổ của các lớp lệnh.

Hệ quả của việc sử dụng thiết kế này là sẽ có rất nhiều lớp nhỏ trong hệ thống, nhưng có rất nhiều lớp nhỏ / tập trung (có tên rõ ràng) là một điều tốt. Cách tiếp cận này rõ ràng là tốt hơn nhiều khi có nhiều quá tải với các tham số khác nhau cho cùng một phương thức trong một kho lưu trữ, vì bạn có thể nhóm chúng trong một lớp truy vấn. Vì vậy, bạn vẫn nhận được ít lớp truy vấn hơn nhiều so với các phương thức trong kho lưu trữ.


2
Có vẻ như bạn nhận được giải thưởng. Tôi thích các khái niệm, tôi chỉ hy vọng ai đó trình bày một cái gì đó thực sự khác biệt. Chúc mừng.
Erik Funkenbusch

1
@FuriCuri, một lớp có thực sự cần 5 truy vấn không? Có lẽ bạn có thể coi đó là một lớp có quá nhiều trách nhiệm. Ngoài ra, nếu các truy vấn đang được tổng hợp thì có lẽ chúng thực sự phải là một truy vấn duy nhất. Tất nhiên đây chỉ là những gợi ý.
Sam

1
@stakx Bạn hoàn toàn đúng rằng trong ví dụ ban đầu của tôi, TResulttham số chung của IQuerygiao diện không hữu ích. Tuy nhiên, trong phản hồi cập nhật của tôi, TResulttham số được sử dụng bởi Processphương thức của IQueryProcessorđể phân giải IQueryHandlerthời gian chạy.
david.s

1
Tôi cũng có một blog với cách triển khai tương tự khiến tôi nghĩ rằng mình đang đi đúng đường, đây là liên kết jupaol.blogspot.mx/2012/11/… và tôi đã sử dụng nó một thời gian trong các ứng dụng PROD, nhưng tôi đã gặp vấn đề với cách tiếp cận này. Chuỗi và sử dụng lại các truy vấn Giả sử rằng tôi có một số truy vấn nhỏ cần được kết hợp để tạo ra các truy vấn phức tạp hơn, cuối cùng tôi chỉ sao chép mã nhưng tôi đang tìm kiếm một cách tiếp cận tốt hơn và rõ ràng hơn. Bất kỳ ý tưởng?
Jupaol

4
@Cemre Tôi đã kết thúc việc đóng gói các truy vấn của mình trong các phương thức Tiện ích mở rộng trả về IQueryablevà đảm bảo không liệt kê bộ sưu tập, sau đó từ QueryHandlertôi vừa gọi / chuỗi các truy vấn. Điều này đã cho tôi sự linh hoạt để kiểm tra đơn vị các truy vấn của mình và xâu chuỗi chúng. Tôi có một dịch vụ Application trên đầu trang của tôi QueryHandler, và điều khiển của tôi là phụ trách để nói chuyện trực tiếp với các dịch vụ thay vì xử lý
Jupaol

4

Cách xử lý của tôi thực sự đơn giản và ORM bất khả tri. Quan điểm của tôi về kho lưu trữ là thế này: Công việc của kho lưu trữ là cung cấp cho ứng dụng mô hình cần thiết cho ngữ cảnh, vì vậy ứng dụng chỉ yêu cầu kho lưu trữ những gì nó muốn nhưng không cho nó biết cách lấy nó.

Tôi cung cấp cho phương thức kho lưu trữ một Tiêu chí (vâng, kiểu DDD), tiêu chí này sẽ được kho lưu trữ sử dụng để tạo truy vấn (hoặc bất cứ điều gì được yêu cầu - nó có thể là một yêu cầu dịch vụ web). Tham gia và nhóm imho là chi tiết về cách thức, không phải cái gì và một tiêu chí chỉ nên là cơ sở để xây dựng mệnh đề where.

Mô hình = đối tượng cuối cùng hoặc cấu trúc dữ liệu mà ứng dụng cần.

public class MyCriteria
{
   public Guid Id {get;set;}
   public string Name {get;set;}
    //etc
 }

 public interface Repository
  {
       MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria);
   }

Có lẽ bạn có thể sử dụng trực tiếp tiêu chí ORM (Nhibernate) nếu bạn muốn. Việc triển khai kho lưu trữ nên biết cách sử dụng Tiêu chí với kho lưu trữ cơ bản hoặc DAO.

Tôi không biết miền của bạn và các yêu cầu về mô hình nhưng sẽ rất lạ nếu cách tốt nhất là ứng dụng tự tạo truy vấn. Mô hình thay đổi quá nhiều mà bạn không thể xác định một cái gì đó ổn định?

Giải pháp này rõ ràng yêu cầu một số mã bổ sung nhưng nó không kết hợp phần còn lại với ORM hoặc bất kỳ thứ gì bạn đang sử dụng để truy cập bộ nhớ. Kho lưu trữ thực hiện công việc của nó để hoạt động như một mặt tiền và IMO nó sạch sẽ và mã 'bản dịch tiêu chí' có thể sử dụng lại


Điều này không giải quyết các vấn đề về tăng trưởng kho lưu trữ và có một danh sách các phương pháp ngày càng mở rộng để trả về nhiều loại dữ liệu khác nhau. Tôi hiểu rằng bạn có thể không thấy vấn đề với điều này (nhiều người thì không), nhưng những người khác lại thấy nó khác (tôi khuyên bạn nên đọc bài viết mà tôi đã liên kết, có rất nhiều người khác có ý kiến ​​tương tự).
Erik Funkenbusch

1
Tôi giải quyết nó, bởi vì các tiêu chí làm cho rất nhiều phương pháp không cần thiết. Tất nhiên, không phải tất cả chúng, tôi không thể nói nhiều nếu không biết bất cứ điều gì về ting bạn cần. Tôi đang bị ấn tượng mặc dù bạn muốn truy vấn trực tiếp db vì vậy, có lẽ một kho lưu trữ đang cản đường. Nếu bạn cần làm việc trực tiếp với sotrage quan hệ, hãy truy cập trực tiếp, không cần kho lưu trữ. Và như một lưu ý, thật khó chịu khi nhiều người trích dẫn Ayende với bài đăng đó. Tôi không đồng ý với điều đó và tôi nghĩ rằng nhiều nhà phát triển đang sử dụng mô hình sai cách.
MikeSW

1
Nó có thể làm giảm phần nào vấn đề, nhưng với một ứng dụng đủ lớn, nó vẫn sẽ tạo ra các kho lưu trữ quái vật. Tôi không đồng ý với giải pháp sử dụng nHibernate trực tiếp theo logic chính của Ayende, nhưng tôi đồng ý với anh ấy về sự phi lý của việc tăng trưởng kho lưu trữ ngoài tầm kiểm soát. Tôi không muốn truy vấn trực tiếp cơ sở dữ liệu, nhưng tôi cũng không muốn chuyển vấn đề từ một kho lưu trữ sang một loạt các đối tượng truy vấn.
Erik Funkenbusch

2

Tôi đã làm điều này, hỗ trợ điều này và hoàn tác điều này.

Vấn đề chính là ở đây: cho dù bạn làm như thế nào đi nữa, thì phần trừu tượng được thêm vào cũng không giúp bạn có được sự độc lập. Nó sẽ bị rò rỉ theo định nghĩa. Về bản chất, bạn đang phát minh ra toàn bộ một lớp chỉ để làm cho mã của bạn trông dễ thương ... nhưng nó không làm giảm khả năng bảo trì, cải thiện khả năng đọc hoặc mang lại cho bạn bất kỳ loại thuyết bất khả tri mô hình nào.

Phần thú vị là bạn đã trả lời câu hỏi của chính mình trước câu trả lời của Olivier: "điều này về cơ bản là sao chép chức năng của Linq mà không có tất cả lợi ích bạn nhận được từ Linq".

Hãy tự hỏi bản thân: làm thế nào nó có thể không được?


Chà, tôi chắc chắn đã gặp vấn đề khi tích hợp Linq vào lớp nghiệp vụ của bạn. Nó rất mạnh mẽ, nhưng khi chúng tôi thực hiện thay đổi mô hình dữ liệu thì đó là một cơn ác mộng. Mọi thứ được cải thiện với kho lưu trữ, vì tôi có thể thực hiện các thay đổi ở một nơi được bản địa hóa mà không ảnh hưởng nhiều đến lớp nghiệp vụ (trừ khi bạn cũng phải thay đổi lớp nghiệp vụ để hỗ trợ các thay đổi). Tuy nhiên, kho lưu trữ trở thành những lớp phình to này vi phạm SRP hàng loạt. Tôi hiểu quan điểm của bạn, nhưng nó cũng không thực sự giải quyết được vấn đề gì.
Erik Funkenbusch

Nếu lớp dữ liệu của bạn sử dụng LINQ và các thay đổi mô hình dữ liệu yêu cầu thay đổi lớp nghiệp vụ của bạn ... thì bạn không phân lớp đúng cách.
Stu

Tôi nghĩ bạn đang nói rằng bạn không còn thêm lớp đó nữa. Khi bạn nói rằng phần trừu tượng được thêm vào không giúp bạn được gì, điều đó có nghĩa là bạn đồng ý với Ayende về việc chuyển phiên nHibernate (hoặc ngữ cảnh EF) trực tiếp vào lớp nghiệp vụ của bạn.
Erik Funkenbusch

1

Bạn có thể sử dụng một giao diện thông thạo. Ý tưởng cơ bản là các phương thức của một lớp trả về cá thể hiện tại cho chính lớp này sau khi thực hiện một số hành động. Điều này cho phép bạn chuỗi các cuộc gọi phương thức.

Bằng cách tạo một hệ thống phân cấp lớp thích hợp, bạn có thể tạo một luồng hợp lý của các phương thức có thể truy cập.

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;
    }
}

Bạn sẽ gọi nó như thế này

string query = new Query("myTable", "name", "SUM(amount) AS total")
    .Where("name LIKE 'A%'")
    .GroupBy("name")
    .Having("COUNT(*) > 2")
    .OrderBy("name")
    .ToString();

Bạn chỉ có thể tạo một phiên bản mới của Query. Các lớp khác có một hàm tạo được bảo vệ. Điểm của hệ thống phân cấp là "vô hiệu hóa" các phương thức. Ví dụ, GroupByphương thức trả về a GroupedQuerylà lớp cơ sở của Queryvà không có Wherephương thức (phương thức được khai báo ở đâu Query). Do đó không thể gọi Wheresau GroupBy.

Tuy nhiên nó không hoàn hảo. Với cấu trúc phân cấp lớp này, bạn có thể ẩn thành viên liên tiếp, nhưng không hiển thị những người mới. Do đó Havingném một ngoại lệ khi nó được gọi trước đó GroupBy.

Lưu ý rằng có thể gọi Wherenhiều lần. Điều này bổ sung các điều kiện mới với ANDđiều kiện hiện có. Điều này giúp dễ dàng hơn trong việc tạo các bộ lọc theo chương trình từ các điều kiện đơn lẻ. Điều tương tự cũng có thể xảy ra với Having.

Các phương thức chấp nhận danh sách trường có một tham số params string[] fields. Nó cho phép bạn chuyển các tên trường đơn lẻ hoặc một mảng chuỗi.


Các giao diện Fluent rất linh hoạt và không yêu cầu bạn tạo nhiều phương thức quá tải với các tổ hợp tham số khác nhau. Ví dụ của tôi hoạt động với chuỗi, tuy nhiên cách tiếp cận có thể được mở rộng cho các loại khác. Bạn cũng có thể khai báo các phương thức được xác định trước cho các trường hợp đặc biệt hoặc các phương thức chấp nhận các loại tùy chỉnh. Bạn cũng có thể thêm các phương thức như ExecuteReaderhoặc ExceuteScalar<T>. Điều này sẽ cho phép bạn xác định các truy vấn như thế này

var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
    .Where(new CurrentMonthCondition())
    .Where(new DivisionCondition{ DivisionType = DivisionType.Production})
    .OrderBy(new StandardMonthlyReportSorting())
    .ExecuteReader();

Ngay cả các lệnh SQL được xây dựng theo cách này cũng có thể có các tham số lệnh và do đó tránh được các vấn đề về chèn SQL và đồng thời cho phép các lệnh được lưu vào bộ đệm bởi máy chủ cơ sở dữ liệu. Đây không phải là sự thay thế cho trình ánh xạ O / R nhưng có thể hữu ích trong các tình huống mà bạn sẽ tạo các lệnh bằng cách sử dụng nối chuỗi đơn giản.


3
Hmm .. Thật thú vị, nhưng giải pháp của bạn dường như có vấn đề với các khả năng SQL Injection và không thực sự tạo các câu lệnh chuẩn bị sẵn để thực thi được biên dịch trước (do đó hoạt động chậm hơn). Nó có thể được điều chỉnh để khắc phục những vấn đề đó, nhưng sau đó chúng tôi bị mắc kẹt với kết quả tập dữ liệu an toàn không phải loại và những gì không. Tôi thích giải pháp dựa trên ORM hơn và có lẽ tôi nên chỉ định điều đó một cách rõ ràng. Điều này về cơ bản là sao chép chức năng của Linq mà không có tất cả những lợi ích bạn nhận được từ Linq.
Erik Funkenbusch

Tôi biết những vấn đề này. Đây chỉ là một giải pháp nhanh chóng và đơn giản, cho thấy cách tạo ra một giao diện thông thạo. Trong một giải pháp thế giới thực, bạn có thể sẽ “nướng” cách tiếp cận hiện có của mình thành một giao diện thông thạo phù hợp với nhu cầu của bạn.
Olivier Jacot-Descombes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.