4

I've got to build some somewhat complicated WHERE clauses in SQL for a project I'm working on, and the clauses feel very hierarchical with their combination of ANDs and ORs. Instead of:

WHERE ([userId] NOT IN @excludeUsers) AND ((([firstname] LIKE @nameFilter) OR ([surname] LIKE @nameFilter)) AND (([jobTitle] LIKE @infoFilter) OR ([mobileNo] LIKE @infoFilter)))

... I want to be able to write something like the following:

// Wcb is a WhereClauseBuilder
OrClause innerOr;
var whereClause =
    Wcb.And(
        "[userId] NOT IN @excludeUsers",
        Wcb.And(
            Wcb.Or(
                "[firstname] LIKE @nameFilter",
                "[surname] LIKE @nameFilter"
            ),
            innerOr = Wcb.Or(
                "[jobTitle] LIKE @infoFilter",
                "[mobileNo] LIKE @infoFilter"
            )
        )
    );

The idea is to eliminate mistakes like missing whitespace, brackets, and AND/OR keywords, from the query. The And and Or static methods would create instances of AndClause and OrClause classes, and they'd overload ToString allowing the whole object graph to resolve to a string upon $"{whereClause}". I'd also like to be able to add to the query later on, like:

if (extraInfoFilter != null) {
    innerOr.Or(
        "[extraInfo] LIKE @extraInfoFilter"
    );
}

However, the code I'm writing for this has gotten complex enough to prompt me to ask: is this solution over-engineered? Should I just build the strings manually instead of generating them from a hierarchical object model like this? Are there any practical reasons why that would be a better approach?

7
  • 2
    Doesn't Linq already support this?
    – Alexander
    Apr 25, 2020 at 18:33
  • 2
    Nope, I don't have a C# environment set up, and I haven't touched it in years. But I thought one of the whole points of Linq is that it's a query interface that's independent of the underlying storage. It could be an array, a CSV file, or an SQL database, and it would work the same docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/linq/…
    – Alexander
    Apr 25, 2020 at 18:48
  • 2
    @Alexander: Well... Sort of. Linq to Objects (what most people simply call Linq) works with in-memory collections, and its syntax is very "standard." But there are things you can do with Linq to Objects that you can't do with, say, Entity Framework, because, unlike Linq to Objects, Linq to EF generates SQL dynamically, and some of Linq's operations are not supported. Linq to XML is also somewhat specialized, and some of that looks very different from Linq to Objects. Apr 25, 2020 at 23:04
  • 2
    @Jez: I'd say either over-enginered or under-engineered. Is it just for a single static query, then just use SQL, it is perfectly fine. But if you need a more general query builder, you are on your way down a rabbit hole and will end up with a bespoke ORM. Just bite the bullet and use a standard ORM like EF Core, and you will save a lot of grief in the long run.
    – JacquesB
    Apr 27, 2020 at 11:03
  • 2
    A small tip - in situations like these, I always write the SQL for the where clause as "WHERE 1 = 1". This allows you to just add the extra filters on without trying to keep track of which one omits the "AND" bit. Also, still works fine if there happens to not be any filters.
    – TallGuy
    Apr 30, 2020 at 20:07

4 Answers 4

10

This would be over engineered if the components of the query are all known ahead of time. If you had one query that required a few dynamic criteria, then I would probably go for string concatenation and be done with it. If you have more than a few dynamic conditions, or multiple queries that need dynamic conditions, then investing the time in a query builder object is definitely justified, if you have no other utility to do so.

1
  • 2
    SQL is far more a mature language and the easiest is to use simple string formatting, many times the builders have gone wild when there is support neede for all parts of the query models. Apr 26, 2020 at 13:47
9

Your proposal does seem over-engineered. Mere proper formatting of the SQL would make the conditions at least as understandable as the alternative you propose.

WHERE
([userId] NOT IN @excludeUsers) 
AND 
(
    (
        ([firstname] LIKE @nameFilter) 
        OR 
        ([surname] LIKE @nameFilter)
    ) 
    AND
    (
        ([jobTitle] LIKE @infoFilter)
        OR
        ([mobileNo] LIKE @infoFilter)
    )
)
3
  • 1
    One should add here that such queries will only look that nice when the programming language supports multiline string literals (which C# does).
    – Doc Brown
    Apr 26, 2020 at 7:34
  • 2
    @DocBrown, very true, but you can still reproduce the same effect in any common language by assembling the string over multiple lines. The result is always considerably more lightweight than trying to reimplement the SQL language with a series of method calls.
    – Steve
    Apr 26, 2020 at 9:51
  • And if formatting is an issue, you can store the query in a text file, or spin it off into a stored procedure. Apr 26, 2020 at 13:55
0

Formatting

Proper formatting is very helpful as is removing excess parentheses and nesting makes the query easier to parse.

Your query is just:

WHERE
    [userId] NOT IN @excludeUsers
    AND 
    ([firstname] LIKE @nameFilter OR [surname] LIKE @nameFilter)
    AND
    ([jobTitle] LIKE @infoFilter OR [mobileNo] LIKE @infoFilter)

This is not necessarily minimal. I don't recall the relative priority of the AND/OR operators off-hand, so I generally prefer using parentheses rather than relying on (possibly failing) memory and make priorities there.

Note: there was a mention that proper formatting was easy in language with multiline string literals, whilst true, any language that support string catenation does the job really.

Query Builders

Query builders can have great advantages over raw queries. One important advantage is checking the absence of spelling mistake at compile-time: catching both joTitle and infoFllter.

This, however, requires a much more elaborate query builder than what you have here:

  • You need your SQL model to be embedded into the application as a language construct.
  • You need a way to represent bindings.

On the other hand, there is the issue that query builders are prone to balloon up quite quickly -- I know, I built a few -- simply because the SQL language is pretty complex. And the SQL model suddenly becomes quite complicated to manipulate (efficiently) when users start using WITH or JOIN.

3
  • 1
    "One important advantage is checking the absence of spelling mistake at compile-time" - I wouldn't overdo this as being an important advantage. An SQL builder can still incorporate spelling mistakes which will only be discovered at runtime, although depending on the exact design, it may reduce the number of string literals which need to be checked for spelling mistakes.
    – Steve
    Apr 26, 2020 at 15:22
  • @Steve: It depends indeed. A schema compatibility check at the start of the application can validate that the schema is indeed matching the database -- instantly validating that all table/column names used in building queries are correct, even dynamic ones. Apr 26, 2020 at 16:14
  • 1
    You can go a long way towards avoiding typos by making all table and field names be constant strings. $"Select {NameField} from {EmployeeTable} where {ActiveField} = 1". May 1, 2020 at 3:33
0

This whole problem arises because we accept SQL in string literals in the host language (C#). The person who helpfully formatted your sql statement conveniently stripped all the C#, but how then are you going to run the query?

My answer to this is QueryFirst, a visual studio extension. You write your SQL in a real environment with syntax checking and intellisense. Then, every time you save, we generate the C# wrapper that lets you call your query. Your queries are continuously validated against the DB, without you having to run your app. It is DRY - you never have to repeat a column name, or look up a datatype, or map result fields to an object. And SQL injection becomes impossible. And it's easier than doing it any other way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.