设计面向对象和单元测试友好的查询系统

发布于 2024-08-04 10:08:40 字数 1189 浏览 3 评论 0原文

我正在开发一个应用程序,允许牙医捕获有关某些临床活动的信息。虽然该应用程序不是高度可定制的(没有定制工作流程或表单),但它确实提供了一些基本的定制功能;客户可以选择使用自己的自定义字段来扩充预定义的表单字段。管理员可以创建大约六种不同的字段类型(即文本、日期、数字、下拉菜单等)。我们在持久性方面使用实体属性值 (EAV) 来对此功能进行建模。

该应用程序的其他主要功能之一是能够针对这些自定义字段创建自定义查询。这是通过 UI 完成的,在 UI 中可以创建任意数量的规则(日期 <=(现在 - 5 天)、文本如“444”、DropDown ==“ICU”)。所有规则都通过 AND 组合在一起以生成查询。

当前的实现(我“继承”的)既不是面向对象的,也不是可单元测试的。本质上,有一个“上帝”类将所有无数的规则类型直接编译成复杂的动态SQL 语句(即内连接、外连接和子选择)。由于以下几个原因,这种方法很麻烦:

  • 单独对各个规则进行单元测试 几乎不可能
  • 最后一点也意味着在 未来肯定会违反 开闭原则。
  • 业务逻辑和持久性问题正在混合在一起。
  • 由于需要真正的数据库,所以运行缓慢的单元测试(SQLLite 无法解析 T-SQL 并且模拟解析器会呃...很难)

我正在尝试提出一种灵活、可维护和可测试的替代设计,同时仍然保持查询性能相当快。最后一点很关键,因为我想象基于 OOAD 的实现将至少将一些数据过滤逻辑从数据库服务器移动到 (.NET) 应用程序服务器。

我正在考虑命令和责任链模式的组合:

查询类包含抽象规则类(DateRule、TextRule 等)的集合。并保存对包含未过滤数据集的 DataSet 类的引用。 DataSet 以与持久性无关的方式建模(即没有对数据库类型的引用或挂钩)。Rule

有一个 Filter() 方法,该方法接受 DataSet,对其进行适当的过滤,然后将其返回给调用者。 Query 类不是简单地迭代每个规则,从而允许每个规则根据其认为合适的方式过滤数据集。一旦所有规则都执行完毕或者数据集被过滤为空,执行就会停止。

这种方法让我担心的一件事是解析 .NET 中潜在的大型未过滤数据集的性能影响。肯定有一些经过验证的方法可以解决此类问题,在可维护性和性能之间提供良好的平衡吗?

最后一点:管理层不允许使用 NHibernate。 Linq to SQL 可能是可行的,但我不确定该技术对当前任务的适用性如何。

非常感谢并期待大家的反馈!

更新:仍在寻找解决方案。

I'm working on an application that allows dentists to capture information about certain clinical activities. While the application is not highly customizable (no custom workflows or forms) it does offer some rudimentary customization capabilities; clients can choose to augment the predefined form fields with their own custom ones. There are about half a dozen different field types that admins can create (i.e. Text, Date, Numeric, DropDown, etc). We're using Entity-Attribute-Value (EAV) on the persistence side to model this functionality.

One of the other key features of the application is the ability to create custom queries against these custom fields. This is accomplished via a UI in which any number of rules (Date <= (Now - 5 Days), Text Like '444', DropDown == 'ICU') can be created. All rules are AND'ed together to produce a query.

The current implementation (which I "inherited") is neither object oriented nor unit testable. Essentially, there is a single "God" class that compiles all the myriad rule types directly into a complex dynamic SQL statement (i.e. inner joins, outer joins, and subselects). This approach is troublesome for several reasons:

  • Unit testing individual rules in isolation
    is nearly impossible
  • That last point also means adding additional rule types in the
    future will most definitely violate
    the Open Closed Principle.
  • Business logic and persistence concerns are being co-mingled.
  • Slow running unit tests since a real database is required (SQLLite can't parse T-SQL and mocking out a parser would be uhh...hard)

I'm trying to come up with a replacement design that is flexible, maintainable and testable, while still keeping query performance fairly snappy. This last point is key since I imagine an OOAD based implementation will move at least some of the data filtering logic from the database server to the (.NET) application server.

I'm considering a combination of the Command and Chain-of-Responsibility patterns:

The Query class contains a collection of abstract Rule classes (DateRule, TextRule, etc). and holds a reference to a DataSet class that contains an unfiltered set of data. DataSet is modeled in a persistence agnostic fashion (i.e no references or hooks into database types)

Rule has a single Filter() method which takes in an DataSet, filters it appropriately, and then returns it to the caller. The Query class than simply iterates over each Rule, allowing each Rule to filter the DataSet as it sees fit. Execution would stop once all rules have been executed or once the DataSet has been filtered down to nothing.

The one thing that worries me about this approach are the performance implications of parsing a potentially large unfiltered data set in .NET. Surely there are some tried and true approaches to solving just this kind of problem that offer a good balance between maintainability and performance?

One final note: management won't allow the use of NHibernate. Linq to SQL might be possible, but I'm not sure how applicable that technology would be to the task at hand.

Many thanks and I look forward to everyone's feedback!

Update: Still looking for a solution on this.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

毁梦 2024-08-11 10:08:40

我认为 LINQ to SQL 或许与 VS2008 示例中的 Dynamic LINQ 结合起来是一个理想的解决方案。使用 LINQ,特别是使用 IEnumerable/IQueryable 上的扩展方法,您可以根据获得的输入使用标准和自定义逻辑来构建查询。我大量使用这种技术在我的许多 MVC 操作上实现过滤器,效果非常好。由于它实际上构建了一个表达式树,然后使用它在需要具体化查询的位置生成 SQL,因此我认为这对于您的场景来说是理想的选择,因为大部分繁重的工作仍然由 SQL 服务器完成。如果 LINQ 被证明生成非最佳查询,您始终可以使用添加到 LINQ 数据上下文中的表值函数或存储过程作为利用优化查询的方法。

更新:您也可以尝试使用 C# 3.0 中的 PredicateBuilder简而言之。

示例:查找标题包含一组搜索词之一且出版商为 O'Reilly 的所有图书。

 var predicate = PredicateBuilder.True<Book>();
 predicate = predicate.And( b => b.Publisher == "O'Reilly" );
 var titlePredicate = PredicateBuilder.False<Book>();
 foreach (var term in searchTerms)
 {
     titlePredicate = titlePredicate.Or( b => b.Title.Contains( term ) );
 }
 predicate = predicate.And( titlePredicate );

 var books = dc.Book.Where( predicate );

I think that LINQ to SQL would be an ideal solution coupled, perhaps, with Dynamic LINQ from the VS2008 samples. Using LINQ, particularly with extension methods on IEnumerable/IQueryable, you can build up your queries using your standard and custom logic depending on the inputs that you get. I use this technique heavily to implement filters on many of my MVC actions to great effect. Since it actually builds an expression tree then uses it to generate the SQL at the point where the query needs to be materialized, I think it would be ideal for your scenario since most of the heavy lifting is still done by the SQL server. In cases where LINQ proves to generate non-optimal queries you can always use table-valued functions or stored procedures added to your LINQ data context as methods to take advantage of optimized queries.

Updated: You might also try using PredicateBuilder from C# 3.0 in a Nutshell.

Example: find all Books where the Title contains one of a set of search terms and the publisher is O'Reilly.

 var predicate = PredicateBuilder.True<Book>();
 predicate = predicate.And( b => b.Publisher == "O'Reilly" );
 var titlePredicate = PredicateBuilder.False<Book>();
 foreach (var term in searchTerms)
 {
     titlePredicate = titlePredicate.Or( b => b.Title.Contains( term ) );
 }
 predicate = predicate.And( titlePredicate );

 var books = dc.Book.Where( predicate );
薔薇婲 2024-08-11 10:08:40

我所看到的完成方式是创建对象来模拟您希望用户构建查询的每个条件,并使用这些对象构建对象树。

从对象树中,您应该能够递归地构建满足查询的 SQL 语句。

您需要的基本对象是 AND 和 OR 对象,以及用于模型比较的对象,例如 EQUALS、LESSTHAN 等。您可能希望对这些对象使用一个接口,以便更轻松地将它们以不同的方式链接在一起。

一个简单的例子:

public interface IQueryItem
{
    public String GenerateSQL();
}


public class AndQueryItem : IQueryItem
{
    private IQueryItem _FirstItem;
    private IQueryItem _SecondItem;

    // Properties and the like

    public String GenerateSQL()
    {
        StringBuilder builder = new StringBuilder();
        builder.Append(_FirstItem.GenerateSQL());
        builder.Append(" AND ");
        builder.Append(_SecondItem.GenerateSQL());

        return builder.ToString();
    }
}

以这种方式实现它应该允许您非常轻松地对规则进行单元测试。

不利的一面是,该解决方案仍然让数据库完成大量工作,这听起来像是您并不真正想做的。

The way I've seen it done is by creating objects that model each of the conditions you want the user to build their query from, and build up a tree of objects using those.

From the tree of objects you should be able to recursively build up an SQL statement that satisfies the query.

The basic ones you'll need will be AND and OR objects, as well as objects to model comparison, like EQUALS, LESSTHAN etc. You'll probably want to use an interface for these objects to make chaining them together in different ways easier.

A trivial example:

public interface IQueryItem
{
    public String GenerateSQL();
}


public class AndQueryItem : IQueryItem
{
    private IQueryItem _FirstItem;
    private IQueryItem _SecondItem;

    // Properties and the like

    public String GenerateSQL()
    {
        StringBuilder builder = new StringBuilder();
        builder.Append(_FirstItem.GenerateSQL());
        builder.Append(" AND ");
        builder.Append(_SecondItem.GenerateSQL());

        return builder.ToString();
    }
}

Implementing it this way should allow you to Unit Test the rules pretty easily.

On the negative side, this solution still leaves the database to do a lot of the work, which it sounds like you don't really want to do.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文