Query API
The Quino data driver includes a powerful query API defined as a .NET API based on the IQuery
type. The examples below show how to use this API to formulate type-safe queries that can be mapped to the database.
Mapped vs. Local
The querying API allows a product to declare its request in a normalized form so that the data driver can map it to an SQL that the underlying database understands.
Optimally, everything that the product requests can be mapped to the database query language and therefore executed on the database rather than in local code.
As we'll see in the example below (and in the Expressions chapter, it is possible to include query components that cannot be mapped to the database.
In these cases, the data driver maps what it can and then uses .NET Linq to apply what could not be mapped locally in the product itself. See Performance implications for more information.
Creating a query
The following example shows the easiest way to create a query:
var query = Session.CreateQuery<Person>();
The example above uses a generated class to select the type of object on which to base the query, but a product can also use several overloads, as in the following example, which creates the same query as above, but passing an IMetaClass
instead.
var query = Session.CreateQuery(PunchclockModel.Person.Metadata);
While the two queries above address all entities of a given type, the following example creates a query for the time-entries of a single person
.
var query = Session.CreateQuery(person, PunchclockModel.Person.TimeEntries);
All of the queries above can be used with other APIs, like GetList()
, Delete()
, or CreateCursor()
.
Lists
In the example below, we obtain a list for the simple query defined above.
GetList()
returns an IDataList<T>
, which has its own query. Instead of creating a query first, as above, a product can create the list directly and manipulate the query afterward.
There is also a fluent API that lets you adjust the query while creating the list.
Cursors
By definition, a list loads all data into memory at once. To avoid this, a product can use a cursor. A cursor returns an IDataCursor<T>
, which is an IEnumerable<T>
instead of an IDataList<T>
(which extends IList<T>
).
There is also a fluent API that adjusts the query while creating the cursor.
Count
A product can obtain the count matching a query.
There is also a fluent API that adjusts the query before getting the count.
Restrictions
At its core, a restriction is an IExpression
. As we already saw in the examples above, a product generally uses a rich API of extension methods to apply restrictions.
The examples below use the generated metadata to reference properties from the model. This way of building expressions is statically typed, so it will survive refactoring. When something changes in the generated code, the referenced constants will no longer be available and the build will fail. See the end of this section for an example that uses the Expressions language.
An IQuery
or IQueryTable
supports many overloads of Where
, such as the following query that finds all people with a certain last name:
The method above is a shortcut that assumes the equality operator. The underlying method accepts any operator, such as the following query that finds all people whose last name starts with "S" or "s":
Most of these Where()
methods wrap a CreateExpression()
method so that a product can compose expressions in more complex ways.
The next example shows how to test for the condition above or another, similar condition.
The Or
method takes either an IEnumerable<IExpression>
or a variable list of IExpression
parameters, so the pattern above can be arbitrarily extended with more expressions.
A product can also create the expression for Or
instead of applying it directly. Here, we nest one Or
inside of another.
The previous query can also be formulated using the Expressions language:
Though this may be clearer to read, unlike the other queries, it will not fail at compile-time if it is no longer valid. Instead, it will fail at run-time if anything changes in the model.
Sorts
Like a restriction, a sort has an IExpression
that describes how to sort and a direction.
The following query orders by last name.
The following query orders by last name, but descending.
Joins
A product can join related data in order to restrict on or sort by data in sub-objects.
For example, the following query sorts people by the company name.
Joins can be nested as well. The following query sorts people by the company zip-code.
A query can also join multiple tables. The following query extends the previous example to return only people with salaries above a certain level.
Join Condition
The JoinCondition
is also available for restriction. The mapper creates the default join condition for the relation between the joined classes. The following example extends the join condition rather than introducing restrictions on the table.
Join Type
TODO Document the various join types and how to use them
All
WhereExists
WhereNotExists
Aliases
TODO Describe how to use aliases to resolve ambiguities when a product joins the same relation multiple times
Distinct
An unjoined query will already be distinct. However, it is possible for a query with joined 1-n relations to return the same object multiple times. To avoid this, a product can make the query "distinct".
Grouping
There is no support for grouping.
A product can set IQuery.Distinct
to get only unique objects.
A product can use either custom SQL or retrieve data with Quino and group locally with .NET Linq.
Aggregation
There is no support for aggregation.
A product can use either custom SQL or retrieve data with Quino and apply aggregations locally with .NET Linq.
Custom SQL
The ADO.NET layer can be accessed via the following methods:
A custom query can be mapped to an existing MetaClass as well:
The SQL query needs to fullfil the following constraints for a successful Quino-object mapping:
The primary key (most of the time the “Id”) of the object needs to be selected. Otherwise subsequent calls to “Save” will fail.
All selected columns need to match the “Name” or “SchemaIdentifier” of a property in the class - otherwise the column is not mapped.
Integration with Metadata
The examples above show how to build queries and obtain data in business logic. A product can use use this API into metadata using IQueryAspects
.
Custom Data
A query has IQuery.CustomData
, a dictionary of data that a product can add to a query and then use from IQueryAspect
objects in the model. Quino's TimelineAspect
uses custom data to determine how to restrict data that rests on a timeline.
TODO Include example of setting and reading custom data (using the timeline)
TODO Describe how to override the query factory to include custom data by default
Performance Implications
While very convenient, even essential, this behavior has performance implications. A product should be aware of how a restriction or sorting could affect the performance of a query.
Performance Restrictions
For example, suppose a product wants to get all active people.
The data driver can map this entire query to SQL, for optimal performance.
Now suppose that we rewrite the restriction as C#, as shown below.
In this case, the driver cannot map any restrictions to the database, so it must get all people from the database and filter them locally. Even if the product asks for a list matching this query, the driver does its best to avoid loading everything into memory at once, but must still create all the people in order to filter them.
With restrictions, it suffices to include another restriction in order to limit the performance impact. For example, the speed of the query in the next example is not really impacted.
In this case, the relatively restrictive restriction to the last name will drastically reduce the amount of data returned by the database.
Sorts Performance
Similarly, if a product uses a sort that cannot be mapped to the database, the driver must retrieve all matching data and perform the sort (and any subsequent sorts) locally.