Tip
All examples used are valid against the Microsoft Northwind Sample Database You can run the resulting SQL from the examples against Northwind in order to test and play with the SqlObjects library.
Advanced Concepts¶
Overview¶
If you’ve read through any significant portions of this documentation then you’ve probably noticed by now that this library is backed by a full object model. The Fluent API doesn’t simply execute functions that return strings. Rather the functions create stateful objects each of which produces the SQL for their respective SQL fragments. Because there is a full object model underneath, it opens the door for all sorts of possibilities.
When crafting a SQL statement using the Fluent API, you must call the .Build()
method to return the
SQL code as a string. If you do not execute the .Build()
method then the Fluent API methods return
objects that implement the SqlStatement
base class.
The concrete type of the SqlStatement
object
returned by the Fluent API methods will differ depending on the method that was called but it will represent
some SQL fragment. The SQL fragment represented by the SqlStatement
object may be a fragment in the
middle of the SQL statement being built or it may be the SQL object at the top.
The SQL object model is in the form of a tree, where the SqLStatement
base class is a self-referencing
type containing a collection of child SqlStatement
objects (children). Rendering the SQL for any fragment
will automatically render the SQL for all children, children of those children, etc. recursively down the tree.
The .Build()
method essentially renders the SQL from the root object in order to produce the entire SQL
statement.
To illustrate, the example below shows that sql1
and sql2
are equivalent.
var sqlObj = TSQL
.SELECT()
.COLUMN("ProductID")
.COLUMN("ProductName")
.FROM("Products")
;
var sql1 = sqlObj.Build();
var sql2 = sqlObj.Root.ToString();
Console.WriteLine(sql1 == sql2);
Output:
True
Once you understand that the Fluent API is building a tree of SqlStatement
objects, then you can take
advantage of the model to do all sorts of cool things if you want to. The sections below demonstrate some
of the things you can do with the SQL object model produced with this library.
Sharing Columns¶
Consider as an example the Employees
table in the Microsoft Northwind Database, which has been the
subject database of all the examples in this documentation. This table has the following columns.
EmployeeID
LastName
FirstName
Title
TitleOfCourtesy
BirthDate
HireDate
Address
City
Region
PostalCode
Country
HomePhone
Extension
Photo
Notes
ReportsTo
PhotoPath
In addition to that, imagine that some day you may have to add a column to that table. This causes some maintenance overhead in the application code if you aren’t using some SQL code builder to help you manage your queries.
There are several ways you can make this easier in your application code but let’s examine the most basic, which is to share a collection of strings that your various SQL queries can utilize as demonstrated below.
var table = "Employees";
// Somewhere you can create your collection of column name strings.
// You can put it in a base class, inject it with your IoC container ... whatever.
var columns = new List<string>
{
"EmployeeID",
"LastName",
"FirstName",
"Title ",
"TitleOfCourtesy",
"BirthDate",
"HireDate",
"Address",
"City",
"Region",
"PostalCode",
"Country",
"HomePhone",
"Extension",
"Photo",
"Notes",
"ReportsTo",
"PhotoPath"
};
// Utilize it in your different SQL statements that require it
var insert = TSQL
.INSERT().INTO(table)
.COLUMNS(columns)
.VALUES(columns)
;
var selectAll = TSQL
.SELECT()
.COLUMNS(columns)
.FROM(table)
;
var selectById = TSQL
.SELECT()
.COLUMNS(columns)
.FROM(table)
.WHERE("EmployeeID").IsEqualTo("@EmployeeID")
;
var update = TSQL
.UPDATE(table).SET()
.COLUMNS(columns)
.WHERE("EmployeeID").IsEqualTo("@EmployeeID")
;
It is easy enough to utilize a collection of strings for SELECT
statements in regular old
concatenated or interpolated SQL strings but it isn’t as easy to produce the parameterized
queries that the .INSERT()
and .UPDATE()
suite of Fluent API methods gives you.
Furthermore, the collection of columns can be TSQLColumn
objects, which offer the ability
to include data type information.
Lastly, the .COLUMNS()
method offers an overload to apply a multipart identifier
to all the columns in the collection. That way, anywhere you need to JOIN to another table you
have an easy way to disambiguate the column references.
You can imagine ways to take this further by, for example, deriving the column names from an entity model class using reflection or by pulling the column names directly from the database using a schema metadata query, both of which can be executed on startup and placed in a singleton so the performance hit is taken only one time.
In short, the ability to manage the columns in a table in one place is powerful making the application code far more maintainable while also promoting the DRY principle.
Adding To & Merging SQL Objects¶
Obtaining a SQL object model also provides the opportunity to add to an already-existing SQL object graph or combine SQL fragments that were independently built. You simply have to identify the fragment you want to attach another fragment to and thereby add to or combine multiple object graphs to produce a new single SQL statement.
Basic Add
Below is an example where we use a base SQL select statement for pulling all records but then utilize it to produce a SQL statement for retrieving a single record by adding to the object graph that already exists.
var table = "Products";
var columns = new List<string> { "ProductID", "ProductName" };
var baseSelect = TSQL
.SELECT()
.COLUMNS(columns)
.FROM(table)
;
var selectAll = baseSelect
.Build();
var selectOne = baseSelect
.WHERE("ProductID").IsEqualTo("@ProductID")
.Build();
Console.WriteLine(selectAll);
Console.WriteLine(selectOne);
Output:
SELECT
[ProductID],
[ProductName]
FROM [Products]
SELECT
[ProductID],
[ProductName]
FROM [Products]
WHERE 1=1
AND [ProductID] = @ProductID
Adding to Arbitrary Fragments
The trouble you might run into is that you may not obtain a reference to the SQL fragment that you are trying to add to. When this happens you have a couple of options.
Tag the target fragment with an ID using
WithId()
so that you can use theFindById()
method to obtain a reference to that fragment later.var productCols = new List<string> { "ProductID", "ProductName" }; var categoryCols = new List<string> { "CategoryID", "CategoryName" }; var sqlObj = TSQL // Tag the SELECT object with an ID to lookup later .SELECT().WithId("abc123") .COLUMNS("p", productCols) .FROM("dbo", "Products", "p") ; // See the type of object returned at this point Console.WriteLine("sqlObj: " + sqlObj.GetType().Name); Console.WriteLine(""); // Grab a reference to the SELECT object using the ID. // MUST find starting at a node above the target object. // Using Root to guarantee the lookup var selectFragment = sqlObj.Root.FindById("abc123") as SqlStatement; // Confirm that we looked up the desired SELECT object Console.WriteLine("selectFragment: " + selectFragment.GetType().Name); Console.WriteLine(""); // Now we can do extra stuff with the original SQL object graph selectFragment .COLUMNS("c", categoryCols) .INNERJOIN("dbo", "Categories", "c").ON("c", "CategoryID").IsEqualTo("p", "CategoryID"); // Build and output the whole thing var sql = selectFragment.Build(); Console.WriteLine(sql);
Output:
sqlObj: TSQLFrom
selectFragment: TSQLSelect
SELECT [p].[ProductID], [p].[ProductName], [c].[CategoryID], [c].[CategoryName] FROM [dbo].[Products] p INNER JOIN [dbo].[Categories] c ON [c].[CategoryID] = [p].[CategoryID]
Keep in mind that when looking up by
Id
you must start the lookup from a fragment that is above the fragment you’re looking for. If you start the find from theRoot
object then you are guaranteed to get a successful lookup assuming you provide anId
that exists in the object graph. This is because the.FindById()
method recursively walks the tree of SQL objects downward starting from the object you call the method upon BUT it will not go UP the tree to check the ancestors.You can manually walk the tree of
SqlStatement
objects checking each node until you find the one you’re looking for and obtain a reference to the desired SQL fragment that way.Below is the same example except that instead of using the
.FindById()
method, you can walk the tree manually to accomplish the same thing. For example, you may need to do this if the fragment you’re looking for is not tagged with an ID.var productCols = new List<string> { "ProductID", "ProductName" }; var categoryCols = new List<string> { "CategoryID", "CategoryName" }; var sqlObj = TSQL // This SELECT object is not tagged with an identifier .SELECT() .COLUMNS("p", productCols) .FROM("dbo", "Products", "p") ; // See the type of object returned at this point Console.WriteLine("sqlObj: " + sqlObj.GetType().Name); Console.WriteLine(""); SqlStatement selectFragment = null; // ProcessTree recurses the tree for you. Each object s in the lambda // is a SqlStatement node in the hierarchical object graph. You just // check for what you're looking for, in this case a SELECT statement. sqlObj.Root.ProcessTree(s => { var sqlFragment = s as SqlStatement; if (sqlFragment.IsSelect) selectFragment = sqlFragment; }); // Confirm that we looked up the desired SELECT object Console.WriteLine("selectFragment: " + selectFragment.GetType().Name); Console.WriteLine(""); // Now we can do extra stuff with the original SQL object graph selectFragment .COLUMNS("c", categoryCols) .INNERJOIN("dbo", "Categories", "c").ON("c", "CategoryID").IsEqualTo("p", "CategoryID"); // Build and output the whole thing var sql = selectFragment.Build(); Console.WriteLine(sql);
Output:
sqlObj: TSQLFrom
selectFragment: TSQLSelect
SELECT [p].[ProductID], [p].[ProductName], [c].[CategoryID], [c].[CategoryName] FROM [dbo].[Products] p INNER JOIN [dbo].[Categories] c ON [c].[CategoryID] = [p].[CategoryID]
Merging SQL Object Graphs Together
In some cases where you have two independently created statements you may need to combine them.
To make this easier, you can call the Merge()
method to combine two SQL object graphs together.
This is not a simple concatenation of SQL strings. This method will do the job of adding the SQL
object fragments from the second object graph under the SQL object fragments of the first object
graph.
Consider the example below where two independently created SQL statements are combined to produce a multi-data set result. We can combine the two ro
var selectA = TSQL
.SELECT()
.STAR()
.FROM("Products")
.Terminate()
;
var selectB = TSQL
.SELECT()
.STAR()
.FROM("Categories")
;
var sql = selectA
.Merge(selectB)
.Build();
Console.WriteLine(sql);
Output:
SELECT
*
FROM [Products];
SELECT
*
FROM [Categories]
An important thing to consider in the example above is the use of the Terminate()
method, which is explained in detail below.
Terminate()¶
The Terminate()
method simply ensures that the immediately preceding SQL fragment
is output with a terminating semicolon ;
character. You may need to use this if
you have a complex query or script that must or should be delineated by an explicitly
terminated statement.
The example below, similar to the example above where they are independently built and merged, shows two SELECT statements meant to return a multi-data-set result, which can be crafted in a single fluent statement.
var sql = TSQL
.SELECT()
.STAR()
.FROM("Products")
.Terminate()
.SELECT()
.STAR()
.FROM("Categories")
.Build()
;
Console.WriteLine(sql);
Output:
SELECT
*
FROM [Products];
SELECT
*
FROM [Categories]
You can see that the first SELECT statement is terminated with the semicolon character ;
,
which was placed there using the Terminate()
method.
WithId() / FindById()¶
Any SQL fragment object can be tagged with an identifier using the WithId()
method if you
plan to obtain a reference to that particular fragment later. It can then be used in
combination with the FindById()
method when the time comes to find it and use it.
Under the hood, the WithId()
method populates the Id
property of the immediately preceding
SQL fragment object with the value provided in the method call.
var productCols = new List<string> { "ProductID", "ProductName" };
var sqlObj = TSQL
.SELECT()
.COLUMNS(productCols)
.FROM("Products")
.WHERE("CategoryID").WithId("wc") // Tag WHERE with Id = "wc"
.IN(1, 2, 3);
;
// Now grab a reference to the WHERE clause
// It's good practice to start at the Root object
var whereClause = sqlObj.Root.FindById("wc") as SqlStatement;
// Confirm we have the WHERE clause object
Console.WriteLine(whereClause.GetType().Name);
Console.WriteLine();
var sql = whereClause
.OR().COLUMN("ProductID").IsEqualTo(99)
.Build();
Console.WriteLine(sql);
Output:
TSQLWhereClause
SELECT
[ProductID],
[ProductName]
FROM [Products]
WHERE 1=1
AND 'CategoryID' IN (1,2,3)
OR [ProductID] = 99
Keep in mind that when looking up by Id
you must start the lookup from a fragment that is above the fragment
you’re looking for. If you start the find from the Root
object then you are guaranteed to get a successful
lookup assuming you provide an Id
that exists in the object graph. This is because the FindById()
method
recursively walks the tree of SQL objects downward starting from the object you call the method upon BUT it
will not go UP the tree to check the ancestors.
WithName()¶
The WithName()
method functions exactly like the Withid()
method except that it is
simply for a different purpose. Tagging a fragment with an identifier is typically meant
for numeric values or GUIDs whereas tagging a fragment with a name is for labeling the
fragment with something human readable.
However, the name is strictly metadata where the major difference is that there is NO corresponding
FindByName()
method. You could use the Name
property of the SQL object when Walking the Tree to
print the underlying Name
property or use it to find an object but typically if you want to find a SQL
fragment it is easier to tag it with an Id
and then use the FindById()
method.
You may find it most useful if you are building a UI that utilizes the underlying object model directly instead of the Fluent API and you need to label the objects for display in a form. Another use case is to plant names on the objects so that they make sense when looking at the serialized JSON of the SQL object graph.
WithDoc()¶
The WithDoc()
is most like the WithName()
method where the WithDoc()
method populates the
object’s Description
property with the value provided in the method call.
The purpose of this is to add documentation about the SQL query in cases where the SQL object may
need to be examined in JSON or conceivably, for example, in a UI that is designed to serve as a SQL
object explorer. You can imagine hovering the mouse over a SQL fragment that is wired up to
display the Description
text in a pop-over.
It’s basically metadata that lives with the object graph but is completely ignored when producing the actual SQL code.
You can see how it works by adding doc to a SQL fragment and then using Serialization to render the object graph into JSON. An exercise I leave to the curious reader.
Walking the Tree¶
Though it may come as no surprise, the KnightMoves.SQLObjects library utilizes the KnightMoves.Hierarchical library at its core. Examining the documentation for the KnightMoves.Hierarchical library will give you insight into how this library functions.
The SQL object graph that you build with the Fluent API basically creates a tree of objects that implement
the SqlStatement
base class. Once you know that it is a tree built with the KnightMoves.Hierarchical
library, you can see that the underlying SQL objects are tree nodes with all of the methods and properties
that come with a tree node. As such, you can take advantage of the features offered by the KnightMoves.Hierarchical
library.
In particular, this object structure could be useful when you need to process the SQL objects recursively or in other words … when you need to “walk the tree”. The KnightMoves.Hierarchical library offers three methods for doing so, listed below.
ProcessTree()
ProcessChildren()
ProcessAncestors()
Each of these is described below.
For each of the examples consider the following SQL object graph built with the Fluent API below.
var sqlObj = TSQL
.SELECT()
.COLUMN("c", "CategoryName")
.COUNT("p", "ProductID").AS("TotalProducts").WithId("abc123")
.AVG("p", "UnitPrice").AS("AvgPrice")
.FROM("dbo", "Products", "p")
.INNERJOIN("dbo", "Categories", "c").ON("c", "CategoryID").IsEqualTo("p", "CategoryID")
.GROUPBY()
.COLUMN("c", "CategoryName")
;
Notice that we tagged the COUNT()
method with and identifier, which we will make use of later.
So with that object graph above, you can walk the tree up or down using the relevant methods below.
ProcessTree()
Use the ProcessTree()
method to walk the tree of objects down from AND INCLUDING the object that
the method is called from.
sqlObj.Root.ProcessTree(node =>
{
Console.WriteLine(node.IndentString + node.GetType().Name);
});
In the code above, notice that we reference the Root
object. The Root
object is referenced by
every node in the entire tree so you can get to the top of the tree from anywhere.
The ProcessTree()
method accepts a lambda where the parameter (here we called it node
) is the
object in the tree passed to your lambda function as it is recursively walking down the tree.
Also notice that we utilize the node.IndentString
property to display the values with indentation
so it is easier to see the structure of the tree. In fact, the IndentString
property is used to format
the SQL output when calling .Build()
as you’ve seen throughout this documentation.
Here’s the output of the ProcessTree()
logic above.
TSQLScript
TSQLSelect
TSQLColumn
TSQLFuncCount
TSQLFuncAvg
TSQLFrom
TSQLInnerJoin
TSQLBasicCondition
TSQLColumn
TSQLColumn
TSQLGroupBy
TSQLColumn
This reveals the types of objects that are being created by the Fluent API as you call its methods to
build your SQL object graph. If you cast the node
object as SqlStatement
or its concrete type
then you can access their relevant properties or call the SQL()
method to pull the SQL output from
them individually.
The most common use case for this is if you need to find an object so you can reference it for the purpose
of building more SQL under it using the Fluent API, assuming you cannot use the FindById()
method.
Once you have a reference to it and cast it to SqlStatement
, then you can call the Fluent API methods
upon it to continue to insert SQL statements at that point. The other objects below it will still remain.
Warning
Manually obtaining references to SQL fragments is not the primary use case for this library so your mileage may vary. What you do with it may or may not work as intended.
ProcessChildren()
The ProcessChildren()
method functions just like the ProcessTree()
method except that it will begin
its processing starting with the child nodes. In the case of the Root
object in this example, which is
a TSQLScript
object, then processing will not include itself (i.e. the TSQLScript
object).
sqlObj.Root.ProcessChildren(node =>
{
Console.WriteLine(node.IndentString + node.GetType().Name);
});
And here’s the output:
TSQLSelect
TSQLColumn
TSQLFuncCount
TSQLFuncAvg
TSQLFrom
TSQLInnerJoin
TSQLBasicCondition
TSQLColumn
TSQLColumn
TSQLGroupBy
TSQLColumn
You can see that the TSQLScript
object was not included. Use this if you do not want to include the
current object in the recursion - you only want it to recurse the objects below the current object.
ProcessAncestors()
The ProcessAncestors()
is a bit more robust. Full documentation of this method is provided in the
KnightMoves.Hierarchical documentation.
In the example below, we obtain a reference to the SQL COUNT() function that we tagged with an identifier
so that once we have a reference to a SQL fragment object in the middle of the tree somewhere, we can then
walk UP the tree using the ProcessAncestors()
method.
var countFunction = sqlObj.Root.FindById("abc123");
countFunction.ProcessAncestors(
node =>
{
Console.WriteLine(node.IndentString + node.GetType().Name);
},
countFunction
);
Since we are going up the tree and printing the type names with indentation, it has the effect of flipping the tree upside down from the point we started processing.
Here is the output:
TSQLFuncCount
TSQLSelect
TSQLScript
The first argument to the ProcessAncestors()
method is a lambda that functions just like the ProcessTree()
and ProcessChildren()
methods except that instead of recursing down the tree it is recusing up the tree for you.
The second argument to the ProcessAncestors()
method is the object that you want to start from, in this case
the reference to the SQL COUNT() function. The process is inclusive of the starting point as you can see in the
output that it begins with printing the name of the TSQLFuncCount
class.
Tip
Keep in mind that when recursing up the tree, it is going from parent to parent and siblings are ignored. This means that the inverted tree output only goes so far. In the example above you see that the other SELECT list items are not displayed because they are siblings of the COUNT() function.
Whether you need to go up or down the tree for whatever purpose you desire, you can see that using the methods above should provide you with the tools to do pretty much anything you want.
Checking Fragment Types¶
If you take advantage of the underlying SQL object model in any way, you are likely find it useful to check what
type of SQL fragment you are dealing with. Since each SQL fragment object implements the SqlStatement
base
class, they will all provide the following boolean properties that will tell you what sort of object you’re
dealing with, without having to know the name of the actual C# object type.
Property |
Remarks |
---|---|
IsDelete |
|
IsFrom |
|
IsGroupBy |
|
IsInsert |
|
IsJoin |
Any |
IsRightJoin |
|
IsLeftJoin |
|
IsInnerJoin |
|
IsScript |
True if the object is an ad hoc script or the root script |
IsSelect |
|
IsUnion |
|
IsUpdate |
|
IsWhereClause |
|
IsBasicCondition |
Condition as |
IsBetween |
|
IsCalculation |
Calculation as |
IsColumn |
A column specification |
IsComment |
An object containing a SQL comment |
IsCondition |
Any condition |
IsConditionGroup |
A scope containing coditions output between |
IsDistinct |
|
IsFunction |
Any type of function call |
IsFunctionAbs |
|
IsFunctionAvg |
|
IsFunctionCeiling |
|
IsFunctionCount |
|
IsFunctionDateAdd |
|
IsFunctionDateDiff |
|
IsFunctionDateName |
|
IsFunctionDatePart |
|
IsFunctionDay |
|
IsFunctionFloor |
|
IsFunctionGetDate |
|
IsFunctionMax |
|
IsFunctionMin |
|
IsFunctionMonth |
|
IsFunctionParameter |
A function parameter object |
IsFunctionSum |
|
IsFunctionYear |
|
IsHaving |
|
IsInList |
|
IsLike |
|
IsLiteral |
An object containing an ad hoc literal |
IsOrderBy |
|
IsOrderByExpression |
An expression under and |
IsQueryExpression |
Any SQL fragment that constitutes a valid query expression |
IsSubQuery |
A subquery scope denoted by |
IsTop |
|
These boolean properties can be utilized to identify the type of object you are seeking in your logic while processing the SQL fragments in the tree.