DA LINQ (.NET)
LINQ, or Language Integrated Query, was a language paradigm introduced by .NET 3.5 Framework. This paradigm allows to write type-safe Query Expressions to query data as part of the primary programming language (C#, Oxygene, or other).
Data Abstract for .NET contains a LINQ query provider for Data Abstract services. This LINQ query provider can be found in the RemObjects.DataAbstract.dll
assembly.
Querying the data
Data query
Accessing the data via the LINQ provider is very simple.
At first an IQueryable<>
data source should be obtained via the Data Adapter's GetTable<>
method. Then in can be used in the usual LINQ query:
var result = from c in dataAdapter.GetTable<Customers>()
where c.Name == "ALFKI"
select new { c.Id, c.Name, c.Phone };
var result := from x in dataAdapter.GetTable<Customer>
where x.CustomerId = 'ALFKI'
select new class(x.CustomerID, x.Name, x.ContactName);
The Customers
class mentioned in the code above is called a Table Definition class (see below).
Code the sample above won't result in an actual request being sent to the Data Abstract server. The request will be sent to the server only when the query is materialized (f.e. when the result.ToList()
method is called).
Materialization of a DA LINQ query can be performed synchronously or asynchronously.
The simplest way of actually loading data is to call .ToList
or .ToArray
method on a DA LINQ query:
var data = query.ToList();
var data := query.ToList();
The code above is performed synchronously. Data Abstract also provides a set of methods that allow to load data in asynchronous manner:
ToListAsync<T>
ToArrayAsync<T>
ToBindingListAsync<T>
var data = await query.ToListAsync();
var data := await query.ToListAsync();
Note: Starting December 2020 Data Abstract builds targeted for .NET Standard and .NET Core (including .NET 5) provide asyncronous data loading methods as part of the main RemObjects.DataAbstract
assembly. Older builds as well as builds targeted for .NET Framework require an additional reference to the RemObjects.DataAbstract.Async
assembly to provide async data loading methods support.
Inner Join query
DA LINQ also allows to query data from more than one table at once when these tables are joined via an INNER JOIN:
var result = from c in dataAdapter.GetTable<Clients>()
join o in dataAdapter.GetTable<Jobs>()
on c.CustomerId equals o.CustomerId
where c.CustomerId == "ALFKI"
orderby o.OrderDateField
select c;
Left Join Query
DA LINQ also allows to query data from more than one table at once when these tables are joined via a LEFT JOIN:
var query = from o in dataAdapter.GetTable<Orders>()
join od in dataAdapter.GetTable<OrderDetails>() on o.Id equals od.OrderId into oj
from od in oj.DefaultIfEmpty()
select new {Order = o, Details = new OrderDetails {Id = od.Id, OrderId = od.OrderId}};
Please note that in case of a LEFT JOIN query DA LINQ requires that queried fields of the joined table to be explicitly listed in the query.
Built-in functions support
DA LINQ supports a small subset of method and properties to be used in the where
clause of a LINQ expression. These calls are converted into SQL statements and are executed directly on the database level.
Supported methods and properties are:
-
String values
Trim()
TrimStart()
TrimEnd()
Substring()
(*)Contains()
StartsWith()
EndsWith()
ToLower()
/ToLowerInvariant()
ToUpper()
/ToUpperInvariant()
Length
-
DateTime values
DateTime.Now
DateTime.Today
Second
Minute
Hour
Day
Month
Year
-
Any nullable values
coalesce
(in Oxygene) /??
(in C#)
Note: When used in the where
part of a DA LINQ expression the .Substring
call is converted into the corresponding SQL function call. A very important difference between SQL implementation of Substring
functions and the .NET one is that in SQL string value positions are 1-based being while in .NET they are 0-based.
DA LINQ processor tries to adjust start position parameter value by adding +1 to better mimic the .NET function behavior.
Eager Data Loading
Note: This is an advanced data query technique. In most cases it won't be required to perform such complex query.
Let's assume there are 3 tables with obvious relationships (Orders
-> OrderDetails
-> Products
) and it is required to load some set of Orders along with their details and products data.
Doing this in the straightforward manner will result in a set of queries (query the orders, then query order details for each of these orders, then query product data...).
This is the infamous n+1 selects
problem that affects all existing ORM (in this case Data Abstract works as an ORM and is also affected by this fundamental problem).
In general Data Abstract doesn’t support navigational properties or eager loading. Still it IS possible to fetch all data is several related tables using singe query, however this will require some additional code.
At first the table definition class fro the Orders
table has to be expanded with an additional property that will contain the loaded details data and a pair of Equals
/GetHashCode
methods. The methods are necessary to allow the Orders
class to be used as a key in the LinqToObjects GroupBy
method.
In general Table Definition classes do not need these methods because the GroupBy
is effectively translated into the GROUP BY SQL clause and is executed by the database server itself. At the same time LinqToObjects operations are performed over objects in local memory, so .NET runtime needs some help to be able to distinguish objects correctly.
Thanks to the partial modifier applied to the Orders
class it can be expanded by additional code placed in a separate file. This way this custom code won't be lost when the TableDefinitions file is regenerated.
public partial class Orders
{
[RemObjects.DataAbstract.Linq.IgnoreProperty]
public IList<OrderDetails> Details { get; set; }
public override bool Equals(object obj)
{
...
}
public override int GetHashCode()
{
...
}
}
This is a relatively simple join over 2 tables:
// Aliases to simplify the query code
// These assignments do not cause any data request operations
var orders = this._dataModule.DataAdapter.GetTable<Orders>();
var orderDetails = this._dataModule.DataAdapter.GetTable<OrderDetails>();
var products = this._dataModule.DataAdapter.GetTable<Products>();
var query = orders
.Where(o => o.OrderStatus != 1)
.Join(orderDetails,
o => o.Id,
od => od.OrderId,
(order, details) => new { Order = order, Details = details })
.ToList()
.Select(g => g)
.GroupBy(g => g.Order)
.Select(g =>
{
g.Key.Details = g.Select(r => r.Details).ToList();
return g.Key;
});
What happens here:
- A WHERE condition is defined. It is better to apply conditions earlier as this will reduce the server -> client traffic and the database workload.
- JOIN between the Orders and OrderDetails tables is defined.
.ToList()
call is issued. At this point the query will be sent to the Data Abstract server and executed. The result is a denormalized join result between the Orders and OrderDetails tables. All Linq operators below are performed over local in-memory objects, not over the database rows..GropyBy
operation is performed. This is the point where earlier definedEquals
/GetHashCode
methods are used.- The
.Select
operation is performed. This operation takes the LINQ group object and converts it into aOrders
object with initialized Details property.
The query below is a bit more complex because it involves more than one join:
// Aliases to simplify the query code
// These assignments do not cause any data request operations
var orders = this._dataModule.DataAdapter.GetTable<Orders>();
var orderDetails = this._dataModule.DataAdapter.GetTable<OrderDetails>();
var products = this._dataModule.DataAdapter.GetTable<Products>();
var query = orderDetails
.Join(products,
od => od.ProductId,
p => p.Id,
(od, p) => new { Detail = od, Product = p })
.Join(orders,
od => od.Detail.OrderId,
o => o.Id,
(detail, order) => new { Order = order, Detail = detail })
.Where(o => o.Order.OrderStatus != 128)
.ToList()
.Select(g => g)
.GroupBy(g => g.Order)
.Select(g => new { Order = g.Key, Details = g.Select(r => r.Detail).ToList() });
Still this query uses the same approach:
- Fetch denormalized data
- Normalize it back client-side
Updating the data
Changes tracking
Objects loaded via DA LINQ can be changed and passed beck to the server to trigger the database update.
There are 2 kinds of changes tracking - automatic and manual ones.
Automatic one is performed when 2 conditions are met:
DataAdapter.UseBindableClass
property is set to true- The result of the DA LINQ query is a value(s) of one the TableDefinition classes (ie when result type is not an anonymous class and the TableNameAttribute has been applied to it)
In this case the resulting data has automatic tracking changes enabled.
Note: If the data requested won’t be changed or these changes shouldn’t be persisted, consider setting DataAdapter.UseBindableClass
to false to save CPU and memory
Manual changes tracking is performed via the NewRow
/ UpdateRow
/ DeleteRow
methods:
var customer = new Customers();
// Set object properties
// ...
// Set object properties
// Insert data
dataAdapter.InsertRow(customer);
// Update data
customer.Name := "XXX";
dataAdapter.UpdateRow(customer);
// Delete data
dataAdapter.DeleteRow(customer);
The Customers
class mentioned in the code above is called a Table Definition class (see below).
Applying Changes
Changes made to the data objects are not applied immediately to the database, regardless of the changes tracking mode used (manual or automatic one).
An explicit method call is required to trigged the data update process:
dataAdapter.ApplyChanges();
dataAdapter.ApplyChanges();
An asynchronous version of this method is available as well:
await dataAdapter.ApplyChangesAsync();
await dataAdapter.ApplyChangesAsync();
BeginUpdate
/ EndUpdate
/ CancelUpdate
These 3 methods are generated for each TableDefinition class:
BeginUpdate
creates an internal clone of the current objectEndUpdate
provides that internal clone to the Update method of the DataAdapter along with the current object valuesCancelUpdate
clears that internal clone object
It is not required to call these methods explicitly, but they provide a great benefit as they allow to send reduced deltas for the data changes made.
To successfully compose a delta for an object client code requires current values stored in the object fields. The downside of this approach is that the resulting delta will contain values for all object fields. Even more, it won’t be possible to reduce it to send only changed values to the server, because there is no information about old values.
Luckily the BeginUpdate
/EndUpdate
method pair does provide data required to compose a reduced delta because it provides old object values.
So the general rule is:
- For reasonably-sized objects use non-reduced deltas and automatic change tracking until it becomes a bottleneck, then switch to explicit
BeginUpdate
/EndUpdate
method calls. - For objects that contain significant amount of data (like binary blobs) consider to always use the
BeginUpdate
/EndUpdate
methods.
Table definition classes
DA LINQ uses auto generated plain simple classes to materialize data fetched from the Data Abstract server.
These auto generated classes are called Table Definition classes and represent Schema tables exposed by Data Abstract server. All Table Definition classes are marked as public partial so they can be extended in a separate code file(s) when needed.
Table Definition classes are generated during creating new Data Abstract client project by the New Project Wizard. Table Definition classes have to be regenerated once the server schema changes. This can be done either via context menu available for the TableDefinitions project item or via corresponding Visual Studio menu item. In Visual Studio 2019 this menu item can be found under Extensions
-> Remoting SDK & Data Abstract
, while in Visual Studio 2017 it is located under Tools
-> Remoting SDK & Data Abstract
.