Overview
SqlSet is a class designed to build and optionally execute SQL queries. It’s a LINQ-inspired query API that uses SQL instead of lambda expressions. The main design goals are:
- Execute the most common queries without having to build the complete SQL yourself.
- Abstract-away SQL dialect differences (e.g. pagination).
Let’s take a look at the first example:
var db = new Database("<connection string>", "<provider invariant name>");
SqlSet<Product> products = db.From<Product>("Products");
To get an instance call the Database.From method specifying the table name and the type you want to map to.
AsEnumerable, ToArray, ToList
IEnumerable<Product> r1 = products.AsEnumerable();
Product[] r2 = products.ToArray();
List<Product> r3 = products.ToList();
Because SqlSet is a connected query, you can directly call methods like AsEnumerable, ToArray or ToList to get back results. Note that in the above code only two queries are executed, because AsEnumerable is deferred-executed.
First, FirstOrDefault, Single, SingleOrDefault
Product p1 = products.First();
Product p2 = products.FirstOrDefault();
Product p3 = products.Single();
Product p4 = products.SingleOrDefault();
Call these methods to get a single result. If you know LINQ you should know the difference between them :)
All, Any
bool haveProducts = products.Any();
bool allHavePrice = products.All("NOT UnitPrice IS NULL");
…which executes:
SELECT (CASE WHEN EXISTS (
SELECT *
FROM Products) THEN 1 ELSE 0 END)
SELECT (CASE WHEN EXISTS (
SELECT *
FROM Products
WHERE NOT (NOT UnitPrice IS NULL)) THEN 1 ELSE 0 END)
Count, LongCount
int count1 = products.Count();
long count2 = products.LongCount();
Both execute the same query:
SELECT COUNT(*) FROM Products
SELECT COUNT(*) FROM Products
Where, OrderBy, Skip, Take, Select
These methods return a new SqlSet instance. Unlike SqlBuilder, SqlSet is immutable. Also unlike SqlBuilder, you can call methods in any order. For example:
Product[] topFiveWithLeastStock = products
.OrderBy("UnitsInStock")
.Take(5)
.ToArray();
…which executes:
…but if we call Take first and then OrderBy:
Product[] firstFiveOrderedByStock = products
.Take(5)
.OrderBy("UnitsInStock")
.ToArray();
…it executes:
Again, if you know LINQ this shouldn’t come as a surprise.
For more about Select, see Projections.
SqlSet vs. SqlSet<TResult>
So far we’ve been using the generic SqlSet<TResult>. There is also a non-generic SqlSet (inherited by SqlSet<TResult>) which is useful when you don’t know the type you are mapping to until runtime.
SqlSet<Product> products1 = db.From<Product>("Products");
SqlSet products2 = db.From("Products", typeof(Product));
A SqlSet can be promoted to a SqlSql<TResult> using the Cast method:
// The type parameter must be the same type used when products2 was created
SqlSet<Product> products3 = products2.Cast<Product>();
Typed vs. Untyped
You can also completely omit the result type:
// Untyped set
SqlSet products = db.From("Products");
When you execute queries with an untyped set you get dynamic objects:
dynamic p1 = products.First();
string name = p1.ProductName;
Untyped sets are also useful when you don’t need to execute and just want to build a query and get back a SqlBuilder representation of it.
SqlBuilder query = products.GetDefiningQuery();
Again, you can promote an untyped set to a typed set using the Cast method.
Projections
To change the result type of a set you must call the Select method.
SqlSet<Product> products = db.From<Product>("Products");
SqlSet<ProductStock> productStocks = products.Select<ProductStock>("ProductID, ProductName, UnitsInStock");
Warning
It’s not recommended to project onto an annotated entity type. Updating a partially loaded entity may cause data loss.
You can also provide a custom mapping function:
SqlSet<string> productNames = products.Select(r => r.GetString(0), "ProductName");
The function takes an IDataRecord and can return anything you want, which means you can also use SqlSet for simple values like string
or int
.
If you provide no result type or mapping function it turns into an untyped set.
SqlSet productNames = products.Select("ProductName");
Complex queries
SqlSet doesn’t support joins or grouping. After all, the goal is not to completely abstract the SQL language, but to provide a simple API for the most common queries. For complex queries use SqlBuilder instead, then you can pass the query to the From method and continue building using SqlSet:
public IEnumerable<Product> GetProductsByCategory(int categoryId, int skip = 0, int take = 20) {
var query = SQL
.SELECT("p.ProductID, p.ProductName, p.CategoryID")
._("c.CategoryID AS Category$CategoryID, c.CategoryName AS Category$CategoryName")
.FROM("Products p")
.LEFT_JOIN("Categories c ON p.CategoryID = c.CategoryID");
return this.db
.From<Product>(query)
.Where("CategoryID = {0}", categoryId)
.OrderBy("ProductID DESC")
.Skip(skip)
.Take(take)
.AsEnumerable();
}
…which executes:
Include
The previous example used query mapping syntax to eagerly-load the Category for each Product. The same can be acomplished using the Include method:
public IEnumerable<Product> GetProductsByCategory(int categoryId, int skip = 0, int take = 20) {
return this.db
.Table<Product>()
.Include("Category")
.Where("CategoryID = {0}", categoryId)
.OrderBy("ProductID DESC")
.Skip(skip)
.Take(take)
.AsEnumerable();
}
…which executes:
Note that I called the Database.Table method instead of Database.From. Although using Database.From would also work, with Database.Table you don’t need to specify the table name. Both Database.Table and Include only work for annotated types.
Include can be called many times:
return this.db
.Table<Product>()
.Include("Category")
.Include("Supplier")
.AsEnumerable();
And you can use a deep path:
return this.db
.Table<EmployeeTerritory>()
.Include("Territory.Region")
.AsEnumerable();
And load one-to-many associations:
return this.db
.Table<Order>()
.Include("OrderDetails.Product")
.AsEnumerable();
Find
Find is another method that only works for annotated types. You can use it to get a single result (or null) that matches the given primary key value.
Order order = db
.Table<Order>()
.Include("OrderDetails.Product")
.Find(orderId);
Conclusions
Having the power to write your own SQL is great. Not having to write the same simple queries over and over is even better. SqlSet helps you compose and reuse SQL in a database independent way. While SqlBuilder is meant for private use, SqlSet can be shared, allowing the caller to further refine the query. LINQ lovers should feel right at home with SqlSet.