This is the fourteenth in a series of posts on how to build a LINQ IQueryable provider. If you have not read the previous posts you might request a weeks vacation, sit back, relax with a mochacino in one hand a netbook in the other, or if you've got better things to do with your time print them all out and stuff them under your pillow. Who knows, it might work better.
Complete list of posts in the Building an IQueryable Provider series
Okay, enough with all the post-is-late guilt! It's done now, so breathe a sigh of relief and get on with the reading.
What's inside:
More Mapping - Finally a real mapping system, with attributes and XML. More Providers - MS Access and MS SQL Server Compact Edition More POCO - Constructors, Enum and Interfaces. More More More
More Mapping - Finally a real mapping system, with attributes and XML.
More Providers - MS Access and MS SQL Server Compact Edition
More POCO - Constructors, Enum and Interfaces.
More More More
The full source code can be found at:
http://www.codeplex.com/IQToolkit
[Table] [Column(Member = "CustomerId", IsPrimaryKey = true)] [Column(Member = "ContactName")] [Column(Member = "CompanyName")] [Column(Member = "Phone")] [Column(Member = "City", DbType="NVARCHAR(20)")] [Column(Member = "Country")] [Association(Member = "Orders", KeyMembers = "CustomerID", RelatedEntityID = "Orders", RelatedKeyMembers = "CustomerID")] public IUpdatableTable<Customer> Customers
You specify the Table, Column and Association attributes as necessary. The 'Member' refers to the member in the entity type. If this is the same name as the database's column name you don't need to repeat it by specifying 'Name' too. You can specify nested mapping information by using a dot in the Member name. This allows you to have what some call value types, but to keep from clashing with .Net terminology I don't. For example, if you've defined an Address type that you want to use in a nested relationship (actually embedded in the same table row) you can do that like this:[Table] [Column(Member = "EmployeeID", IsPrimaryKey = true)] [Column(Member = "LastName")] [Column(Member = "FirstName")] [Column(Member = "Title")] [Column(Member = "Address.Street", Name = "Address")] [Column(Member = "Address.City")] [Column(Member = "Address.Region")] [Column(Member = "Address.PostalCode")] public IUpdatable<Employee> Employees
You specify the Table, Column and Association attributes as necessary. The 'Member' refers to the member in the entity type. If this is the same name as the database's column name you don't need to repeat it by specifying 'Name' too. You can specify nested mapping information by using a dot in the Member name. This allows you to have what some call value types, but to keep from clashing with .Net terminology I don't. For example, if you've defined an Address type that you want to use in a nested relationship (actually embedded in the same table row) you can do that like this:
[Table] [Column(Member = "EmployeeID", IsPrimaryKey = true)] [Column(Member = "LastName")] [Column(Member = "FirstName")] [Column(Member = "Title")] [Column(Member = "Address.Street", Name = "Address")] [Column(Member = "Address.City")] [Column(Member = "Address.Region")] [Column(Member = "Address.PostalCode")] public IUpdatable<Employee> Employees
<?xml version="1.0" encoding="utf-8" ?> <map> <Entity Id="Customers"> <Table Name="Customers" /> <Column Member = "CustomerId" IsPrimaryKey = "true" /> <Column Member = "ContactName" /> <Column Member = "CompanyName" /> <Column Member = "Phone" /> <Column Member = "City" DbType="NVARCHAR(20)" /> <Column Member = "Country" /> <Association Member = "Orders" KeyMembers = "CustomerID" RelatedEntityID = "Orders" RelatedKeyMembers = "CustomerID" /> </Entity> <Entity Id="Orders"> <Column Member = "OrderID" IsPrimaryKey = "true" IsGenerated = "true"/> <Column Member = "CustomerID" /> <Column Member = "OrderDate" /> <Association Member = "Customer" KeyMembers = "CustomerID" RelatedEntityID = "Customers" RelatedKeyMembers = "CustomerID" /> <Association Member = "Details" KeyMembers = "OrderID" RelatedEntityID = "OrderDetails" RelatedKeyMembers = "OrderID" /> </Entity> <Entity Id="OrderDetails"> <Table Name="Order Details"/> <Column Member = "OrderID" IsPrimaryKey = "true" /> <Column Member = "ProductID" IsPrimaryKey = "true" /> <Association Member = "Product" KeyMembers = "ProductID" RelatedEntityID = "Products" RelatedKeyMembers = "ProductID" /> </Entity> </map>
You use it like this:
XmlMapping mapping = XmlMapping.FromXml(TSqlLanguage.Default, File.ReadAllText(@"northwind.xml")); SqlQueryProvider provider = new SqlQueryProvider(connection, mapping);
[Table(Name = "TestTable1", Alias = "TT1")] [ExtensionTable(Name = "TestTable2", Alias = "TT2", KeyColumns = "ID", RelatedAlias = "TT1", RelatedKeyColumns = "ID")] [ExtensionTable(Name = "TestTable3", Alias = "TT3", KeyColumns = "ID", RelatedAlias = "TT1", RelatedKeyColumns = "ID")] [Column(Member = "ID", Alias = "TT1", IsPrimaryKey = true, IsGenerated = true)] [Column(Member = "Value1", Alias = "TT1")] [Column(Member = "Value2", Alias = "TT2")] [Column(Member = "Value3", Alias = "TT3")] public IUpdatable<MultiTableEntity> MultiTableEntities
Extension tables are specified similar to how Associations are specified, except you are never referring to members, only column names. You use the 'Alias' value to connect column & association mappings with columns from particular tables. All queries for this multi-table entity treat the 'Table' as the primary table queried, all other tables are queried with left-outer joins. All keys for associations must be from the same alias. Can I mix nested mapping with multi-table mapping? I have not tried it, but in theory it should work. It should not matter which table your nested entity gets it's data from, so in effect you can have a composition relationship between one table and another as long as it is 1:1.
What about many-to-many? Not yet. Making the system query a many-to-many relationship is relatively easy. I haven't yet figured out the right semantics for inserts & updates. Right now, all insert, updates and deletes are explicit via calls to the IUpdatable with real-live entities. Yet how do you make an explicit update to the link table that you don't have an entity directly mapped to? I need to ponder this some more. Possibly if one side of the relationship is a composition as opposed to an association, then it would be implied when that side is updated. Yet what if you chose not to load the relationship, how do you tell the system to not delete all previous relationships?
In order to make this work I've added an AccessLanguage object that is necessary to get the correct semantics for MS Access queries and an AccessFormatter object that handles generating the correct command text. In order to salvage as much as I could from the TSqlFormatter, I moved most of this code to a common SqlFormatter base class, and now the TSQL and Access formatters only supply the deviations from the standard syntax. (Of course, 'standard' is currently whatever I deem it to be so don't go getting some actual online specification and prove me wrong.) Access only allows one command at a time, so that added an extra wrinkle, but in the end there is now support in the system for providers that can only do one command at a time. This means there are multiple round-trips to the engine for things like inserting a record and getting back the computed keys. Luckily, the access engine is in-proc so this is not really a burden. A new property on QueryLanguage, 'AllowMultipleCommands' determines how the execution plan is generated and whether multiple commands can be lumped together into a single ADO command. The good news is that the access engine passes almost all the Northwind tests; some are not possible (mostly ones testing translation of framework methods that have no apparent equivalent in the access expression engine). There were a lot of hairy strange & subtle differences in syntax between Access and TSQL, but most were handled by having different format rules, some required new expression visitors to change the query tree, like no explicit cross joins! This caused me to write a visitor to attempt to get rid of cross joins (often injected by my visitor that tries to get rid of cross-apply joins) which is now generally useful to everyone, and if that didn't do it, another visitor that would attempt to isolate out the cross joins from any other joins and push them into sub-queries where Access lets me use the old-style comma-list, which is truly a cross join, though it just can't be mixed with other kinds of joins in the same from clause.
- IQToolkit.dll - IQToolkit.Data.Access.dll - IQToolkit.Data.SqlClient.dll - IQToolkit.Data.SqlServerCe.dll
There's not a whole lot of policy being used right now and the policy objects dependence on the mapping object was no where near as deep as the mapping object's dependence on the language. So policy is now independent of mapping, which means you can construct providers without specifying policy and/or reusing mapping with different policies. Now if I could only make it simpler to specify/construct mapping without needing to know the language. Back to the drawing board.
I apologize for the churn. The namespace changed so now all heck is going to break loose. Gone is the simple 'IQ' namespace and in its place is the 'IQToolkit' namespace. I really did like the 'IQ' name, it was short, classy and made you feel intelligent just by looking at it. Yet, it was hard to guess at if you did not already know what it was. I chose to change the namespace name to match the product name and the DLL name. You add reference to the IQToolkit.dll and you import/use the IQToolkit namespace. No fuss, no muss. Except for all those files you'll have to edit now. But hey, this is pre-pre-pre-pre beta stuff. Some people may think they are something special by snarkily keeping all their products in beta. They've got a lot to learn.
I hope this toolkit is becoming useful to many. I realize there have been a variety of requests for new things in the toolkit that I just have not gotten time to put in yet. So you can expect plenty more in the future.
So enough with reading. It's time to code!