Skip to content

Release Notes 0.8

Dave Tomcik edited this page Mar 31, 2016 · 2 revisions

Tortuga Chain Beta 1 (version 0.8)

I would like to present the first beta release of Tortuga Chain, a database-aware ORM with a fluent API.

Database Aware

Most ORMs for .NET are database neutral. This means that they either require manually written SQL or they default to the lowest common denominator.

Chain is different in that it embraces the capabilities of the database. For example, if your database supports OUTPUT clauses or a real UPSERT, we’ll use them. If it doesn’t, we’ll generate the necessary SQL to simulate it.

The secret to making this work is simple. Instead of assuming your classes are an exact match for the database schema, Chain asks. It reaches out to the database, grabs the schema, and uses it to generate the right SQL every time.

Fluent API

Unlike other ORMs, Chain truly is a fluent API. By using a thread-safe data source that is shared application-wide, most operations can be accomplished in a single statement. No more messing around with using statements and connections. No more forgetting to call Save on a database context. (Unless you are in a transaction, sorry.)

Tables are not Classes

A common mistake in ORM design is to assume that there is a one-to-one mapping between tables and classes. This leads to verbose syntax when you only want a handful of columns instead of a full entity. It gets even worse when you try to perform an update with the lightweight class, as you have to perform an unnecessary database read, then manually copy the fields of interest from the lightweight class to the entity before actually updating the record. This leads to error prone code, as invariable a column is missed.

Since Chain doesn’t assume a one-to-one mapping between tables and classes, you can populate any object from any table or view so long as the column and property names match.

dataSource.From("Customer”).AsCollection<Customer>().Execute();
dataSource.From("Customer”).AsCollection<CustomerNameAndPhone>().Execute();

Inserts and updates work the same way. You can even update multiple tables from the same object. For example, say you have a 0 to 1 mapping between the Person and Customer table. This may happen when Person contains generic information such as name and address, while Customer has sales data. To update them both, you simply make two calls.

dataSource.Update ("Customer”, customer).Execute();
dataSource.Update("Person”, customer).Execute();

Try doing that with any other ORM.

Intelligent Modeling

Taking this a step further, what happens if you have a column that should be written to only once such as CreatedBy/CreatedDate?

Using the intelligent modeling capabilities of Chain, you no longer have to worry about accidentally overwriting a value. Simple tag your properties with the IgnoreOnInsert and/or IgnoreOnUpdate attributes and Chain takes care of the rest.

Consider this code from Dapper:

using (var connection = …)
{
    var data = connection.Query<Post, User, Post>(sql, (post, user) => { post.Owner = user; return post;});
}

Here is the same code in Chain:

var data = dataSource.Sql(sql).ToCollection<Post>().Execute();

How’s that possible? By simply putting a Decompose attribute on the Post.Owner property, Chain automatically knows to create a User object, populate its values, and assign it to Post.Owner. And you can decompose as many complex properties as you want, nested as deeply as you want.

Performance

Performance is always tricky to measure, so we can’t promise that we’re going to be the fastest in every situation. But in an unofficial run of Frans Bouma's RawDataAccessBencher, we beat everything except hand-coded data readers.

Results per framework. Values are given as: 'mean (standard deviation)'
==============================================================================
Non-change tracking fetches, set fetches (25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader                            : 160.50ms (4.88ms)        Enum: 1.17ms (0.22ms)
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 166.47ms (2.27ms)        Enum: 1.30ms (0.14ms)
Chain v0.7.5927.4601 with Compiled Materializers                     : 168.82ms (5.02ms)        Enum: 1.14ms (0.08ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (normal)                              : 173.59ms (4.34ms)        Enum: 1.12ms (0.18ms)
Raw DbDataReader materializer using object arrays                    : 176.37ms (1.40ms)        Enum: 3.13ms (0.24ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 179.14ms (16.55ms)       Enum: 1.33ms (0.25ms)
PetaPoco Fast v4.0.3                                                 : 184.00ms (3.39ms)        Enum: 1.30ms (0.14ms)
LLBLGen Pro v5.0.0.0 (v5.0.0), Poco typed view with QuerySpec        : 192.11ms (8.95ms)        Enum: 1.35ms (0.16ms)
LLBLGen Pro v5.0.0.0 (v5.0.0), Poco typed view with Linq             : 192.27ms (1.93ms)        Enum: 1.12ms (0.07ms)
PetaPoco v4.0.3                                                      : 211.58ms (16.01ms)       Enum: 1.18ms (0.10ms)
Dapper v1.40.0.0                                                     : 212.84ms (3.96ms)        Enum: 1.33ms (0.19ms)

Non-change tracking individual fetches (100 elements, 25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 0.19ms (0.01ms) per individual fetch
Handcoded materializer using DbDataReader                            : 0.19ms (0.01ms) per individual fetch
Chain v0.7.5927.4601 with Compiled Materializers                     : 0.21ms (0.01ms) per individual fetch
Dapper v1.40.0.0                                                     : 0.21ms (0.02ms) per individual fetch
Raw DbDataReader materializer using object arrays                    : 0.28ms (0.02ms) per individual fetch
Massive using dynamic class                                          : 0.29ms (0.02ms) per individual fetch
Oak.DynamicDb using dynamic Dto class                                : 0.36ms (0.15ms) per individual fetch
ServiceStack OrmLite v4.0.50.0 (v4.0.50.0)                           : 0.41ms (0.15ms) per individual fetch
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 0.41ms (0.03ms) per individual fetch

Raw scores: https://github.com/docevaad/Chain/wiki/RawBencher-Performance---Unofficial

We do this by using CS Script to generate our compiled materializers in the same way you would hand-write data readers.

OOP isn’t everything

We realize that sometimes you really don’t need an object. Sometimes just a list of strings or integers is sufficient. That’s why we support that as well using the same syntax. Simple replace ToCollection with ToStringList(columnName) or ToInt32List(columnName) and you get your answers.

Caching that you control

Using “appenders”, you have direct control over caching. You decide which operations read from the cache, which update it, and which invalidate it simply by inserting the correct method between ToCollection and Execute.

You can even do tricks such as cache both a collection and the individual records:

dataSource.From("Customer").ToCollection<Customer>().CacheAllItems( c => "Customer: " + c.Id).ReadOrCache("AllCustomers").Execute();

Later on when you try to read, it knows where to find your object.

dataSource.From("Customer", new {Id = id}).ToObject<Customer>().ReadOrCache("Customer: " + id).Execute();

And updates only invalidate what you need them to invalidate.

dataSource.Update("Customer", customer).InvalidateCache("AllCustomers").InvalidateCache("Customer: " + id).Execute();

Yes, this is a bit more work than automatic caching. But it gives you precise control through the same fluent API that you do everything else.

No Cost Raw SQL Access

Some ORMs punish you for using raw SQL. Features that would otherwise be available (e.g. caching in EF) suddenly disappear when you start mixing in raw SQL. With Chain, you have full access to all of the materializers and appenders that you used with any other command.

Advanced database features at your fingertips

We mentioned before that we’re embracing database specific functionality. Our first example is integration with SQL Dependency. If you are using SQL Server, you can add a WithChangeTracking appender to your query. Without the need for polling, the database will notify you via a callback when the table beneath the query changed in a way that invalidates your query.

And we’re not done yet

Some of the highlights in our roadmap include:

  • Support for PostgreSQL and MySQL
  • Paging and Sorting
  • Bulk Inserts
  • Table Value Functions
  • Immutable Objects
  • Dynamic and JSON based materializers
  • Partial updates based on property-level change tracking

What we need form you?

Use it, abuse it, break it. Then let us know how you did it.

Our goal isn’t just to create another ORM. We’re aiming lower. We want to create a pit of success where excellent outcomes are the default and you have to actively try to get bad performance.

Clone this wiki locally