|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionEver wished you could truly embed SQL functionality in your C# code without using strings or late binding? Imagine being able to write complex xQuery.Where =
(CustomerTbl.Col.NameFirst == "Chad" | CustomerTbl.Col.NameFirst == "Hadi")
& CustomerTbl.Col.CustomerID > 100 & CustomerTbl.Col.Tag != View.Null;
Look closely. This is a C# code, not SQL. It's resolved and bound at compile time, but evaluated at run time. In this article I shall provide an introduction to this method and the full source code for your use. C OmegaC Omega is a research programming language from Microsoft. C Omega is used to test out and try new ideas for future versions of C#. C Omega supports many items that are similar to what is provided by Indy.Data. However Indy.Data provides some more items that C Omega does not. And most important of all - Indy.Data is here now and usable in production code. C Omega is a research project and at best will be incorporated into a future version of C#. In fact, if we look at some example C Omega code: struct {
SqlString CustomerID;
SqlString ContactName;
}* res
= select CustomerID, ContactName from DB.Customers;
foreach( row in res ) {
Console.WriteLine("{0,-12} {1}", row.CustomerID, row.ContactName);
}
It looks very much like Indy.Data: [Select("select `CustomerID`, `ContactName` from `Customers`")]
public class CustomerRow : View.Row {
public DbInt32 CustomerID;
public DbString ContactName;
}
using (Query xCustomers = new Query(_DB, typeof(CustomerRow))) {
foreach (CustomerRow xCustomer in xCustomers) {
Console.WriteLine("{0,-12} {1}", row.CustomerID, row.ContactName);
}
}
Indy.Data does not quite have the syntax of C Omega, but with C Omega Microsoft has full flexibility to change the language. With Indy.Data we are restricted to how C# can be extended. Fortunately C# can be extended farther than most realize. Future articlesThe main assembly is called Indy.Data and relies on Neither meat nor fishIndy.Data is neither an O/R mapper nor a code generator in strict terms. Instead Indy.Data is something different, and something similar. In this article this will not be completely apparent, so for this article consider Indy.Data to be a Data Access Library (DAL) only. Indy.Data does support code generation to keep the DAL objects in sync with your database, however it does not create mappings to business logic, nor does it generate its own SQL during generation. Indy.Data is not an O/R mapper either. Indy.Data is more flexible in that it is not restricted to parameterized queries or stored procedures. Indy.Data also does not provide query construction and mapping, but instead relies on existing objects in the database, or provided SQL. Indy.Data provides object wrappers on a 1:1 basis for database tables, views, stored procedures, or SQL statements. These objects can be regenerated at any time to be kept in sync with database changes. In future articles I will explain how Indy.Data can be used to perform the same functions as a code gen or O/R mapper, but in a slightly different way. However for the scope of this article assume Indy.Data to be an advanced implementation of an ADO.NET Extreme databasesADO.NET is a good library for connecting to databases. But using ADO.NET still uses the standard methodology that data connectivity is not type safe, and that the binding to the database is loose. Fields are bound using string literals, or numeric indexes. All types are typecast to the desired types. Changes to the database will introduce bugs in the application. These bugs however will not be found until run time because of the loose binding. Unless every execution point and logic combination can be executed in a test, bugs will not appear until a customer finds them. Because of this, as developers we have been conditioned to never ever change a database. This causes databases to be inefficient, contain old data, contain duplicate data, and contain many hacks to add new functionality. In fact this is an incorrect approach, but we've all grown accustomed to accepting this as a fact of development. However if we use a tight bound approach as we do with our other code, we can upgrade and update our database to grow with our system. Simply change the database, and recompile. Your system will find all newly created conflicts at compile time and the functionality can then be easily altered to meet the new demand. I call this an "Extreme Database" or XDB, inline with Extreme Programming or XP. Using the built in ADO.NET commands reading from a query is as follows: IDbCommand xCmd = _DB.DbConnection.CreateCommand();
xCmd.CommandText = "select \"CustomerID\", \"NameLast\", \"CountryName\""
+ " from \"Customer\" C"
+ " join \"Country\" Y on Y.\"CountryID\" = C.\"CountryID\""
+ " where \"NameLast\" = @PNameLast1 or \"NameLast\" = @PNameLast2";
xCmd.Connection = _DB.DbConnection;
xCmd.Transaction = xTx.DbTransaction;
IDbDataParameter xParam1 = xCmd.CreateParameter();
xParam1.ParameterName = "@NameLast1";
xParam1.Value = "Hower";
xCmd.Parameters.Add(xParam1);
IDbDataParameter xParam2 = xCmd.CreateParameter();
xParam2.ParameterName = "@PNameLast2";
xParam2.Value = "Hauer";
xCmd.Parameters.Add(xParam2);
using (IDataReader xReader = xCmd.ExecuteReader()) {
while (xReader.Read()) {
Console.WriteLine(xReader["CustomerID"] + ": " + xReader["CountryName"]);
}
}
The same code when written using Indy.Data is as follows: using (CustomerQry xCustomers = new CustomerQry(_DB)) {
xCustomers.Where = CustomerQry.Col.NameLast == "Hower"
| CustomerQry.Col.NameLast == "Hauer";
foreach (CustomerQry.Row xCustomer in xCustomers) {
Console.WriteLine(xCustomer.CustomerID + ": " + xCustomer.CountryName);
}
}
First lets ignore the fact that it is shorter. The standard ADO.NET could be wrapped in an object or method as well. What I want to point out is the fact that the standard ADO.NET requires the use of text strings. In fact, the code listed above has a mistake in it. " Could this be done with a codegen or O/R mapper? Yes, but generally they offer one of the following:
With Indy.Data's approach, full freedom is retained to form the One languageTraditionally developers building database systems had to learn not only a development language (C#), but also had to become quite versed in SQL and stored procedure language (such as T-SQL). To develop complex systems, a developer must not only be competent in all the three, but must split the system logic between the three languages. Even if you have a DA who writes all your stored procedures and SQL, it still splits your system logic into multiple languages. With Indy.Data, SQL is still used, however the SQL that is used is simplified into a basic form and isolated into discrete pieces (Queries and views). All of the system logic can then be coded in C#. Since C# code can now be used to modify the basic SQL building blocks. In short, now you can develop your system using one language, C#. This is done by extending the C# language to handle ViewsIndy.Data's primary object is the View Types Tables/Views Views can exist for tables or views in a database. Stored proceduresSupport for stored procedures which return result sets is not implemented yet. Support for these types of stored procedures will be coming soon. QueriesCustom SQL can be created and stored in a database as a view or a stored procedure. However during development the management of such views and especially the alteration of views can be quite difficult. For this case, Indy.Data also allows the developer to specify local SQL statements that are external to the database which are then embedded into a GenerationView classes are generated by an external utility. The utility is configured to examine a database and scan the database for tables, views, stored procedures, and external SQL statements. From these, it generates view base classes. From these base classes, it also generates a default shell for user extension. Since the generator separates the classes into two, the base classes can be regenerated without overwriting the custom code. The generator is currently being ported and updated to support both Firebird and SQL Server. I expect to release this within a few days. ExamplesIn this first article all of the examples are on a simple database. I will be expanding on these examples in later articles. These examples show the operations on a single table. Indy.Data can accept views as well as SQL statements. Many examples have been taken from the NUnit test project. Because of this you will see a lot of assertions and other checks in the example code. DatabaseFor these examples, the following database is used. CREATE TABLE "Customer" (
"CustomerID" INTEGER NOT NULL,
"NameFirst" VARCHAR(40) NOT NULL,
"NameLast" VARCHAR(40) NOT NULL,
"Tag" INTEGER
);
CREATE TABLE "Country" (
"CountryID" "KeyDmn" NOT NULL,
"CountryName" VARCHAR(50) NOT NULL,
"ISOCode" CHAR(2) CHARACTER SET ASCII NOT NULL,
CONSTRAINT "PK_Country" PRIMARY KEY ("CountryID"),
CONSTRAINT "UNQ_Country_1" UNIQUE ("ISOCode"),
CONSTRAINT "UNQ_Country_2" UNIQUE ("CountryName")
);
Code samplesReading from a ViewThe basic form to read from a view is as follows: using (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
xCustomers.SelectAll();
foreach (CustomerTbl.Row xCustomer in xCustomers) {
Console.WriteLine(xCustomer.NameFirst + " " + xCustomer.NameLast);
}
}
Once the view is selected, a The Manual Readusing (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
xCustomers.SelectAll();
CustomerTbl.Row xCustomer1 = (CustomerTblRow)xCustomers.Read();
CustomerTbl.Row xCustomer2 = (CustomerTblRow)xCustomers.Read();
Console.WriteLine(xCustomer1.NameFirst + " " + xCustomer2.NameFirst);
}
Instead of Since the row class is separate from the reader, rows can be saved and cached. For example you may wish to compare the current row to the last row. With Indy.Data this is easy, you just need to store a reference to the previous row. Accessing columnsIn the previous examples a column can be easily accessed: string s = xCustomer1.NameFirst
using (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
xCustomers.SelectAll();
foreach (CustomerTbl.Row xCustomer in xCustomers) {
if (!xCustomer.Tag.IsNull) {
Console.WriteLine(xCustomer.Tag);
}
else {
Console.WriteLine(xCustomer.NameFirst + " " + xCustomer.NameLast);
}
}
}
Notice that Null"Null is a state, not a value". Fortunately all major databases follow this properly. However nearly every data access layer treats accessing a value that is Tag == 0
Tag.IsNull == true
So you can read the value of Inserting a rowusing (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
CustomerTbl.Row xCustomer = new CustomerTbl.Row();
xCustomer.CustomerID = xCustomers.NewKey();
xCustomer.NameFirst = "First";
xCustomer.NameLast = "Last";
xCustomers.Insert(xCustomer);
}
Updating a rowusing (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
xCustomers.SelectFirstName("Chad");
CustomerTbl.Row xCustomer = xCustomers.Read();
// SQL Server can only have one data command per
// connection so we must close before update
xCustomers.Close();
xCustomer.NameLast = "Hauer";
xCustomers.Update(xCustomer);
}
Updating a row is similar to inserting. However to update a row, we must first obtain the row somehow from the database. In this update, the first row with the first name of Filtering dataIn updating a Where = Col.NameFirst == aName;
Note that the Where =
(CustomerTbl.Col.NameFirst == "Chad" | CustomerTbl.Col.NameFirst == "Hadi")
& CustomerTbl.Col.CustomerID > aMinID & CustomerTbl.Col.Tag != View.Null;
Even the Where = Col.NameFirst % aName;
The Where = CustomerTbl.Col.CustomerID > aMinID;
if (aNullTagsOnly) {
_Where = _Where & CustomerTbl.Col.Tag == View.Null;
}
Of course, parenthesis and other operators still apply. Other operationsIndy.Data supports many other operations and capabilities. This article is just an introduction and I will write others in the near future to expand on these. DataBindingsUsing WinForms or WebForms with DataBindings? The library can support this too. It can even be used to fill Supported DatabasesThe library is designed to be database agnostic and work with any SQL based database using ADO.NET. Currently the library has been tested with Firebird and SQL Server. The library should work with Oracle, Interbase and others with minimal additions. Implementation notesRootsThe roots of Indy.Data go back to a library I had originally built in Delphi in 1998. Of course because Delphi did not support implicit converters, operators overload, and many other features I used, it was not nearly as smooth as Indy.Data. The first implementation in .NET was created in 2003. .NET 2.0The library is currently written for .NET 1.1. In many places it would benefit greatly from the use of generics, partial classes, and ADO.NET data provider factories. When .NET 2.0 is released, Indy.Data will be updated to take advantage of such functionality. C#The library is designed for C#. It may be used for other languages, however languages such as Visual Basic .NET do not support implicit conversions and other C# language features used by the library. Open SourceIndy.Data is open source. If you are interested in using or contributing to Indy.Data, a yahoo group has been established where you can join. | ||||||||||||||||||||