1.Introduction to Sooda
1.1.What is Sooda?
Sooda (Simple Object-Oriented Data Access) is an Object-To-Relational Mapping software for the .NET (http://www.microsoft.com/NET/) environment. O/R Mapping Software enables you to express your business rules without SQL.
Sooda includes a code generator, which can generate entire Data Access Layer for your application. Each class of the DAL typically corresponds to a database table, its properties map to database columns and relationships map to object references and collections.
1.1.1.Features
Sooda supports the following features, which are explained in the following sections:
1.1.2.Supported Platforms
Sooda supports .NET Framework 2.0, 3.5 and 4.0 and Mono profiles 2.0 and 3.5.
The following development tools/build environments are supported:
1.1.3.Supported Relational Databases
The following RDBMSes are supported:
Sooda has been used in production systems for at least 9 years and things are generally working smooth. Some graphical and command line tools to support the development are available, but they are generally newer and still require some work.
1.1.4.Supported Languages
Sooda supports code generation using CodeDOM technology. In theory any language should be supported, but some languages have problems with their CodeDOM providers or their compilers are buggy. The following languages have been tested:
1.2.Basic Concepts
1.2.1.Mapping Classes to Tables
Most applications written in high-level languages use the object-oriented approach and often they need to store data in relational databases. Working with objects, we use method calls, properties events, collections, inheritance, polymorphism, and so on, while working with relational data requires us to think in terms of SELECT, INSERT, UPDATE and DELETE SQL operations on rectangular tables.
Obviously the two models (object-oriented and relational) are not compatible. Sooda lets you bridge the gap by providing an object-oriented interface to the database so that you can write code in your favourite high-level language without using SQL at all.
Let's see how objects can be stored in a database. Assuming we have a simple class named Employee.
Objects of this class can be persisted when we follow the rules of so called "natural mapping", where:
Applying these "natural mapping" rules to the Employee class, we get the following table:
Natural mapping as described above is not always possible/desirable and therefore we need a mapping schema (3) to fully describe the mapping for tables/columns/datatypes and more. We can for example:
SELECT, FROM, WHERE, GROUP
)1.2.2.Primary Keys and Object Identity
Entities in the databases are identified by their primary keys. In object-oriented languages we refer to objects by their references. The references are valid only for a single execution of the program and cannot be stored in a database directly. O/R mapper needs to convert between primary key values and object references when loading and storing data.
In order to participate in O/R mapping each database table needs to have one or more primary key columns (either natural or surrogate) and you need to tell the mapper which columns are primary keys. The primary keys must be properly maintained in the database (the best way is to create a UNIQUE index on primary key columns). If you fail to do so (for example when there are many rows sharing the same value in the primary key column), you may get all sorts of undefined behavior.
Each object in Sooda knows about the primary key that represents it in the database. Transaction-level object cache (6.5) (L1 cache) is used to map primary keys to objects. This two-way mapping lets Sooda provide reference identity (6.5) for objects it manages.
When creating a new object in memory, the primary key needs to be known in advance (before the object is actually stored in the database). Because of this, we cannot use identity columns in databases to provide primary keys, other mechanism to generate primary keys is necessary.
Sooda provides key generators (6.9) which may be used for this purpose. The default one uses a special table in the database to manage batched key allocation. Alternatively, for primary keys which are GUIDs, you can use the standard Guid.NewGuid() generation method.
1.2.3.One-To-Many Relationships
create table Customer ( ID int primary key not null, Name varchar(64), Address1 varchar(64), Address2 varchar(64) ); create table Order ( ID int primary key not null, -- more columns here CustomerID int not null references Customer(ID) );
Sooda makes the task of managing foreign key relationships rather simple by providing a dual view of the relationship: reference from a "child" to its "parent" and a collection of "children" that is a member of the "parent" objects. You only need to specify which property in the "child" object is a reference to the "parent" object. Code generated by Sooda includes the following APIs:
class Customer { int ID { get; set; } string Name { get; set; } ... OrderList Orders { get; } } class Order { int ID { get; set; } Customer Customer { get; set; } }
The OrderList is functionally equivalent to IList<Order> with some filtering and sorting methods included for convenience (8.4.7) . Customer is a reference to the Customer class. Sooda lets you write:
Customer c; // create new order Order o = new Order() c.Orders.Add(o); // this is equivalent to the above o.Customer = c; c.Orders.Remove(o); // get order count c.Orders.Count; // check it the orders contains the specified one c.Orders.Contains(o); // return a sorted collection c.Orders.OrderBy(sortExpression);
Convenience methods of Sooda collections are described in section "8.4.7. List wrappers" .
1.2.4.Many-To-Many Relationships
Many-to-many relationships are used in many situations, such as these:
Database representation of many-to-many relationships requires a separate table that will hold pairs of primary keys that are in relationship, such as the Employee2Permission table in the following figure:
The SQL used to create this database is:
create table Employee ( ID int primary key not null, Name varchar(64), Address1 varchar(64), Address2 varchar(64) ); create table Permission ( ID int primary key not null, PermissionName varchar(64), ); create table Employee2Permission ( EmployeeID int not null references Employee(ID), PermissionID int not null references Permission(ID) ) alter table Employee2Permission add primary key (EmployeeID,PermissionID);
Sooda generates a pair of collection which provide access to objects in the relationship. In this example, we have:
class Employee { ... PermissionList Permissions { get; } } class Permission { ... EmployeeList Employees { get; } }
These collections support the same set of operations as one-to-many collections, namely Add(), Remove(), Contains(), Count and others. The collections are synchronized, no matter which object you use to modify the relationships, the result is the same. In this example you could:
using (SoodaTransaction t = new SoodaTransaction()) { Employee emp = ...; Permission perm = ...; // the following two lines are equivalent emp.Permissions.Add(perm); // add permission to the emp's permission collection perm.Employees.Add(emp); // add employee to perm's employees collection if (emp.Permissions.Contains(perm)) { Console.WriteLine("Employee is authorized."); } // this tests essentially the same thing if (perm.Employees.Contains(emp)) { Console.WriteLine("Employee is authorized."); } }
1.2.5.Lazy Loading
Often you need to operate on an object but you do not need access to its properties. It would be very inefficient to load full record from a database each time a reference to it is needed. For example you do not need to know the name of the employee just to check if he is in relationship with some other objects.
Sooda supports lazy loading by maintaining a data structure that holds object data. It loads the data and initializes the data structures on as-needed basis, which means objects do not allocate memory for their data values until the properties are accessed.
Each object managed by Sooda can be in one of the four states:
Each time a property is read in Data Not Loaded or Data Not Loaded-Modified state Sooda needs to access the database to load the data. If a property is only written to, there is no need to even load the data from the database.
Lazy loading is described in section "6.3. Loading objects" .
1.2.6.Path expressions and managing referenced objects
Objects in memory may form graphs: they may include properties which are references to other objects and these objects may have references to other objects and so on.
Sooda supports path expressions both in code and in query language (1.2.9) which let you access properties of related objects easily. For example to access the last name of the manager of a person's organization unit:
// display the last name of person's org unit
Console.WriteLine(person.OrganizationUnit.Manager.LastName);
You can access collections in the same manner:
// display the number of org unit members
Console.WriteLine(person.OrganizationUnit.Members.Count);
Object references are fully lazy-loaded and managed by transaction L1 cache (1.2.2) so that the number of database accesses is reduced.
1.2.7.Mapping Multiple Tables to a Single Class
A class can be mapped to more than one table in the database or it can be split into many logical "tables" which are actually mapped to the same physical table. We may want to do it for many reasons:
This is the most typical situation where some columns may be placed in a separate table for performance:
If the data row size is not a problem, we may still want to do the logical partitioning of data while keeping them in a single table. We only need to tell Sooda to keep some fields together by grouping them in a logical Table.
The following example shows the Employee table which contains two large columns which we want to load separately: Picture which is an image and VoiceSample which is a blob. Possible partitions of this table are marked with separate colors. Note that the primary key column is not marked because it is always present in memory and does not need to be loaded at all.
Sooda makes it possible to create a class based on up to 32 physical or logical tables. The only thing they need to have in common is their primary key. This feature is used in inheritance, which is described in section "1.2.8. Inheritance" .
1.2.8.Inheritance
Inheritance ("is-a" relationship) is an object-oriented programming concept which is not present in relational databases. One of the most important features of inheritance is polymorphism (ability to act on objects through their base classes or interfaces without knowing their exact type).
Inheriting classes may introduce additional fields (properties) and we need a way to store them in a database. There are 3 methods of storing inheritance hierarchy in database:
Assuming you have a simple hierarchy of 4 classes:
The TPT representation of this hierarchy looks like this (note that each subclass is stored in its own table, and only the fields added in that subclass are kept there, the other fields are accessible through primary key join):
TPH model (note that fields from all classes are gathered in a single table and each field from a non-root class is nullable):
Or a TPC model (fields from the base class are repeated in all tables):
Supporting polymorphism in O/R mapping requires the engine to be able to create instances of the proper subclass on load. This is required because when you load an object from the database you may get an instance of a subclass. Object references (stored as foreign keys) are also polymorphic (you only store the key value, not the concrete type). We need a way to be able to quickly determine the concrete class that will represent the object from the database.
TPT and TPH storage models require a special column (called discriminator column) to select between subclasses. This column needs to provide a unique mapping between the concrete class and an integer value (for example 1-QuestionWorkItem, 2-AnalysisWorkItem, 3-BugWorkItem). Each time an object of a particular class is persisted, a value corresponding to its class is stored in this column and on load this value is used to recreate object of the proper class. TPC model does not need this column but makes it more difficult to access objects in a polymorphic manner, because you need to perform a UNION select or make multiple queries in order to fetch all objects.
Polymorphic access enables you to write:
WorkItem wi = WorkItem.Load(3); if (wi is QuestionWorkItem) { QuestionWorkItem qwi = (QuestionWorkItem)wi; Console.WriteLine("question: {0}", qwi.Question); } // display concrete types of all work items foreach (WorkItem wi in WorkItem.GetList(true)) { Console.WriteLine("wi: {0}", wi.GetType().FullName); }
Sooda supports TPT and TPH, but does not support TPC model because it is less efficient than the other two and requires complex SQL queries to be generated.
Sooda extends the concept of "tables" on which each class is based to mean not only physical tables but also vertical table partitions. This way you can create a mixture of TPT and TPH in your program. For example you can store the hierarchy in TWO tables instead of one. You can also store most commonly used subclasses in a single table but move rarely used or "heavy" data (such as blobs) to separate physical tables.
1.2.9.Query Language
We need a way to retrieve objects from the database based on the specified criteria. SQL is inappropriate for this, because it does not make use of concepts such as:
Sooda supports three object-oriented query languages:
All of them let you use path expressions and collections to write join-free powerful object-oriented queries. This is described in section "7. Query Languages" .
1.2.10.Cross-transaction (Level 2) Caching
Frequent use of single rows from the database (which is common for all O/R mapping products) can be expensive. The cost of an SQL server roundtrip can be quite high. Sooda implements in-memory caching of objects retrieved from the SQL database (both single rows and collections) so that the speed of processing is improved.
The Caching section (6.11) explains things in detail.
1.2.11.Dynamic Fields
Sooda supports creation and deletion of fields at runtime. If this feature is enabled, fields can be added to any Sooda class. When you add a field, Sooda creates a database table with the requested column type and optionally a foreign key constraint. Dynamic fields can be accessed from Sooda objects and queries just like the fields defined in the mapping schema.
This feature is described in section "6.13. Dynamic Fields" .
2.Getting Started
Sooda comes with a graphical wizard that can be used to automatically add Sooda support to your Visual Studio project.
In this tutorial we will use it to create a simple C# application that exercises various features of Sooda. We will be
using the Northwind
database that comes with MSSQL Server but any database should be ok.
2.1.Configuring Sooda project
Let's start by creating an empty C# Console application project in Visual Studio. After the project has been created and saved to disk (you need to save your projects explicitly when using Visual Studio Express Editions), run the wizard by clicking on the "Add Sooda Support to Visual Studio Project" shortcut on the Start menu:
The first step of the wizard is a greeting message that reminds the user to prepare a database
and choose a Visual Studio project file to configure. Choose the project by clicking Browse
.
We can proceed to the next step by clicking the Next
button.
In the second step of the wizard you need to specify database server and credentials used to connect to the database.
Make sure the account used to connect has the right to create additional tables in the database or you will not be able
to complete the wizard (using a local administrator account and Windows Authentication should be sufficient in most cases).
Click Next
to proceed to the database selection:
The third step of the wizard prompts you to choose one of the databases that are running on the server. Choose Northwind
and click Next
.
The last page of the wizard presents a list of actions the wizard can perform for you. The default set of options should
be just fine, and clicking Finish
will add Sooda support to your project as requested.
If you choose to set advanced options by clicking the Advanced Options
button, the wizard will present
you with a list of more settings each followed by the Explain
link that will direct you to the appropriate
section of the documentation.
Once you have set up your project it is time to compile it. Switch to Visual Studio and click "Build" (note that you will get warnings about project file modifications - choose to Reload your project when prompted).
It is possible that you get the following error when building your project.
error CS0234: The type or namespace name 'Drawing' does not exist in the namespace 'System' (are you missing an assembly reference?)
If you do, add 'System.Drawing.dll' reference to your project and recompile. This error may occur if you
use have any pictures stored in the database (which is the case with Northwind
database).
Note how the Wizard generated the following files for your project:
projectname.soodaproject
- that stores code generation settings (4.2)
SoodaSchema.xml
- that contains the object-to-relational mapping schema (3)
App.config
- that contains runtime configuration settingsIn addition to these your project has been modified to automatically launch the SoodaStubGen utility (8.1.1) on each build and optionally to launch SoodaCompileStubs (8.1.1.3) to precompile stubs to a DLL.
2.2.Renaming classes
Class names detected by the wizard are plural nouns, because this is how tables are named in the Northwind
database. For example we have Territories
instead of Territory
.
Using plurals in class names is not a good idea so we should fix it by renaming the classes.
To do this, open SoodaSchema.xml
and replace all plural class names with the appropriate singular forms.
Make sure to replace only class names not table names, so it is best to use the following renaming strategy:
"Territories"
with "Territory"
(quotes should be included in the
Search For and Replace With strings)<table name="Territory" />
with <table name="Territories" />
Repeat the above steps to rename:
Categories
with Category
Customers
with Customer
Employees
with Employee
Orders
with Order
Products
with Product
Region
with Region
Shippers
with Shipper
Suppliers
with Supplier
Territories
with Territory
In addition, you should give meaningful names to all collections declared with
<collectionOneToMany /> and <collectionManyToMany />. You should
remove CollectionOf
and trailing digits from the names.
Remove the following files for classes that are no longer present or you will get errors about some non-existent stub classes.
When you are done, rebuild your project.
2.3.Adding example code to your project
It is now time to add some code to the project that will demonstrate the use of DAL classes generated by Sooda. By default Sooda generates one class for each table found in the database and the name of the class corresponds to the name of the table as described in section "8.1.1.5.3. genschema" .
Before you can use Sooda objects, you need to create an instance of SoodaTransaction (6.1)
class. You can only access your objects within the transaction and after the transaction is closed, object references
are not valid. Typically you create a SoodaTransaction
object in a using()
clause.
To begin, add the following using
clauses to the top of your Program.cs
source file
replacing YOURNAMESPACE
with the namespace of the project:
using Sooda; using YOURNAMESPACE.TypedQueries;
Copy the following piece of code and paste it to replace the generated Main()
method:
public static void Main() { // you cannot access Sooda objects here using (SoodaTransaction tran = new SoodaTransaction()) { // access your objects here // commit all changes made within transaction tran.Commit(); } // you cannot access Sooda objects here }
We are now ready to test various Sooda features. Let's start with simple object loading. To load an object given its
primary key, you can use the generated Load()
static method. The following code loads a row from the
Territories
table and displays its TerritoryDescription
. Note that you use the
(renamed) class names instead of the table names in your code.
Territory t = Territory.Load("98104"); Console.WriteLine("Territory is: {0}", t.TerritoryDescription);
When you run this program, it should display:
Territory is: Seattle
To see what region Seattle belongs to, we can use the path expression (1.2.6)
t.Region.RegionDescription
:
Territory t = Territory.Load("98104"); Console.WriteLine("Territory is: {0}", t.TerritoryDescription); Console.WriteLine("Region is: {0}", t.Region.RegionDescription);
We can use foreach()
to enumerate all employees from Seattle:
Territory t = Territory.Load("98104"); foreach (Employee e in t.Employees) { Console.WriteLine("name: {0} {1} id: {2}", e.LastName, e.FirstName, e); }
There is one: Michael Suyama
and his Employee ID is 6
.
Let's see a list of his all superiors:
Employee michaelSuyama = Employee.Load(6); for (Employee e = michaelSuyama; e != null; e = e.ReportsTo) { Console.WriteLine("id={0} name={1} {2} reports to={3}", e.EmployeeID, e.LastName, e.FirstName, e.ReportsTo); }
Running this code presents us with a list of 3 people and their IDs:
id=6 name=Suyama Michael reports to=5 id=5 name=Buchanan Steven reports to=2 id=2 name=Fuller Andrew reports to=
What if we wanted to find people by their names? Sooda supports three query languages: LINQ (7.6) , typed queries (7.5) (supported only in C#) and SOQL (7.1) .
foreach (Employee emp in Employee.Linq().Where(e => e.LastName == "Suyama")) { Console.WriteLine("{0}", emp.FirstName); }
Same query using Typed Queries:
foreach (Employee emp in Employee.GetList(EmployeeField.LastName == "Suyama")) { Console.WriteLine("{0}", emp.FirstName); }
Same query using SOQL:
foreach (Employee emp in Employee.GetList(new SoodaWhereClause("LastName == {0}", "Suyama"))) { Console.WriteLine("{0}", emp.FirstName); }
LINQ and Typed Queries have the advantage of queries being validated at compile time. This is great when developing large ever-changing projects, as you can immediately know the impact of removing a field or changing its data type.
In all three Sooda query languages you use path expressions (7.2.1)
instead of SQL JOIN
operators.
The following example displays the details of all shipments made via "Speedy Express"
carrier that have more than 3 items:
foreach (Order o in Order.GetList( OrderField.ShipVia.CompanyName == "Speedy Express" && OrderField.Items.Count > 3)) { Console.WriteLine("Order #{0} Date: {1}", o.OrderID, o.OrderDate); Console.WriteLine("Ship to: {0} {1} {2} {3} {4}", o.ShipName, o.ShipAddress, o.ShipCity, o.ShipPostalCode, o.ShipCountry); for (int i = 0; i < o.Items.Count; ++i) { Product p = Product.Load(o.Items[i].Product); Console.WriteLine(" {0}. {1} {2} x {3}", i, p.ProductName, p.UnitPrice, o.Items[i].Quantity); } //Console.WriteLine("o: {0}", o.Items.Count); }
Creating and modifying objects is easy. To create objects you simply use the familiar new
construct. To create new order you would write:
Order o = new Order();
To modify objects, just set their appropriate properties:
o.ShipCity = "Redmond"; o.ShipCountry = "US"; o.ShipPostalCode = "98052"; o.ShipAddress = "One Microsoft Way";
When you are done with object modifications, you can call the Commit()
method on the transaction
object to save your changes to the database.
3.O/R Mapping Schema
O/R Mapping Schema defines the mapping between CLI classes and database tables.
The schema is contained in an XML file that is the input to SoodaStubGen utility (8.1.1) which generates class stubs. You can write the XML file by hand or you can use a tool, such as SoodaSchemaTool (8.1.1.5) to generate it for you by reverse-engineering the database. Note that it is not possible to generate the mapping schema in a fully automated manner. There are things that simply cannot be retrieved from the database (such as inheritance relationships, named constants, many-to-many relationship names), anyway the tools can give you a good approximation of the actual schema.
The root of the XML document is the <schema /> element with the appropriate XML namespace (http://www.sooda.org/schemas/SoodaSchema.xsd
).
Child elements of the <schema /> define classes, relations and so on, as described below.
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource/> <class/> <class/> <class/> <class/> <relation/> <relation/> </schema>
3.1.Data Sources - <datasource />
Data Sources are defined with the <datasource /> element. A data source represents a
connection to the database (or other store) where entities are stored. Each data source has a name and type.
Sooda currently supports only Sql Data Source (6.12)
, but
it is (theoretically) possible to add more data source types. Unless specified otherwise, the
default data source for a class or relation is "default
", which you need to have defined.
While Sooda supports no other data source types, the declaration that should be present at the beginning of each mapping schema is:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource name="default" type="Sooda.Sql.SqlDataSource"/> </schema>
You can define additional SQL-based data sources for example to connect to multiple databases at once. Sooda will keep a single connection to each data source, but will not create one until you use an object that is based on this data source.
3.2.Classes - <class />
Classes are defined using <class /> elements. Each class is based on one or more tables, each represented by a <table /> element. The following table lists available attributes of the <class /> element (bold ones are required).
Name | Type | Description |
---|---|---|
name | string | Class name - used by SoodaStubGen (8.1.1) to generate the class skeleton, stub and related classes (factory, list wrapper, typed queries and so on). Details about the generated code can be found in section "8.4. Generated Code" . |
cardinality | ClassCardinality | Specifies the approximate number of instances of this class (number of rows in a table).
Possible values are:
|
datasource | string | Specifies the name of the data source (6.12) for this class. The default name is "default". |
extBaseClassName | string | Specifies the name of the base class that this class should inherit from. By default each class N inherits from N_Stub which in turn inherits from SoodaObject. Set this option to make the stub class inherit from some other class. Note that the class must ultimately inherit from SoodaObject (either directly or indirectly). |
label | string | Name of the property that provides the human readable "identity" of the object. For example the "Person" class might have a label of "Name" or "Email", the "Location" class might have a label of "Address". The label is used to generate the default implementation of ToString() for object. |
keygen | string | Specifies a method that will be used to generate primary keys for newly created objects.
The following options are supported:
|
inheritFrom | string | Specifies the base class. This parameter is explained in the inheritance section (3.9) . |
defaultPrecommitValue | depends on primary key type | Specifies the default precommit value for this class. This is the value that will be INSERTed into the database for each not-nullable column that does not have a value specified. You can override it on the <field /> (3.4) level. |
subclassSelectorValue | depends on primary key type | Specifies the value of the subclass selector field that determines this object type. This parameter is explained in the inheritance section (3.9) . |
subclassSelectorField | string | Specifies the name of the subclass selector field that determines this object type. This parameter is explained in the inheritance section (3.9) . |
cached | bool | Caching hint. The actual interpretation depends on the caching policy (1.2.10) that is in effect. |
cacheCollections | bool | Collection caching hint. The actual interpretation depends on the caching policy (1.2.10) that is in effect. |
disableTypeCache | bool | Disable type cache for this type. This parameter is described in the inheritance section (3.9) |
triggers | bool | Determines whether triggers (6.8) should be generated for this class. |
readOnly | boolean | Generates read-only class. No property setters are generated in such case and some scenarios are optimized. |
3.3.Tables - <table />
<table /> elements are used to define the mapping from database tables to classes and relations as described in section "1.2.1. Mapping Classes to Tables" . Each class is based on at least one <table /> element. Each table consists of <field /> elements which represent columns in a database and properties in C#/VB. Each table must have at least one field which is marked as the primary key. Compound primary keys (formed by multiple fields with primaryKey attribute) are supported, but tables with zero primary keys are not allowed.
<class name="className"> <table name="tableName1"> <field name="f1" type="t1" primaryKey="true"/> <field name="f2" type="t2"/> <field name="f3" type="t3"/> <field name="f4" type="t4"/> </table> <table name="tableName2"> <field name="f1" type="t1" primaryKey="true"/> <field name="f2" type="t2"/> <field name="f3" type="t3"/> <field name="f4" type="t4"/> </table> <table name="tableName3"> <field name="f1" type="t1" primaryKey="true"/> <field name="f2" type="t2"/> <field name="f3" type="t3"/> <field name="f4" type="t4"/> </table> </class>
The typical definition of a class that is based on a single table looks like this:
<class name="Person"> <table name="Person"> <field name="Id" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="Location" type="String" size="40"/> <field name="PictureBlob" type="Image"/> </table> </class>
<field /> elements are discussed in depth in section "3.4. Fields - <field />" .
Objects may need to be stored in multiple tables (1.2.7) for efficiency reasons. In the following example we have two tables - one has employee ID, Name, Salary, HireDate and Active flag, the other stores his/her Picture (potentially very large piece of binary data that encodes the person's photo). By storing the picture in a separate table we can avoid reading from it until the data is actually needed.
This is represented by the following fragment of schema:
<class name="Employee"> <table name="Employee"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="Salary" type="Decimal"/> <field name="HireDate" type="DateTime"/> <field name="Active" type="BooleanAsInteger"/> </table> <table name="EmployeePicture"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Picture" type="Image"/> </table> </class>
Each <table /> is a unit for the load operation, which means whenever you access one field from a table, the entire table row gets loaded and is cached for further accesses. This is easy way to implement fine-grained lazy loading (1.2.5) by splitting your fields in groups that are always accessed together.
It is possible to have two <table /> elements that are bound to the same physical database table. You only need to repeat the primary key field in each <table /> element. This allows for column-based lazy loading without the need to create additional database tables. For example, the following Employee class uses mentioned partitioning feature to avoid loading Picture and VoiceSample with other columns.
<class name="Employee"> <table name="Employee"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="Salary" type="Decimal"/> <field name="HireDate" type="DateTime"/> <field name="Active" type="BooleanAsInteger"/> </table> <table name="Employee"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Picture" type="Image"/> </table> <table name="Employee"> <field name="ID" type="Integer" primaryKey="true"/> <field name="VoiceSample" type="Blob"/> </table> </class>
The table definition looks like this (different lazy loading groups are marked with different colors):
Example: We have 4 objects: Adam (Person, with primary key 1), Eva (with primary key 2), Mary (with primary key 3), John (with primary key 4). Let's assume they are represented by the corresponding variable names and see how lazy loading works as we access their properties and display them.
Person adam; Person eva; Person mary; Person john; // 1. read Adam's name - this causes a SELECT on a database. Console.WriteLine(adam.Name); // 2. read Adam's address - database access is not needed because the appropriate row // from the database has been loaded in step #1 Console.WriteLine(adam.Location); // 3. access the picture of Adam - we need to access a database Console.WriteLine(adam.Picture); // 4. we load PictureBlob for Eva (note that neither Name nor Location is loaded yet) Console.WriteLine(eva.Picture); // 5. access the Name of Eva - we load it along with her Location Console.WriteLine(eva.Name); // 6. location already loaded in step #5 - no database read here Console.WriteLine(eva.Location); // 7. read Adam's name - no database access - already loaded in step #1 Console.WriteLine(adam.Name); // 8. read Adam's picture - no database access - already loaded #3 Console.WriteLine(adam.Picture); // 9. read Adam's voice sample - causes a select on a database Console.WriteLine(adam.VoiceSample.Length);
3.4.Fields - <field />
<field /> elements define the mapping between database columns and object properties or fields in relation table. Each field represents a single column in the database.
Usage in a class definition:
<class name="class1"> <table name="table1"> <field name="f1" type="t1" primaryKey="true"/> <field name="f2" type="t2"/> <field name="f3" type="t3"/> <field name="f4" type="t4"/> </table> </class>
Usage in many-to-many relation definition:
<relation name="class1"> <table name="table1"> <field name="f1" type="t1" primaryKey="true"/> <field name="f2" type="t2" primaryKey="true"/> </table> </relation>
The following attributes are available on the <field /> element. Attributes marked with bold are required:
Name | Type | Description |
---|---|---|
name | string | Field name - used by SoodaStubGen (8.1.1) to generate the C#/VB.NET property |
type | FieldDataType | Data type. See the supported data types (3.4.1) for more information. |
dbcolumn | string | Name of the database column. If it is not specified, Sooda assumes this is the same as 'name' |
size | integer | Data type size. |
precision | integer | Data type precision. |
references | string | Name of the class that this field references. The referenced class must have a single-field (i.e. not compound) primary key. It is not possible to automatically generate references to classes with compound primary keys, but it is possible to do it in code. |
onDelete | DeleteAction | The action to be taken when the referenced object is deleted (valid when references is not empty).
|
prefetch | integer | Prefetch level for this field. See the prefetching objects (8.8) section for more information. |
nullable | boolean | Allow this field to hold the null value. Exact representation of the null value (8.4.3) depends on options passed to SoodaStubGen (8.1.1) . |
readOnly | boolean | This field cannot be modified (no property setter is generated). |
forceTrigger | boolean | Always generate triggers (6.8)
for this field even if triggers attribute for the enclosing class is set to false . |
primaryKey | boolean | Mark this field as primary key. Each <table /> (3.3) must have at least one field marked as primary key. |
precommitValue | depends on data type | The value to be used when precommitting (6.10) not-nullable field and the value is not yet provided. Makes sense only for not-null fields. |
find | boolean |
Generate "finder" method for this field. The method will be named FindByNNN where NNN is the name of the field and will return
an instance of the class the field is defined in that has field NNN set to the passed value.
For example, when you declare:
<class name="Class1"> <table name="Table1"> <field name="Id" type="Integer" primaryKey="true"/> <field name="Name" type="String" find="true"/> </table> </class> SoodaStubGen will generate: class Class1 { ... public static Class1 FindByName(string name) { // } ... } |
findList | boolean |
Generate list "finder" method for this field. The method will be named FindListByNNN where NNN is the name of the field and will return
a list of objects that have NNN set to the passed value.
For example, when you declare:
<class name="Class1"> <table name="Table1"> <field name="Id" type="Integer" primaryKey="true"/> <field name="Name" type="String" findList="true"/> </table> </class> SoodaStubGen will generate: class Class1 { ... public static Class1List FindListByName(string name) { // } ... } |
There is no support for compound properties (that would map to more than one field), but it is relatively easy to do it by hand by adding a new property of the required type.
3.4.1.Supported data types
The following data types are supported for the type
attribute of <field /> (3.4)
:
Name | C# Type | Database Type | Comments |
---|---|---|---|
Integer |
| int (32-bit) | For databases that do not support 32-bit integers (such as Oracle) you can use the appropriate numeric type |
Long |
| bigint (64-bit) | For databases that do not support 64-bit integers (such as Oracle) you can use the appropriate numeric type |
Boolean |
| bit | The data provider should return the value in response to calling IDataReader.GetBoolean(). |
BooleanAsInteger |
| int | The database stores true as 1 and false as 0. When reading any value other than zero is considered "true" and zero is considered "false" |
Decimal |
| decimal | The Decimal value type is appropriate for financial calculations requiring large numbers of significant integral and fractional digits and no round-off errors. |
Float |
| float | 32-bit precision floating point |
Double |
| real | 64-bit precision floating point |
DateTime |
|
| Date and/or time value |
String |
|
| Unicode String |
AnsiString |
|
| ANSI String (8-bit characters) |
Blob |
|
| Block of bytes |
Guid |
|
| 128-bit randomly generated unique identifier. GUID is not guaranteed to be unique, but the number of GUIDS (2^128) is so large that the possibility of collision is very small. |
TimeSpan |
|
| Timespan value represented as integral number of seconds. |
Image |
|
| Block of bytes interpreted as image(picture) by the System.Drawing.Image class |
3.5.Named Constants - <const />
The <const /> element defines named constants (8.4.6) to be generated. Each constant has a name and value, whose type depends on the primary key type of this class. Constants are not supported for classes with compound primary keys or where the type of the primary key is other than Integer or String.
<class> ... <const name="..." value="..."/> </class>
Example:
<class name="User"> <table name="User"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> </table> <const name="Administrator" value="1"/> <const name="Guest" value="2"/> <const name="SystemUser" value="2"/> </class>
Once you have defined the constants you may use them instead of calling classname.GetRef(). Note that you cannot pass the instance of SoodaTransaction (6.1) so constants are limited to be used in the context of implicit transaction.
User.GetRef(1) == User.Administrator User.GetRef(2) == User.Guest User.GetRef(3) == User.SystemUser
3.6.One-To-Many Relationships - <collectionOneToMany />
One-to-many relationships are defined using <collectionOneToMany /> which needs to be placed inside <class /> element.
<class name="className"> <field/> <field/> <field/> <field/> <collectionOneToMany name="COLLECTION_NAME" class="CHILD_CLASS" foreignField="FOREIGN_FIELD"/> </class>
In short, it defines a collection named COLLECTION_NAME whose items are of class CHILD_CLASS where the value of FOREIGN_FIELD equals to this object's primary key. In other words this is a list of objects referencing this object through the specified reference field.
Sooda also supports additional attribute named where
which can be used to define where clause to
be used when fetching objects. This can be used to create filtered collections:
<class name="Group"> <collectionOneToMany name="Members" class="Person" foreignField="Group"/> <collectionOneToMany name="VipMembers" class="Person" foreignField="Group" where="IsVip = True"/> </class>
The above example defines two collections in the Group class: one named Members
holds all members of
the group, the other is named VipMembers
and holds only members who are VIPs.
CAUTION: Collections defined with "where" are not recommended because they do not automatically reflect the state of child objects when you manually change the foreignField. Collections without where clause do not exhibit this problem.
The details about generated code can be found in section "8.4.4. Collections" .
3.7.Many-To-Many Relationships - <collectionManyToMany /> and <relation />
Many-to-many relationships are defined using <collectionManyToMany /> in cooperation with the <relation />. The <collectionManyToMany /> needs to be placed inside <class /> element and <relation /> needs to be placed inside <schema /> element.
<schema> <class name="className"> ... <collectionManyToMany name="COLLECTION_NAME" relation="RELATION" foreignField="FOREIGN_FIELD"/> </class> <relation name="RELATION"> <table name="RELATION_TABLE"> <field name="f1" type="t1" primaryKey="true" references="R1"/> <field name="f2" type="t2" primaryKey="true" references="R2"/> </table> </relation> </schema>
The <relation /> element defines a relation (table) that resolves many-to-many relationships. It must include exactly two fields, which are references to other tables.
Each record in this table represents a relationships between two objects pointed at by the fields. For example, assuming we have a relation Contact2Group with 2 columns: contact_id and group_id and it contains a row where contact_id=1 and group_id=7 it means that contact[1] and group[7] are in (some) relationship. The name of the relationship is established by <collectionManyToMany />.
<relation name="Contact2Group"> <table name="Contact2Group"> <field name="TheContact" type="Integer" references="Contact" primaryKey="true"/> <field name="TheGroup" type="Integer" references="Group" primaryKey="true"/> </table> </relation>
Each side of the relation has a multiplicity of "many", which means each contact may be in relationship with many groups and each group can be in relationship with many contacts. This is represented by a pair of strongly typed collections.
The collections are declared using <collectionManyToMany />.
class Contact { GroupList Groups { get; } } class Group { ContactList Members { get; } }
The <collectionManyToMany /> declaration accepts the following attributes:
Name | Type | Description |
---|---|---|
name | string | Collection name |
relation | string | Name of the relation (table) that represents the relationship. |
foreignField | string | Name of the field defined in <relation /> that points back at the
object that declares the collection. For example, considering the Contact2Group relation defined above,
you should put this in <class name="Contact" /> to get Contact.Groups collection:
<class name="Contact"> ... <collectionManyToMany name="Groups" relation="Contact2Group" foreignField="TheContact"/> </class>Symmetrically, you should put this in <class name="Group" /> to get the Group.Members collection: <class name="Group"> ... <collectionManyToMany name="Members" relation="Contact2Group" foreignField="TheGroup"/> </class>You can of course choose not to declare collections. This is a way to get unidirectional relationships. |
masterField | integer (0 or 1) | The ordinal of the field defined in <relation /> that points at the referenced object.
Specifying this attribute is equivalent to providing the appropriate value of foreignField, but
the meaning is reversed. The following declaration declares Contact.Groups and Group.Members collections:
Then you should put this in <class name="Contact" /> to get Groups collection:
<class name="Contact"> ... <collectionManyToMany name="Groups" relation="Contact2Group" masterField="1"/> </class>Symmetrically, you should put this in <class name="Group" /> to get the Members collection: <class name="Group"> ... <collectionManyToMany name="Members" relation="Contact2Group" masterField="0"/> </class>OBSOLETE WARNING: It is recommended not to use masterField, but use foreignField instead. masterField is provided only for compatibility purposes and may be removed in future versions. |
The details about generated code can be found in in section "8.4.4. Collections" .
3.8.Include Files - <include />
Schemas can include one another. This is a way to split complex domain models into many manageable files which are subject to separate compilation and code generation.
You need to use the <include /> to include another schema. The following attributes are supported (attributes marked with bold are required).
Name | Type | Description |
---|---|---|
schema | string | Path to the included schema. The path is relative to the directory where the including schema is located. |
namespace | string | Namespace of the included schema. Sooda will generate the appropriate using clause. |
assembly | string | Name of the assembly (without *.dll extension) that contains classes generated from the schema |
Example usage (assuming that SoodaSchema.xml is located in c:\MyProject the included file should be located in c:\MyProject\SharedBusinessObjects\SoodaSchema.xml):
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <include schema="SharedBusinessObjects/SoodaSchema.xml" namespace="MyCompany.SharedBusinessObjects" assembly="MyCompany.SharedBusinessObjects"/> </schema>
3.9.Inheritance
In order to define inheritance relationship (1.2.8) you need to designate one class which will serve as a root of the inheritance hierarchy and make other inherit (directly or indirectly) from it. Attributes which control inheritance are specified at the <class /> level. They are:
Name | Type | Description |
---|---|---|
subclassSelectorField | string |
Specifies the name of the subclass selector field that determines this object type. The
field must be defined in this <class />, in the first <table /> and must
be either integer or string. Based on the value stored in this field Sooda is
able to instantiate objects of the appropriate class in a polymorphic manner.
Values stored in this field correspond to concrete (non-abstract) classes and are defined
using The class that defines subclassSelectorField is named the root of inheritance hierarchy. You cannot put subclassSelectorField in any class that inherits from another class (to form sub-hierarchies). This feature might be added in future versions of Sooda. |
inheritFrom | string |
Inherits from the specified class. The class must either be the root of the inheritance hierarchy
or inherit (directly or indirectly) from such a class. It is an error to inherit from a class
that does not ultimately derive from a root class (that has subclassSelectorField defined).
|
subclassSelectorValue | integer or string | Specifies the value of the subclass selector field that determines this object type. This parameter must be is specified for each concrete (non-abstract) class in the inheritance hierarchy. Sooda will instantiate objects of the appropriate type based on a value of the subclassSelectorField. If the value is equal to subclassSelectorValue for a particular class, an instance of this class will be created to represent the object. If you do not specify the value for subclassSelectorValue, SoodaStubGen (8.1.1.5) will generate an abstract class that cannot be instantiated programmatically. Values provided in subclassSelectorValue are also used in polymorphic queries to limit the selected objects to instances of a particular class. For example if you get a list of objects of a particular class you may get objects of this class (provided that they are not abstract) and all non-abstract classes that derive from it, either directly or indirectly. |
The following table summarizes possible combinations of the above attributes:
subclassSelectorField | subclassSelectorValue | inheritFrom | Description |
---|---|---|---|
not defined | not defined | not defined | A class that does not participate in inheritance hierarchy |
defined | defined | not defined | Root non-abstract class. Can have subclasses. |
defined | not defined | not defined | Root abstract class (cannot create instances). Can have subclasses. |
not defined | not defined | defined | Non-root abstract class. Can have subclasses. |
not defined | defined | defined | Non-root non-abstract class. Can have subclasses. Can create instances. |
defined | (any) | defined | Not allowed. Cannot define a class that has both subclassSelectorField and inherits from another class |
not defined | defined | not defined | Not allowed. When providing subclassSelectorValue you need to also provide subclassSelectorField or inheritFrom |
Sooda supports two models of inheritance mapping: table-per-hierarchy (TPH) and table-per-subclass (TPS) as described in section "1.2.8. Inheritance" . They only differ in actual table names used to store fields in subclasses, so the configuration is similar. Assuming the familiar table of WorkItems structure we can map it to classes using the provided schema:
The schema that maps this to a class hierarchy using TPS model is:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource name="default" type="Sooda.Sql.SqlDataSource"/> <!-- Root class of the inheritance hierarchy. --> <!-- Note that it is abstract because it does not define subclassSelectorValue --> <class name="WorkItem" subclassSelectorField="Type"> <table name="WorkItem"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Type" type="Integer"/> <field name="AssignedTo" type="Integer"/> <field name="AssignedDate" type="DateTime"/> <field name="DueDate" type="DateTime"/> </table> </class> <!-- Concrete class stored in QuestionWorkItem table, identified by Type=1 --> <class name="QuestionWorkItem" subclassSelectorValue="1"> <table name="QuestionWorkItem"> <!-- primary key must be repeated for each table --> <field name="ID" type="Integer" primaryKey="true"/> <field name="Question" type="String" size="40"/> <field name="Answer" type="String" size="40"/> </table> </class> <!-- Concrete class stored in AnalysisWorkItem table, identified by Type=2 --> <class name="AnalysisWorkItem" subclassSelectorValue="2"> <table name="AnalysisWorkItem"> <!-- primary key must be repeated for each table --> <field name="ID" type="Integer" primaryKey="true"/> <field name="Subject" type="String" size="40"/> </table> </class> <!-- Concrete class stored in BugWorkItem table, identified by Type=3 --> <class name="BugWorkItem" subclassSelectorValue="3"> <table name="BugWorkItem"> <!-- primary key must be repeated for each table --> <field name="ID" type="Integer" primaryKey="true"/> <field name="BugID" type="Integer"/> <field name="BugDescription" type="String" size="40"/> </table> </class> </schema>
The mapping in TPH model is very similar. You only need to change the table names, so that they are all "TPH_WorkItem". We will also rename fields by prefixing them with acronyms of concrete subclasses they belong to:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource name="default" type="Sooda.Sql.SqlDataSource"/> <!-- Root class of the inheritance hierarchy. --> <!-- Note that it is abstract because it does not define subclassSelectorValue --> <class name="WorkItem" subclassSelectorField="Type"> <table name="TPH_WorkItem"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Type" type="Integer"/> <field name="AssignedTo" type="Integer"/> <field name="AssignedDate" type="DateTime"/> <field name="DueDate" type="DateTime"/> </table> </class> <!-- Concrete class stored in TPH_WorkItem table, identified by Type=1 --> <class name="QuestionWorkItem" subclassSelectorValue="1"> <table name="TPH_WorkItem"> <!-- primary key must be repeated for each table --> <field name="ID" type="Integer" primaryKey="true"/> <field name="Question" dbcolumn="QWI_Question" type="String" size="40"/> <field name="Answer" dbcolumn="QWI_Answer" type="String" size="40"/> </table> </class> <!-- Concrete class stored in TPH_WorkItem table, identified by Type=2 --> <class name="AnalysisWorkItem" subclassSelectorValue="2"> <table name="TPH_WorkItem"> <!-- primary key must be repeated for each table --> <field name="ID" type="Integer" primaryKey="true"/> <field name="Subject" dbcolumn="AWI_Subject" type="String" size="40"/> </table> </class> <!-- Concrete class stored in TPH_WorkItem table, identified by Type=3 --> <class name="BugWorkItem" subclassSelectorValue="3"> <table name="TPH_WorkItem"> <!-- primary key must be repeated for each table --> <field name="ID" type="Integer" primaryKey="true"/> <field name="BugID" dbcolumn="BWI_BugID" type="Integer"/> <field name="BugDescription" dbcolumn="BWI_BugDescription" type="String" size="40"/> </table> </class> </schema>
3.10.Precommit Values
There are situations where Sooda needs to temporarily store an object in a database. This is called precommit and is described in section "6.10. Precommitting objects in transactions" .
Typically Sooda objects can be stored in a database without problems. One notable exception are not-nullable fields which are null because there were not initialized yet.
In new Sooda objects, not-nullable fields of simple types are initialized with "zero" values:
Data Type | "Zero" Value |
---|---|
Integer | 0 |
Long | 0 |
Boolean | false |
BooleanAsInteger | 0 |
Decimal | 0.0 |
Float | 0.0 |
Double | 0.0 |
DateTime | 00:00:00.0000000, January 1, 0001 (DateTime.MinValue) |
String | (empty string) |
AnsiString | (empty string) |
Blob | (zero-length blob) |
Guid | 00000001-0000-0000-0000-000000000000 |
TimeSpan | 00:00:00 |
Image | (zero-length blob) |
Note that not-nullable string fields are initialized with an empty string instead of a null.
Not-nullable reference fields cannot be initialized and pose a problem to temporarily storing an object in a database. Historically Sooda includes highly-configurable solution to this problem called precommit values. At the schema level you can choose one of the strategies:
<defaultPrecommitValues>null</defaultPrecommitValues> is recommended because of its simplicity. Below we describe the whole mechanism of configuring precommit values.
There are four levels of precommit values, listed in the order of precedence:
Per-field precommit values are defined on each <field /> using precommitValue attribute. The following example defines a precommitValue for the User.Manager field.
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource name="default" type="Sooda.Sql.SqlDataSource"/> <class name="User" defaultPrecommitValue="-1"> <table name="_User"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="Manager" type="Integer" references="User" precommitValue="1111"/> </table> </class> </schema>
In order to define per-class precommit value, you use the defaultPrecommitValue attribute on a class. Typically you use this feature when your database uses foreign key constraints and defaultPrecommitValue is a value of an object that is known to exist in the table.
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource name="default" type="Sooda.Sql.SqlDataSource"/> <class name="User" defaultPrecommitValue="-1"> <table/> </class> </schema>
You can use <precommitValue /> elements in your schema to define per-datatype precommit value. This example defines a precommit value of -1 for Integer fields, ***NONE*** string for all String fields and ***NOTHING*** for all AnsiString fields. You should pick a value that you do not commonly search for in your queries.
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource name="default" type="Sooda.Sql.SqlDataSource"/> <precommitValue type="Integer" value="-1"/> <precommitValue type="String" value="***NONE***"/> <precommitValue type="AnsiString" value="***NOTHING***"/> </schema>
4.Compilation
4.1.Compilation Process
Sooda provides a utility called SoodaStubGen (8.1.1) that generates stub source code for the Data Access Layer. The generated source code needs to be compiled with a standard C#/VB compiler to produce a working assembly. This section describes the compilation process in detail.
It is recommended to run SoodaStubGen as part of your (preferably fully automated and repeatable) build process so Sooda supports calling stub generator from a batch file, NAnt script or Visual Studio 2005/2008/2010 solution.
Sooda supports two compilation modes:
Separate compilation of stubs is difficult because of cyclic dependency between stub classes and skeleton classes, which exists because:
The following figure describes the problem. Note how Book_Stub class must be able to return references to Person class and Person_Stub must be able to return Book objects. At the same time Book inherits from Book_Stub and Person inherits from Person_Stub:
CAUTION Separate Stub Compilation relies on the ability of C# compiler to compile DLLs which reference each other (cyclic dependencies). SoodaCompileStubs is used to ensure the proper bootstrapping of such inter-dependent DLLs. Microsoft C# compiler is supported but Mono Project Compiler (mcs) is not because it is unable to properly handle cyclic references.
4.1.1.Standard Stub Compilation
Standard Stub Compilation is a process of building Sooda-based data access library, which uses two utilities:
The following figure provides an overview of the standard stub compilation process:
On input the process requires an XML document with O/R Mapping Schema (3)
, typically
stored in a file named SoodaSchema.xml
. Arguments to SoodaStubGen (8.1.1)
can be either passed on the command line or through a special XML project file (4.2)
which
is easier to manage.
Build steps to produce business objects assembly are:
ClassName.cs
where ClassName is the name of the class.
The skeleton is only generated in the first run and is never automatically regenerated
You can add your own properties and business methods in the skeleton and your changes will be preserved
when rebuilding project. "Skeleton" inherits from a "stub" class so you get access to all its
properties and methods.
4.1.2.Separate Stub Compilation
Separate Stub Compilation is a process in which you precompile stubs to a separate DLL and rebuild the DLL only when the schema changes. This can save compile time for very large schemas and can also make Visual Studio run faster (for some reason Visual Studio 2005 does not perform very well on very large source files which SoodaStubGen creates).
The following utilities are required:
The following figure provides an overview of the separate stub compilation process:
The steps of the separate stub compilation are:
OutputAssemblyName.Stubs.dll
from _MiniStubs.csx
.
The DLL references Sooda.dll only.OutputAssemblyName.dll
from _MiniSkeleton.csx
referencing
OutputAssemblyName.Stubs.dll
built in the previous step.OutputAssemblyName.Stubs.dll
from _Stubs.csx
replacing
the DLL built in the first step. The newly compiled DLL references OutputAssemblyName.dll
built in the
second step.
4.1.3.Files used during the compilation process
The following files are created or used during the compilation process:
Name | Location | Provided by user | Generated | Description |
---|---|---|---|---|
SoodaSchema.xml | any | Yes | Yes/No | Defines mapping schema (3) . Can be written by hand or reverse-engineered from an existing database with the SoodaSchemaTool utility (8.1.1.5) . |
ProjectName.soodaproject | any | Yes | No | Defines code generation parameters. The format of this file is described in section "4.2. Sooda Project Files - *.soodaproject" . You may choose to not use this file, but pass all code generation parameters as command line arguments to SoodaStubGen utility (8.1.1) instead. |
_DBSchema.bin | output directory | No | Yes | Binary version of SoodaSchema.xml. Enables access to schema-level metadata at runtime. Must be embedded into the assembly that contains business objects. Optimized for fast loading and small size by using binary serialization. |
_Stubs.cs | output directory | No | Yes | Generated code for stub classes, list wrappers, factories and other opaque types that should be compiled together to build a Data Access Layer. More information about the generated code is available in the Generated Code section (8.4) . The exact extension of this file matches the selected language and is cs for C# and vb for VB.NET. Other CodeDOM providers have their specific extensions. |
ClassName.cs | output directory | Yes | Yes | Skeleton classes. They are generated by SoodaStubGen (8.1.1) the first time it is executed and are not regenerated later. You can put your business methods in these files. There is one skeleton class for each <class /> as defined in the mapping schema (3) . Each class N is stored in a file named "output directory/N.cs" (for C#) and this cannot be changed by the user. |
_Stubs.csx | output directory/Stubs | No | Yes | This file is used only in separate stubs compilation mode Generated code for stub classes, list wrappers, factories and other opaque types that should be compiled together to build a Data Access Layer stubs library. More information about the generated code is available in the Generated Code section (8.4) . |
_MiniStubs.csx | output directory/Stubs | No | Yes | This file is used only in separate stubs compilation mode Miniature version of generated code for stub classes, list wrappers, factories. Miniature stub classes do not rely on skeleton classes so they can be compiled separately during the separate compilation bootstrap procedure (4.1.2) . |
_MiniSkeleton.csx | output directory/Stubs | No | Yes | This file is used only in separate stubs compilation mode Miniature version of skeleton classes. The skeleton classes have no code other than constructors and they inherit from the appropriate stub classes. This is a temporary file and is used during the separate compilation bootstrap procedure (4.1.2) . |
_Stubs.csx | output directory/Stubs | No | Yes | This file is used only in separate stubs compilation mode Same as _Stubs.cs, but located in _Stubs.csx. This file is compiled to create the stubs assembly. |
OutputAssemblyName.Stubs.dll | output directory/Stubs | No | Yes | This file is used only in separate stubs compilation mode The resulting stubs assembly. You should reference this file each time you reference OutputAssemblyName.dll. |
OutputAssemblyName.dll | output directory/Stubs | No | Yes | This file is used only in separate stubs compilation mode Mini-skeletons assembly. Should not be removed or your application will not build properly. |
4.1.4.NAnt
Sooda supports NAnt tasks that can be used to automate the build process. They are contained in the Sooda.NAnt.Tasks library and must be loaded before they can be used:
<project> <loadtasks assembly="${sooda.dir}/Sooda.NAnt.Tasks.dll"/> </project>
<sooda-generate-code /> task is an equivalent of the SoodaStubGen utility (8.1.1) . Usage:
<project> <loadtasks assembly="${sooda.dir}/Sooda.NAnt.Tasks.dll"/> <sooda-generate-code project="path/to/proj.soodaproject"/> </project>
This is equivalent to calling SoodaStubGen with the name of the soodaproject (4.2) file as an argument:
<project> <exec program="${sooda.dir}/SoodaStubGen.exe" commandline="path/to/proj.soodaproject"/> </project>
Once you have generated the stubs you should compile the skeleton classes, stub classes and embedded resources into a DLL. <csc /> task should be used for that. The following example is taken from Sooda unit tests:
<csc target="library" output="${sooda.dir}/Sooda.UnitTests.BaseObjects.dll" debug="true"> <references> <include name="${sooda.dir}/Sooda.dll"/> <include name="System.Data.dll" asis="true"/> </references> <sources basedir="tests/BaseObjects"> <include name="*.cs"/> </sources> <resources basedir="tests/BaseObjects" prefix="Sooda.UnitTests.BaseObjects" dynamicprefix="true"> <include name="_DBSchema.*"/> </resources> </csc>
4.1.5.Visual Studio
Automating Visual Studio builds using Sooda relies on the Pre-Build Event
setting which invokes
SoodaStubGen.exe (8.1.1)
and optionally SoodaCompileStubs.exe (8.1.1.3)
.
It is possible to pass arbitrary command-line arguments to these commands, but it is recommended to use
Sooda Project Files (4.2)
which make this process a simpler to manage.
To set the Pre-Build Event command line, open project properties in Visual Studio and locate "Build Events
".
Recommended Pre-Build Event command line is (for .NET 2.0 / Visual Studio 2005)
"%SOODA_DIR%\bin\net-2.0\SoodaStubGen.exe" "$(ProjectName).soodaproject"
SOODA_DIR environment variable must be set properly (it is automatically initialized by the installer). You may want to adjust the path if you installed Sooda manually in some other location.
To call SoodaCompileStubs (8.1.1.3) from within Visual Studio, add the appropriate code:
"%SOODA_DIR%\bin\net-2.0\SoodaCompileStubs.exe" $(ProjectName) "$(ProjectDir)Stubs" "$(ProjectDir)AssemblyInfo.cs"
4.2.Sooda Project Files - *.soodaproject
Sooda Project Files are used to control the generation of stub source code with the SoodaStubGen utility (8.1.1) . Project files are easier to read and maintain than command-line arguments passed to SoodaStubGen. Visual Studio supports Intellisense so editing them is easy, even without documentation.
Sooda Project file must be saved in a file with *.soodaproject
extension.
It is an XML file, whose root element is <sooda-project /> and its
XML Namespace is http://www.sooda.org/schemas/SoodaProject.xsd
<sooda-project xmlns="http://www.sooda.org/schemas/SoodaProject.xsd"> <!-- parameters go here --> </sooda-project>
The following table describes elements which can be used in Sooda Project files:
XML Element | Type | Description |
---|---|---|
schema-file | string | Path to the file that defines the mapping schema (3)
.
Example: <schema-file>MySoodaSchema.xml</schema-file> |
language | string | Specifies the programming language. Possible (case insensitive) values are:
The default is C#. Examples: C#: <language>c#</language> VB.NET: <language>vb</language> Boo: <language>Boo.Lang.CodeDom.BooCodeProvider, Boo.Lang.CodeDom</language> |
output-assembly | string | Name of the output assembly without the DLL extension.
Example: <output-assembly>MyAssembly</output-assembly> |
output-namespace | string | Name of the output namespace.
Example: <output-namespace>MyNamespace</output-namespace> |
output-path | string | Output path. By default it is "." which represents current working directory.
Example: <output-path>src\MyBusinessObjects</output-path> |
nullable-representation not-null-representation | string | Specifies how nullable and not-nullable fields are mapped to properties.
Possible methods are discussed
in section "4.2.1. Primitive Representations"
.
Default value for nullable representation is SqlType and default not-null representation is Raw. Example: <nullable-representation>Boxed</nullable-representation> |
null-propagation | boolean | Enable null propagation for nullable properties and reference properties: <null-propagation>true</null-propagation> Enabling this option causes the following prolog code to be emitted for all property getters: if (this == null) return null;
Without this feature we would get a
OrgUnit o = this.OrganizationUnit.Parent.Parent.Parent;
Assuming that This feature is experimental and should be used with caution. |
loader-class | boolean | Generate Load(), GetRef(), GetList(), FindByXXX() and FindListByXXX() static methods in a separate class called ClassNameLoader instead of ClassName. <loader-class>true</loader-class> Some languages such as VJ# do not properly support inherited classes having the same public static method with different return type (which would be needed to support inheritance in Sooda). To remedy this problem, this option causes the methods to be moved to a separate class.
When <loader-class /> is set to Contact.Load(10); Contact.GetRef(10); Contact.TryGet(10); Contact.GetList(whereClause); Contact.LoadSingleObject(whereClause);
When <loader-class /> is set to ContactLoader.Load(10); ContactLoader.GetRef(10); ContactLoader.TryGet(10); ContactLoader.GetList(whereClause); ContactLoader.LoadSingleObject(whereClause); |
with-typed-queries | boolean | This option causes the SoodaStubGen utility (8.1.1)
to
emit SQOL typed queries (7.5)
. By default it is <with-typed-queries>true</with-typed-queries> |
stubs-compiled-separately | boolean | Enables separate compilation of stubs (4.1.2) . <stubs-compiled-separately>true</stubs-compiled-separately> |
file-per-namespace | boolean | Write each namespace to a separate file:
When this option is set to false, all files are written to <file-per-namespace>true</file-per-namespace> |
base-class-name | string | Specifies the name of the class that all stubs should ultimately derive from. If you do not specify this parameter, all root stubs classes will derive from the SoodaObject class. <base-class-name>AdditionalBaseClass</base-class-name> More ways to override base classes are discussed in section "8.4.2. Class hierarchy" . |
embed-schema type | Xml|Binary | Specifies the method of embedding schema in the result DLL. Can be either <embed-schema>Binary</embed-schema> |
external-projects | array | Defines external projects (such as Visual Studio projects) that need to be updated each time a new class is added to the schema. Each project is represented by <project type="..." file="..." />. The following example XML defines a Visual Studio 2005 project: <external-projects> <project type="vs2005" file="MyProject.vs2005.csproj"/> </external-projects> The paths are relative to the output directory. The following project types are available:
Note that files (such as source files and embedded resources) can only be added to projects, they are not removed automatically. If you remove a class from the mapping schema (3) , you need to manually remove the appropriate file from your project. |
4.2.1.Primitive Representations
nullable-representation
and not-null-representation
:
Name | Description |
---|---|
Boxed | Use boxing to represent nullable values. Null value is represented
as null reference, but a cast is required to get to get the value.
This representation is not recommended because of the lack of the type safety.
// test for null if (x.Field1 != null) { // retrieve non-null value int v = (int)x.Field1; // WARNING - no type checking at compilation time occurs // so the following will compile, but will fail at runtime: bool b = (bool)x.Field1; // set to null x.Field1 = null; // set to not-null x.Field1 = 42; } |
Raw | Represent nullable fields as properties of primitive types (int,bool,DateTime)
completely ignoring their null value. This is suitable for not-null fields,
but may be a bad idea for nullable fields.
// retrieve non-null value int v = (int)x.Field1; // set to not null x.Field1 = 42; |
RawWithIsNull | Represent nullable fields as a pair of properties - one is of primitive
type and returns the value of the field, the other called FieldName_IsNull returns the
null flag for the field.
// test for null if (!x.Field1_IsNull) { // retrieve non-null value int v = x.Field1; // set to null x._SetNull_Field1(); // set to not-null x.Field1 = 42; } |
SqlType |
Represent nullable fields as the corresponding SqlTypes (value types from the System.Data.SqlTypes namespace).
The following SqlTypes are available:
// test for null if (!x.Field1.IsNull) { // retrieve non-null value int v = x.Field1.Value; // set to null x.Field1 = SqlInt32.Null; // set to not-null x.Field1 = 42; } |
Nullable |
Use
// test for null if (x.Field1 != null) { // retrieve non-null value, no other casts are allowed here int v = (int)x.Field1; // set to null x.Field1 = null; // set to not-null x.Field1 = 42; } |
5.Runtime configuration
Some aspects of Sooda-generated Data Access Library can be controlled at runtime. They are:
Configuration parameters are typically stored in configuration files (5.1) , but there are other possibilities as explained later in this section.
5.1.Configuration Files
Sooda configuration files are usually managed by the .NET configuration mechanism and the parameters
are stored in application configuration files (App.config
) as described
in section "5.1.1. App.config - style configuration"
.
If you have many programs that share runtime configuration, Sooda can be configured from a
shared XML configuration file. This feature is described
in section "5.1.2. Shared XML configuration"
.
If you have developed your own configuration mechanism, you can write a specialized Sooda
configuration provider for it as described
in section "5.1.3. Custom configuration"
.
5.1.1.App.config - style configuration
By default Sooda configuration parameters are stored in standard .NET configuration files named
ApplicationName.exe.config
or Web.config
in a section named <appSettings />.
More information about .NET Configuration files is available in MSDN Library (http://msdn.microsoft.com/en-us/library/1fk1t1t0.aspx).
Each data source configuration entry has a form:
<add name="dataSourceName.parameterName" value="parameterValue"/>
dataSourceName
is the name of the datasource (usually "default
") and parameterName
is the name of the setting for that datasource. The value of the setting is stored in parameterName
.
Possible setting names are described
in section "5.2. Data Source Configuration"
.
Some global options can also be set this way. Global options use the "sooda
" prefix instead of a data source name.
They are described
in section "5.3. Global Configuration"
.
The following configuration file defines two settings for the "default" data source: connectionString
and sqlDialect
.
<configuration> <appSettings> <add key="default.connectionString" value="Integrated Security=true;Server=.;Database=MyDatabase"/> <add key="default.sqlDialect" value="mssql"/> </appSettings> </configuration>
5.1.2.Shared XML configuration
There are situations where multiple applications need to share the same set of configuration parameters. Instead of keeping multiple copies of Sooda-specific configuration, you can store the settings in a single XML file which is read by the applications.
The format of the file is very simple. Each parameter is enclosed in an XML element and nesting is used to group parameters for a single datasource. The root element of the configuration file is not important, which means you can embed the Sooda-specific XML elements in any XML file as long as you can add your own elements.
The following example demonstrates XML configuration file equivalent to the App.config shown above.
<configuration> <default> <connectionString> Integrated Security=true;Server=.;Database=MyDatabase </connectionString> <sqlDialect> mssql </sqlDialect> </default> </configuration>
XML config files also support per-host overrides. Assuming the configuration file is
read from sooda.config.xml
, Sooda looks for sooda.config.HOSTNAME.xml
where HOSTNAME
is the name of the machine the application is running on. If the
host-specific configuration file is found, Sooda reads it as well and any settings specified in this
file take precedence over the common configuration.
Sooda looks for an XML configuration file in the application base directory. If the file is not found
the parent directory is scanned, then its grandparent and so on until the file is found or until
ten levels of directory hierarchy have been scanned or the root directory has been reached.
Assuming that your application is stored in c:\myprojects\mysystem1\app1\bin\Debug\app.exe
and the name of the configuration file is: sooda.config.xml
,
and Sooda will look for it in the following locations:
As you can see, this is convenient because you can keep the settings for all applications making up
entire system in a single file (c:\myprojects\mysystem1\sooda.config.xml
) and
all applications will be able to read it.
To use XML configuration files, add this line of code to the AssemblyInfo.cs
file of your entry assembly (http://msdn2.microsoft.com/en-us/library/system.reflection.assembly.getentryassembly.aspx).
using Sooda; [assembly: SoodaConfig(XmlConfigFileName="sooda.config.xml")]
Alternatively you can add these two entries in the application configuration file:
<configuration> <appSettings> <add key="sooda.config" value="xmlconfig"/> <add key="sooda.xmlconfigfile" value="sooda.config.xml"/> </appSettings> </configuration>
By default the HOSTNAME part of the file name is the name of the machine as returned by Environment.MachineName
, but you can add the sooda.hostname
application configuration setting to override it:
<configuration> <appSettings> <add key="sooda.hostname" value="ALTERNATIVEHOSTNAME"/> </appSettings> </configuration>
5.1.3.Custom configuration
If you have developed your own configuration mechanism, you can get Sooda to use it by creating a class
that implements the Sooda.Config.ISoodaConfigProvider
interface and telling Sooda to use it.
The interface has only one method: string GetString(string)
which must read a configuration
setting with the specified name. The setting name is of the form dataSourceName.parameterName
.
To register your configuration provider in a declarative manner, you can either use an assembly-level attribute
or an application configuration file entry. Assuming you have created a class MyProvider
that implements ISoodaConfigProvider
interface, just add the following code to your AssemblyInfo.cs
:
using Sooda; [assembly: SoodaConfig(ProviderType=typeof(MyNamespace.MyProvider))]
If you prefer to use the application configuration file, add the sooda.config
entry containing
the fully qualified name of the type (including assembly name, version and public key token).
<configuration> <appSettings> <add key="sooda.config" value="MyNamespace.MyProvider, MyAssembly, Version=..., PublicKeyToken=..."/> </appSettings> </configuration>
To register the config provider programmatically, just call SoodaConfig.SetConfigProvider()
in your Main()
method:
using Sooda; using Sooda.Config; class Program { public static void Main(string[] args) { // create the config provider object ISoodaConfigProvider provider = new MyNamespace.MyProvider(); // make it active SoodaConfig.SetConfigProvider(provider); } }
5.2.Data Source Configuration
You can configure your data source either declaratively using the configuration file (5.1)
or in code by properties of the SqlDataSource
objects.
Configuration setting | Property | Description |
---|---|---|
sqlDialect | SqlBuilder | Controls the database-specific SQL generation. The following values can be specified:
Example: <configuration> <appSettings> <add key="default.sqlDialect" value="postgresql"/> </appSettings> </configuration>
To change the dialect in code, set the
Example: using (SqlDataSource sds = new SqlDataSource("default")) { sds.SqlBuilder = new MySqlBuilder(); // change other properties before calling Open() sds.Open(); } |
commandTimeout | CommandTimeout | Specifies the maximum amount of time (in seconds) that SQL queries are allowed to execute before they are terminated.
The default value of <configuration> <appSettings> <add key="default.commandTimeout" value="60"/> </appSettings> </configuration> |
queryTimeTraceInfo queryTimeTraceWarn | QueryTimeTraceInfo QueryTimeTraceWarn |
Control the SQL trace output depending on query time. If the query execution time is
below
The default value of <configuration> <appSettings> <add key="default.queryTimeTraceInfo" value="0.5"/> <add key="default.queryTimeTraceWarn" value="1.0"/> </appSettings> </configuration> |
disableTransactions | DisableTransactions |
Disables the use of SQL transactions. Setting this to Example: <configuration> <appSettings> <add key="default.disableTransactions" value="true"/> </appSettings> </configuration> |
stripWhitespaceInLogs | StripWhitespaceInLogs |
When this parameter is set to Example: <configuration> <appSettings> <add key="default.stripWhitespaceInLogs" value="true"/> </appSettings> </configuration> |
indentQueries | IndentQueries |
Specifies whether queries sent to the RDBMS should be indented with
the whitespace characters to represent their nesting levels.
Setting this to Example: <configuration> <appSettings> <add key="default.indentQueries" value="true"/> </appSettings> </configuration> |
useSafeLiterals | UseSafeLiterals |
Controls the use of ADO.NET parameters. When this parameter is set to
The following literals can be passed unmodified:
All other literals (especially datetime literals) are always passed as ADO.NET parameters. The following example enables the use of safe literals: <configuration> <appSettings> <add key="default.useSafeLiterals" value="true"/> </appSettings> </configuration> |
disableUpdateBatch | DisableUpdateBatch |
Disables batched updates. If this parameter is set to When batched updates are enabled, SQL can send multiple semicolon-separated DML instructions. When you insert five objects in a single transaction, Sooda might persist them in a single batch: INSERT INTO T1(id,name) values(100,'aaa'); INSERT INTO T1(id,name) values(101,'aaa'); INSERT INTO T1(id,name) values(102,'aaa'); INSERT INTO T1(id,name) values(103,'aaa'); INSERT INTO T1(id,name) values(104,'aaa'); When query batching is not enabled, five round trips to the server are required which may degrade performance: INSERT INTO T1(id,name) values(100,'aaa'); INSERT INTO T1(id,name) values(101,'aaa'); INSERT INTO T1(id,name) values(102,'aaa'); INSERT INTO T1(id,name) values(103,'aaa'); INSERT INTO T1(id,name) values(104,'aaa'); The following example disables batched updates: <configuration> <appSettings> <add key="default.disableUpdateBatch" value="true"/> </appSettings> </configuration> |
connectionType | ConnectionType | Specifies fully qualified name of the data provider connection type. For example, to connect to PostgreSQL database using Npgsql driver use:
The default value is <configuration> <appSettings> <add key="default.connectionType" value="Npgsql.NpgsqlConnection, Npgsql, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=5d8b90d52f46fda7"/> </appSettings> </configuration> |
connectionString | ConnectionString | Specifies database connection string. Example: <configuration> <appSettings> <add key="default.connectionString" value="server=127.0.0.1;user id=jaak;password=jaak;database=SoodaUnitTests;encoding=unicode"/> </appSettings> </configuration> |
upperLike | UpperLike |
When this parameter is |
5.3.Global Configuration
Sooda has some options which are not data source specific. These options have the name prefix sooda
.
Configuration setting | Description |
---|---|
sooda.connectionopenretries | Specifies number of tries of opening a database connection.
If the data provider's |
sooda.hostname |
Overrides the HOSTNAME part of the configuration file, which is normally the machine name
as returned by Example: <configuration> <appSettings> <add key="sooda.hostname" value="ALTERNATIVEHOSTNAME"/> </appSettings> </configuration> |
sooda.innerjoins |
Enables use of |
sooda.oracleClientAutoCommitBugWorkaround | Enables a workaround for a bug in Microsoft's OracleClient in .NET 3.5 SP1. The bug causes transactions to be autocommitted in certain conditions. The bug and the workaround are described here (http://social.msdn.microsoft.com/forums/en-US/adodotnetdataproviders/thread/d4834ce2-482f-40ec-ad90-c3f9c9c4d4b1/). |
6.Application Programming Interface
6.1.SoodaTransaction
Each database-backed object in Sooda is associated with a transaction represented by an instance of SoodaTransaction class. Transactions provide database connection management, identity management (ensuring reference-level object identity as mentioned in section "1.2.2. Primary Keys and Object Identity" ).
In order to support identity management, transactions implement an L1 cache of all alive objects. SoodaTransaction
is able to return a cached object given the (className,primaryKeyValue)
pair.
No object can live outside the transaction and object state is not defined after the transaction has been closed. Keeping a transaction open for a longer period of time is not recommended because it consumes unmanaged resources, such as database connections.
Transactions need to be closed when they are no longer needed and if you fail to ensure that, otherwise you may get all sorts of unmanaged resource leaks. Typical usage pattern for a Sooda transaction that ensures the transaction is properly closed is presented below. It makes use of the IDisposable pattern and the C# using() statement:
using (SoodaTransaction t = new SoodaTransaction()) { // perform operations on Sooda objects here t.Commit(); }
In order to support long-running transactions, Sooda supports a differential serialization mechanism (6.6) , which lets you persist uncommitted changes made within transaction to an XML document, and deserialize them later, perhaps in a different process or a different machine.
6.1.1.Creating and managing transactions
Instances of SoodaTransaction
must be created before any operation
on O/R-mapped objects. You need to properly close the transaction after you are
done with the objects. The best way to ensure that your transaction is closed
regardless of any exceptions that may occur is to use the IDisposable
pattern and C# using()
statement:
using (SoodaTransaction t = new SoodaTransaction()) { // perform operations on Sooda objects here t.Commit(); }
This code is equivalent to:
SoodaTransaction t = new SoodaTransaction(); try { // operations here t.Commit(); } finally { t.Dispose(); }
Transactions need to be explicitly committed to properly save all changes made to objects. If you fail to do so, the transaction is rolled back by default.
Each transaction must be associated with a mapping schema which is embedded as a resource into the compiled DAL assembly. You need to configure the transaction to use the appropriate assembly. There are several ways to do it depending on how you are planning to use your DAL:
SoodaTransaction
constructor:
// _DatabaseSchema class is generated by StubGen. using (SoodaTransaction t = new SoodaTransaction(typeof(_DatabaseSchema).Assembly)) { }
ObjectsAssembly
property of an open transaction:
using (SoodaTransaction t = new SoodaTransaction()) { t.ObjectsAssembly = typeof(_DatabaseSchema).Assembly; }
SoodaTransaction.DefaultsObjectAssembly
which establishes default value for ObjectsAssembly
on newly created transactions
// you need to do it only once SoodaTransaction.DefaultObjectsAssembly = typeof(_DatabaseSchema).Assembly; using (SoodaTransaction t = new SoodaTransaction()) { }
<configuration> <appSettings> <add key="sooda.defaultObjectsAssembly" value="MyObjectsAssembly"/> </appSettings> </configuration>
If your objects assembly is in the GAC, you need to use a fully qualified assembly name:
<configuration> <appSettings> <add key="sooda.defaultObjectsAssembly" value="MyObjectsAssembly, Version=1.1.12.1512, Culture=neutral, PublicKeyToken=0123456789abcdef"/> </appSettings> </configuration>
SoodaStubAssemblyAttribute
to all assemblies that will create SoodaTransaction objects
using MyBusinessObjects; [assembly: SoodaStubAssembly(typeof(_DatabaseSchema))] class C1 { public static void Main() { using (SoodaTransaction t = new SoodaTransaction()) { } } }
6.1.2.Implicit transactions
As mentioned earlier, every object which inherits from SoodaObject
must be associated with an instance
of SoodaTransaction
. There are three ways to materialize objects which will be discussed in further sections:
new
Load()
and GetRef()
, FindByXXX()
, LoadSingleObject()
GetList()
, FindListByXXX()
All these methods need a transaction object. It would be inconvenient
to require the transaction reference to be passed explicitly, therefore Sooda supports the concept of
implicit active transaction (returned by the SoodaTransaction.ActiveTransaction
static property).
Implicit transaction is used when no explicit transaction is specified.
SoodaStubGen generates all APIs in two versions: one that takes SoodaTransaction
object as
parameter and the other that assumes implicit transaction.
Using implicit transactions:
using (SoodaTransaction transaction = new SoodaTransaction()) { Person person; PersonList personList; // object construction - implicit transaction person = new Person(); // load single objects person = Person.Load(10); person = Person.GetRef(10); person = Person.LoadSingleObject(PersonField.Name == "Mary Manager"); person = Person.FindByName("Mary Manager"); // load collection personList = Person.GetList(PersonField.Name == "Mary Manager"); personList = Person.FindListByName("Mary%"); }
Explicit transactions: (note that each method call receives transaction as its first argument)
using (SoodaTransaction transaction = new SoodaTransaction(SoodaTransactionOptions.NoImplicit)) { Person person; PersonList personList; // object construction - explicit transaction passed to constructor person = new Person(transaction); // load single objects - transaction passed as first argument person = Person.Load(transaction, 10); person = Person.GetRef(transaction, 10); person = Person.LoadSingleObject(transaction, PersonField.Name == "Mary Manager"); person = Person.FindByName(transaction, "Mary Manager"); // load collection - transaction passed as first argument personList = Person.GetList(transaction, PersonField.Name == "Mary Manager"); personList = Person.FindListByName(transaction, "Mary%"); }
When you create a new SoodaTransaction
object it is automatically
stored in SoodaTransaction.ActiveTransaction
unless you
pass SoodaTransactionOptions.NoImplicit
argument to the constructor.
The property is restored to its previous value when the transaction is closed/disposed,
which means you can nest transactions in an intuitive manner:
using (SoodaTransaction t1 = new SoodaTransaction()) { // t1 is implicit transaction using (SoodaTransaction t2 = new SoodaTransaction()) { // t2 is implicit transaction using (SoodaTransaction t3 = new SoodaTransaction()) { // t3 is implicit transaction } // t2 is implicit transaction } // t1 is implicit transaction }
SoodaTransaction.ActiveTransaction
reference is usually stored in thread local storage,
but other storage strategies are possible, as described
in section "8.7. ActiveTransaction management"
.
All examples in this document use implicit transactions.
6.2.Creating objects
To create a new persistent object in Sooda you simply invoke its constructor, optionally passing the transaction reference. There are 3 constructors generated by Sooda:
SoodaTransaction
as an argument - associates newly created object with the specific transactionSoodaConstructor
as an argument - this
constructor should never be called by the user code and is used internally by SoodaTypical code to create an object, set its properties and save to the database is:
using (SoodaTransaction t = new SoodaTransaction()) { // create new object Person p = new Person(); // set properties p.Name = "Nancy Newcomer"; p.Sex = "F"; p.Address = "1 Microsoft Way"; p.ZipCode = "98052"; p.City = "Redmond"; p.State = "WA"; p.BirthDate = new DateTime(1980, 1, 1); // commit t.Commit(); }
Sooda allows you to create more than one object in the transaction and will properly handle situations where
two newly created objects are dependent on one another. SQL INSERT
operations
will be properly ordered when Commit()
is called, to ensure that all foreign
key constraints are preserved.
The following example demonstrates this:
using (SoodaTransaction t = new SoodaTransaction()) { // We have a new customer who whishes to make his first order of two items // We want to make this transactional Customer c = new Customer(); Order o = new Order(); OrderItem i1 = new OrderItem(); OrderItem i2 = new OrderItem(); c.Name = "Caroline Customer"; c.Address = "..."; o.ShipTo = c.Address; o.Customer = c; i1.ItemType = Item.Foo; i1.Quantity = 10; i2.ItemType = Item.Bar; i2.Quantity = 10; o.Items.Add(i1); o.Items.Add(i2); t.Commit(); // Sooda properly orders the SQL INSERT statements: // // insert into Customer ... // insert into Order ... // insert into OrderItem ... // insert into OrderItem ... // // SQL statements issued in a different order would violate // referential identity constraints }
6.3.Loading objects
There are various ways to load objects from the database depending on the desired result:
6.3.1.Loading objects by primary key
Sooda provides two ways to get a reference to an object given its primary key:
ClassName.GetRef()
- gets a reference to an object without loading its dataClassName.Load()
- gets a reference to an object and loads its dataLazy loading is a way to get a reference to the object without actually loading data from the database. Property values will be loaded on first read access. Getting object reference is very fast and should be used when we do not need actual object data, just its reference, as described in section "1.2.5. Lazy Loading" .
The following example demonstrates basic concepts of lazy loading:
using (SoodaTransaction t = new SoodaTransaction()) { Employee emp; ////////////////////////////////////////// // get a reference to an object - no database access here emp = Employee.GetRef(3); // first READ - this causes the object to be loaded // (roughly equivalent to SELECT * FROM Employee WHERE id = 3) Console.WriteLine(emp.Name); // display hire date - no need to access the database // as the data is already loaded Console.WriteLine(emp.HireDate); ////////////////////////////////////////// // get a reference and load object data emp = Employee.Load(5); // no database access here - data is already in memory Console.WriteLine(emp.Name); // no database access here - data is already in memory Console.WriteLine(emp.HireDate); ////////////////////////////////////////// // get a reference to another object emp = Employee.GetRef(7); // change the state of the object // we do not need to load the object at all! emp.Active = false; // commit changes. this sends the following SQL to the database: // UPDATE Employee SET Active=false WHERE ID=7 t.Commit(); }
Reference fields use GetRef()
internally so that when you access a property that returns another object
this object usually starts in the Data Not Loaded
state.
Note that it is not always possible to provide a reference to an object without loading its data, so GetRef()
may sometimes need to load the data. When you invoke GetRef()
or Load()
on an
object of class that has subclasses, Sooda needs to load a record from the database to check the concrete type
to instantiate. Fortunately Sooda has a Factory Cache mechanism to avoid loading
records more than once as described
in section "8.6. Factory Cache"
.
6.3.2.Loading objects that match specific criteria
Sooda uses a query language called SOQL (7)
to select
objects that meet the specific criteria. To get a list of matching objects you
use the GetList()
method.
To load a single object by SOQL query use LoadSingleObject()
.
GetList()
method is overloaded to support various usage scenarios.
The most complex variant of the method takes five parameters, but SoodaStubGen (8.1.1)
also generates simplified variants of the GetList() method which only accept 1, 2, 3 or 4 parameters. There are
32 overloads generated for each class.
GetList(transaction, whereClause, topCount, orderBy, options);
The parameters are:
transaction
- (optional) transaction reference, in case it is omitted SoodaTransaction.ActiveTransaction
is usedwhereClause
- filter clause, either SoodaWhereClause
for textual queries or
SoqlBooleanExpression
for typed queries.topCount
- (optional) maximum number of objects to retrieve - this is equivalent to SELECT TOP n
SQL queryorderBy
- (optional) specifies ordering of result objectsoptions
- (optional) various flagsClassName.GetList()
returns an instance of ClassNameList
which is a type-safe wrapper over
ISoodaObjectList
and implements IList
and IList<ClassName>
.
More information about collections is given
in section "6.4. Collections"
.
For example, you can use the following code to run text-based SOQL query (7.1) to fetch at most 5 Persons living in Redmond, WA:
using (SoodaTransaction t = new SoodaTransaction()) { foreach (Person p in Person.GetList( new SoodaWhereClause("City={0} and State={1}", "Redmond", "WA"), 5)) { Console.WriteLine("name: {0}", p.Name); } }
To run a typed query:
using (SoodaTransaction t = new SoodaTransaction()) { foreach (Person p in Person.GetList( PersonField.City == "Redmond" && PersonField.State == "WA")) { Console.WriteLine("name: {0}", p.Name); } }
To order the results by last name:
using (SoodaTransaction t = new SoodaTransaction()) { foreach (Person p in Person.GetList( PersonField.City == "Redmond" && PersonField.State == "WA", SoodaOrderBy.Ascending("LastName"))) { Console.WriteLine("name: {0}", p.Name); } }
Options you can pass to GetList()
are flags defined in
the SoodaSnapshotOptions
enumeration.:
Default
- Default optionsNoTransaction
- Do not apply changes made in transaction. Use this option to perform a
database snapshot or when you are sure that none of transaction objects will affect the result.
This helps speed things up a bit.NoWriteObjects
- Do not precommit objects. Precommit is described
in section "6.10. Precommitting objects in transactions"
.VerifyAfterLoad
- Verify after load. In case of a precommit it is possible that the result
list will include extra objects. Enabling this option causes the resulting objects to be re-evaluated
in memoryKeysOnly
- Load only the primary key values. Objects will be materialized but fields other than primary keys will require a database query.Cache
- Fetch collection items from cache if possible.NoCache
- Do not load the collection from cache even if it would be possible to do soPrefetchRelated
- Use prefetch definition from the schema to load related objects automatically
The most commonly used option here is NoWriteObjects
which can be used to disable precommit.
This example demonstrates passing this option to GetList()
. Passing SoodaSnapshotOptions.NoWriteObjects
causes "Nancy Newcomer
" not to be included in the list of returned objects. If we omitted this flag,
Sooda would precommit (6.10)
newPerson
object by making the SQL INSERT
and Nancy Newcomer would be
returned by the GetList()
.
using (SoodaTransaction t = new SoodaTransaction()) { // add a person, whose address matches the following SOQL query Person newPerson = new Person(); newPerson.Name = "Nancy Newcomer"; newPerson.City = "Redmond"; newPerson.State = "WA"; // GetList() without precommit - Nancy Newcomer is // not returned foreach (Person p in Person.GetList( PersonField.City == "Redmond" && PersonField.State == "WA", SoodaSnapshotOptions.NoWriteObjects)) { Console.WriteLine("name: {0}", p.Name); } // GetList() with precommit - Nancy Newcomer included // in the list of matching objects foreach (Person p in Person.GetList( PersonField.City == "Redmond" && PersonField.State == "WA")) { Console.WriteLine("name: {0}", p.Name); } }
6.4.Collections
There are 3 types of collections in Sooda:
One-to-many collections are returned by properties declared with <collectionOneToMany /> (3.6) . Many-to-many collections are returned by properties declared with <collectionManyToMany /> (3.7) . All other collection APIs return snapshot collections.
All collection types implement unified interface named ISoodaObjectList
but not all methods of this interface can be called in all cases. The table below summarizes that.
The ISoodaObjectList
interface has the following methods:
(OTM means One-To-Many collections, MTM is Many-To-Many collections, SNAP - snapshots):
Name | OTM | MTM | SNAP | Description |
---|---|---|---|---|
Indexer:get | Yes | Yes | Yes | Gets the item at the specified position.
o = collection[0]; |
Indexer:set | No | No | Yes | Sets the item at the specified position.
collection[0] = o; |
Add(o) | Yes | Yes | Yes | Adds object o to the collection.
collection.Add(o); |
Remove(o) | Yes | Yes | Yes | Removes object o from the collection.
collection.Remove(o); |
Contains(o) | Yes | Yes | Yes | Determines whether specific object o is contained in the collection.
if (collection.Contains(o)) { // o is in the collection } else { // o is not in the collection } |
IndexOf(o) | Yes | Yes | Yes | Gets the position of an object o within the collection.
int pos = collection.IndexOf(o); if (pos >= 0) { // o is in the collection at position 'pos' } else { // o is not in the collection } |
Insert(p,o) | No | No | Yes | Inserts object o at the specified position p .
collection.Insert(3,o); |
RemoveAt(p) | No | No | Yes | Removes object at the specified position p .
collection.RemoveAt(3); |
Clear() | No | No | Yes | Removes all items from the collection.
collection.Clear(); |
CopyTo(a,o) | Yes | Yes | Yes | Copies the collection to the provided Array a starting at offset o .
// create array to hold collection items Person[] array = new Person[collection.Count]; // copy them collection.CopyTo(array,0); |
Count | Yes | Yes | Yes | Returns the number of objects in the collection.
int count = collection.Count; |
IsReadOnly | Yes | Yes | Yes | Determines whether the collection is read only. |
IsFixedSize | Yes | Yes | Yes | Determines whether the collection has fixed size. |
GetEnumerator | Yes | Yes | Yes | Gets the enumerator that returns all collection items.
This can be used with foreach() to iterate over the collection.
// foreach() uses GetEnumerator() internally foreach (Person p in collection) { } |
GetItem(p) | Yes | Yes | Yes | Gets the object at the specified position p .
collection.GetItem(10); |
GetSnapshot() | Yes | Yes | Yes | Creates a new snapshot collection containing all objects of the collection.
newCollection = collection.GetSnapshot(); |
SelectFirst(n) | Yes | Yes | Yes | Creates a new collection containing first n objects of the original collection.
// select first 5 objects
newCollection = collection.SelectFirst(5); |
SelectLast(n) | Yes | Yes | Yes | Creates a new collection containing last n objects of the original collection.
// select last 5 objects
newCollection = collection.SelectLast(5); |
SelectRange(s,e) | Yes | Yes | Yes | Creates a new collection containing objects s to e-1 of the original collection.
// select objects 3,4,5,6 from the collection // (7th object is not included in the result) newCollection = collection.SelectLast(3,7); |
Filter(f) | Yes | Yes | Yes | Creates a new collection containing objects that match the specified filter f which can be:
Examples (all filter out the person named "Mary Manager"): bool FilterMethod(SoodaObject obj) { Person p = (Person)obj; return p.Name != "Mary Manager"; } // filter by delegate collection2 = collection.Filter( new SoodaObjectFilter(this.FilterMethod)); // filter by where clause collection2 = collection.Filter( new SoodaWhereClause("Name != {0}", "Mary Manager")); // filter by typed boolean expression collection2 = collection.Filter( PersonField.Name != "Mary Manager"); |
Sort(ex) | Yes | Yes | Yes | Creates a new collection with objects sorted according to the specified order expression ex . The order can be:
Examples: // sort by Name in ascending order collection2 = collection.Sort("Name"); // sort by Name in descending order collection2 = collection.Sort("Name desc"); // sort by Age in ascending order then by Name in descending order: collection2 = collection.Sort("Age asc,Name desc"); // sort by the group manager's Name in ascending order: collection2 = collection.Sort("Group.Manager.Name asc");
Examples: // sort by Name in ascending order collection2 = collection.Sort(PersonField.Name); // sort by Name in descending order collection2 = collection.Sort(PersonField.Name, SortOrder.Descending); // sort by the group manager's Name in ascending order: collection2 = collection.Sort(PersonField.Group.Manager.Name, SortOrder.Ascending); |
SoodaStubGen utility (8.1.1)
generates type-safe
wrappers for each class, called ClassNameList
where ClassName
is the name of the
<class /> defined
in section "3. O/R Mapping Schema"
. Collections returned from ClassName.GetList()
,
one-to-many and many-to-many collections are wrapped with the appropriate type-safe wrappers.
The wrappers inherits from SoodaObjectCollectionWrapper
or SoodaObjectCollectionWrapperGeneric<T>
and implements the following set of interfaces:
IEnumerable
IList
ICollection
ISoodaObjectList
IEnumerable<T>
- .NET 2.0 and aboveIList<T>
- .NET 2.0 and aboveICollection<T>
- .NET 2.0 and aboveThe following example shows the typical usage of collections. As you can see the amount of boilerplate code is reduced to minimum:
using (SoodaTransaction t = new SoodaTransaction()) { // load Customer from the database Customer c = Customer.Load(10); // loop through the customer's orders and display their summaries // c.Orders is a one-to-many collection foreach (Order o in c.Orders) { Console.WriteLine("o.OrderDate: {0}", o.OrderDate); Console.WriteLine("o.ShipTo: {0}", o.ShipTo); Console.WriteLine("Item count: {0}", o.Items.Count); // now display the details of each ordered item foreach (OrderItem it in o.Items) { Console.WriteLine("item: {0} quantity: {1}", it.ItemType, it.Quantity); } } // create new order with 2 items Order newOrder = new Order(); newOrder.ShipTo = c.Address; OrderItem i1 = new OrderItem(); OrderItem i2 = new OrderItem(); c.Name = "Caroline Customer"; c.Address = "..."; c.Orders.Add(newOrder); i1.ItemType = Item.Foo; i1.Quantity = 10; i2.ItemType = Item.Bar; i2.Quantity = 10; o.Items.Add(i1); o.Items.Add(i2); // display orders sorted by their date foreach (Order o in c.Orders.Sort(OrderField.Date)) { Console.WriteLine("Order #{0} Date: {1}", o.OrderID, o.Date); } }
6.5.Object Identity Management
Sooda guarantees reference identity for objects retrieved from a database. It means that if we make 2 or more queries which retrieve a particular row from the database in the same transaction (6.1) they are guaranteed to be mapped to the same object (not just two objects that are equal). It means that you can modify the object through either any of the references and you will be modifying the same object, so the state is going to be consistent. Mechanism that ensures object identity is called Level 1 (transaction-level) caching.
The following example demonstrates multiple ways to acquire references to objects and the identity assertions that Sooda guarantees:
// load person Person p1 = Person.GetRef(10); // get a reference to a person Person p2 = Person.GetRef(10); // load person by text-based query Person p3 = Person.LoadSingleObject(new SoodaWhereClause("Id = 10")); // load person by typed query Person p4 = Person.LoadSingleObject(PersonField.Id == 10); // some O/R mappers only guarantee this: Assert.AreEqual(p1, p2); Assert.AreEqual(p1, p3); Assert.AreEqual(p1, p4); // but Sooda also provides reference identity guarantee Assert.AreSame(p1, p2); Assert.AreSame(p1, p3); Assert.AreSame(p1, p4); // // NOTATION (using NUnit-syntax): // // Assert.AreSame(a,b) asserts that Object.ReferenceEquals(a,b) // Assert.AreEqual(a,b) asserts that a.Equals(b) and b.Equals(a) //
There are methods and properties of the SoodaTransaction
class that let you to control
the object cache mechanism:
UseWeakReferences
property - enables weak references to be used for L1 cache management. Setting this property to true
causes Sooda to keep track of unmodified objects in transactions using weak references. This makes them
subject to be garbage-collected when the memory is low.
Modified (dirty) objects are always registered using strong references.
Because unmodified objects can always be retrieved from the database, this approach is useful
when you are dealing with a large number of read-only objects that are cheap to reload from the database.
DirtyObjects
property - returns a collection of dirty objects (objects modified within
the transaction)DeletedObjects
property - returns a collection of objects marked for deleteFindObjectWithKey(className,keyValue,expectedType)
method - returns a cached object whose class name
is className
and primary key is keyValue
. If the object is not found in L1 cache or if the object
is not of expectedType
, this function returns null
.GetObject(className,keyValueString)
method - gets a reference to the object of the specified class
and primary key. The primary key is given as a string, so this method is best suited for scripting/web environment
where the primary key can be passed in a URL. This method does not pre-load object data
into memory, that is it is equivalent to ClassName.GetRef(keyValue)
method.LoadObject(className,keyValueString)
method - same as GetObject(className,keyValueString)
but preloads object data into memory. It is an equivalent of the ClassName.Load(keyValue)
method.GetNewObject(className)
- creates a new object of specified class. It is same as
creating a new instance of the object by calling the constructor and passing current transaction as an argument.6.6.Serialization and Deserialization
Transactions in Sooda are meant to be short-lived, because each SoodaTransaction manages one or more open database connections which should be short-lived. It is not recommended to keep the database connections open for a longer period of time because it might negatively impact system scalability.
There are cases when long-lived transactions are desired, such as in a web application that gathers user input from many web pages, makes incremental changes to the database and commits the transaction once at the very end of the process. Sooda provides differential serialization mechanism that allows you to capture the state of an open transaction and store it in an XML document. This XML document can be persisted to some medium (such as a file, buffer in memory, ASP.NET Session state or ViewState) and restored in a new transaction later. The transaction can be restored in another thread, process, or even on another machine, as long as the target environment has access to the original database.
The API for serialization and deserialization is very simple. It consists of methods named Serialize()
and
Deserialize()
respectively. The serialization code is presented below:
using (SoodaTransaction tran = new SoodaTransaction()) { // create, modify or delete objects here Customer c = Customer.Load(1); c.Address = "New Address"; // do not call Commit(), but call Serialize() instead string serializedXml = tran.Serialize(); // write the state of transaction to a file File.WriteAllText("c:\\temp\\transaction.xml", serializedXml); }
To deserialize the transaction in a new environment, just use the Deserialize()
method:
using (SoodaTransaction tran = new SoodaTransaction()) { // load the transaction from a file string serializedXml = File.ReadAllText("c:\\temp\\transaction.xml"); tran.Deserialize(serializedXml); // load the object Customer c = Customer.Load(1); // verify that deserialization worked Assert.AreEqual("New Address, c.Name); // All object modifications uncommitted in // previous transaction are reflected here // you can either commit them here or serialize again // and deserialize in yet another place tran.Commit(); }
The XML returned by the Serialize()
method should be treated as opaque (you should not assume
anything about its structure), but is generally human-readable. Sample transaction dump is presented below:
<transaction> <object mode="insert" class="Announcement"> <key ordinal="0" type="int32" value="1"/> </object> <object mode="insert" class="ActivityObjectInsert"> <key ordinal="0" type="int32" value="222"/> </object> <object mode="update" class="Announcement"> <key ordinal="0" type="int32" value="1"/> <field name="TimeStamp" type="datetime" value="07/19/2006 09:26:10"/> <field name="ActiveFlag" type="boolint" value="true"/> <field name="Description" type="string" value="we have a network problem blah blah blah"/> <field name="Type" type="int32" value="1"/> <field name="ValidFrom" type="datetime" value="07/19/2006 09:26:10"/> <field name="ValidUntil" type="datetime" value="07/19/2006 09:26:10"/> </object> <object mode="update" class="ActivityObjectInsert"> <key ordinal="0" type="int32" value="222"/> <field name="ClassName" type="string" value="Announcement"/> <field name="KeyValue" type="int32" value="1"/> <field name="TimeStamp" type="datetime" value="07/19/2006 09:26:10"/> <field name="Person" type="int32" value="1"/> <field name="Type" type="int32" value="4"/> <field name="SummaryPlainText" type="string" value="Object has been created"/> <field name="SummaryCRTFFile" type="string" value=""/> <field name="Action" type="int32" value="2"/> </object> </transaction>
6.7.SoodaObject
Stub classes generated by SoodaStubGen utility (8.1.1)
are derived from
SoodaObject
which provides some common methods and infrastructure for storing
object data, caching, lazy loading, triggers (6.8)
and more.
This section describes public API exposed by
SoodaObject
class. More details are available in the source code comments.
SoodaObject
exposes no public properties, only methods. All properties
visible from outside of the object are generated by SoodaStubGen (8.1.1)
.
Public methods of SoodaObject
are:
bool AreFieldUpdateTriggersEnabled()
- returns a value indicating whether field
update triggers (BeforeFieldUpdate
and AfterFieldUpdate
) are enabled.
More information about field update triggers can be found
in section "6.8. Triggers"
.bool EnableFieldUpdateTriggers()
- enables field update triggers and returns a boolean value indicating their state before enablingbool DisableFieldUpdateTriggers()
- disables field update triggers and returns a boolean value indicating their state before disablingbool EnableFieldUpdateTriggers(bool flag)
- enables or disables field update triggers based on the boolean flag. This function returns the previous state of triggers.
An ordinary pattern for disabling field update triggers for some region of code is presented below.
bool oldValue = obj.DisableFieldUpdateTriggers(); try { // modify object - no BeforeFieldUpdate or AfterFieldUpdate triggers are called obj.Name = "New Name"; obj.Age = 123; } finally { // restore field update triggers to the saved state obj.EnableFieldUpdateTriggers(oldValue); }
void ForcePostCommit()
- forces the postcommit trigger (AfterObjectUpdate()
) to be called even if there are no changes to object's internal statebool IsInsertMode()
- returns a value indicating whether the object is in insert mode (that is, it has been created in the transaction). Note that it does not indicate
that object has not been inserted into the database yet, because of possible precommit (6.10)
.SoodaTransaction GetTransaction()
- returns a reference to the transaction that manages this object.void MarkForDelete()
- marks the object for deletion and executes any cascade deletes on all objects that reference this object.
Sooda issues SQL DELETE statement as soon as objects are marked for deletion,
but since the transaction is not committed until the very end, this operation can be reversed.
Deleting objects immediately helps Sooda detect any foreign key constraint violations.bool IsMarkedForDelete()
- returns a value indicating whether the object has been marked for deletion.bool IsFieldDirty(fieldNumber)
- returns a value indicating whether the specified field (indicated by its ordinal) is dirty.bool SetFieldDirty(fieldNumber,dirty)
- sets or clears the specified field's dirty flag, which causes it to be saved to the database on commit.
Sooda normally manages the dirty flag automatically, but you might want to override it in certain situation. Use with caution.bool IsObjectDirty()
- returns a value indicating whether the object has been modified in the transaction. Note that resetting field statuses back to non-dirty does not automatically make the object non-dirty.
Only Commit()
or Rollback()
clears this flag.ClassInfo GetClassInfo()
- gets the ClassInfo
object that include the schema-level metadata about the class. More information about ClassInfo
is available
in section "8.5. Schema API"
.string GetObjectKeyString()
- returns a unique string that includes object's primary key. The string is of the form "ClassName[PrimaryKeyValue]"
, for example "Person[10]"
or "Employee[1234]"
.object GetPrimaryKeyValue()
- returns a primary key of the object. For scalar (single-columns) primary keys actual values (integer or string) are returned, for multi-column primary keys this function
returns an instance of the SoodaTuple
class.object Evaluate(SoqlExpression expression[, bool throwOnError])
- evaluates the SOQL expression (7)
in memory and returns its value. throwOnError
causes
the function to either throw exception or return null
on error (such as null reference exception).string GetLabel()
- evaluates the label property and returns its string value. The name of the label
property is defined with the <class label="..." /> declaration (3.2)
in the schema file.6.8.Triggers
Sooda supports application triggers (as opposed to database triggers) which are fired each time one of the following events occurs:
There are two variants of each trigger: "before" trigger and "after" trigger. Triggers are methods declared
in the SoodaObject
class
that can be overridden in skeleton classes generated by the SoodaStubGen utility (8.1.1)
.
Overriding a "before" trigger allows you to prevent some operation (such as field modification) from happening
while "after" triggers are called after the operation has been already performed.
They object-level triggers have canonical names:
void BeforeObjectInsert()
- fires immediately before an object is inserted into the databasevoid AfterObjectInsert()
- fires after all objects have been inserted into the database and committed, once for each object.void AfterObjectUpdate()
- fires immediately before an object is updatedvoid AfterObjectDelete()
- fires after all objects have been updated and committed, once for each object.void BeforeObjectDelete()
- fires immediately before the object is deletedvoid AfterObjectDelete()
- fires after the object has been deleted and committedThe field-level trigger methods are:
void BeforeFieldUpdate(string fieldName, object oldValue, object newValue)
- fires immediately before any of the fields is updatedvoid AfterFieldUpdate(string fieldName, object oldValue, object newValue)
- fires immediately after any the fields has been modifiedvoid BeforeFieldUpdate_FieldName(object oldValue, object newValue)
- fires immediately before field FieldName is modifiedvoid AfterFieldUpdate_FieldName(object oldValue, object newValue)
- fires immediately after field FieldName has been modifiedTo prevent the field update from happening, you need to override the "before" trigger method and throw an exception from it. The following example demonstrates overriding the triggers:
public class Person : Person_Stub { // constructors omitted protected override void BeforeObjectInsert() { base.BeforeObjectInsert(); Console.WriteLine("Object is about to be inserted."); } protected override void AfterObjectInsert() { base.AfterObjectInsert(); Console.WriteLine("Object has been inserted."); } protected override void BeforeObjectUpdate() { base.BeforeObjectUpdate(); Console.WriteLine("Object is about to be updated."); } protected override void AfterObjectUpdate() { base.AfterObjectUpdate(); Console.WriteLine("Object has been updated."); } protected override void BeforeObjectDelete() { base.BeforeObjectDelete(); Console.WriteLine("Object is about to be deleted."); } protected override void AfterObjectDelete() { base.AfterObjectDelete(); Console.WriteLine("Object has been deleted."); } protected override void BeforeFieldUpdate_Age(object oldValue, object newValue) { base.BeforeFieldUpdate_Age(oldValue, newValue); Console.WriteLine("Modifying field Age from {0} to {1}", oldValue, newValue); if (newValue.Equals(42)) { // this exception prevents the field update from happening when the user // attempts to change Age to 42. throw new Exception("Cannot set Age to 42!"); } } protected override void AfterFieldUpdate_Age(object oldValue, object newValue) { base.AfterFieldUpdate_Age(oldValue, newValue); Console.WriteLine("Field Age has been modified from {0} to {1}", oldValue, newValue); } }
Object-level triggers are always available and cannot be disabled. You can disable field-level triggers (BeforeFieldUpdate_xxx and AfterFieldUpdate_xxx) either at stub generation stage or at runtime. The former option is useful to reduce the amount of generated code, because stub classes can get pretty large when you declare many fields in a class.
SoodaStubGen (8.1.1) provides two ways to disable the generation of field-level triggers. You can disable them at the class level, by specifying <class triggers="false" /> and you can enable them individually at the field level by specifying <field forceTrigger="true" />. For example:
<schema> <class name="Employee"> <table name="Employee"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="Salary" type="Decimal"/> <field name="HireDate" type="DateTime"/> <field name="Active" type="BooleanAsInteger"/> </table> </class> <class name="Vehicle" triggers="false"> <table name="Vehicle"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Price" type="Decimal"/> <field name="Description" type="String" size="40" forceTrigger="true"/> </table> </class> </schema>
This declaration will cause the following trigger methods to be generated:
Employee_Stub.BeforeFieldUpdate_Name()
Employee_Stub.AfterFieldUpdate_Name()
Employee_Stub.BeforeFieldUpdate_Salary()
Employee_Stub.AfterFieldUpdate_Salary()
Employee_Stub.BeforeFieldUpdate_HireDate()
Employee_Stub.AfterFieldUpdate_HireDate()
Employee_Stub.BeforeFieldUpdate_Active()
Employee_Stub.AfterFieldUpdate_Active()
Vehicle_Stub.BeforeFieldUpdate_Description()
Vehicle_Stub.AfterFieldUpdate_Description()
The following trigger methods will not be generated because of the triggers="false"
attribute of <class name="Vehicle" />:
Vehicle_Stub.BeforeFieldUpdate_Price()
Vehicle_Stub.AfterFieldUpdate_Price()
Note that Sooda does not generate triggers for primary key fields. That is because they are immutable - you cannot the value of a primary key once it has been set, as it would break the rules of reference identity. It would lead to a situation where two different objects would have the same primary key values which is forbidden.
6.9.Key Generation
Key generation is a process of generating unique identifiers to be used for primary keys of newly created objects.
Because primary keys must be known before objects can be committed, you cannot use SQL Server identity columns for key
generation. By default Sooda uses a table-based key generator that manages primary keys by storing their values in
a special database table, but you can write your own generator by creating a class that implements
the IPrimaryKeyGenerator
interface.
Table-based primary key generation uses a database table (called KeyGen
by default)
with 2 columns: key_name
and key_value
which store class name and
next primary key value to be generated respectively:
Because keys are stored in the database table, generating them requires two SQL statements to be executed. One is SELECT to fetch the current value of the primary key, the other one is to update the database table so that next generation will not use the same value again. Because of this cost, Sooda allocates keys in batches of ten (the number is configurable) so it only needs to make 2 round-trips to the server for every ten new keys generated.
-- fetch current value of the primary key SELECT @kv = key_value FROM KeyGen where key_name = 'ClassName'; -- update KeyGen table with the new possible primary key UPDATE KeyGen SET key_value = @kv + 10 WHERE key_name = 'ClassName' AND key_value = @kv
This algorithm is multi-process safe, so multiple processes can create new objects concurrently, but it does not guarantee that the primary keys of newly created objects will be monotonously increasing.
You can configure the name of the table and batch size on a per-datasource basis by using the configuration parameters:
datasourcename.keygentable.name
- name of the keygen table (by default (KeyGen
)datasourcename.keygentable.keycolumn
- name of the columns that holds key names (by default key_name
)datasourcename.keygentable.valuecolumn
- name of the columns that holds values (by default key_value
)datasourcename.keygentable.pool_size
- the number of keys to allocate at one (by default 10
)The following example demonstrates using application configuration file to override default name of the KeyGen table and names of its columns.
<configuration> <appSettings> <add key="default.keygentable.name" value="MyKeyGen"/> <add key="default.keygentable.keycolumn" value="keys"/> <add key="default.keygentable.valuecolumn" value="values"/> </appSettings> </configuration>
6.10.Precommitting objects in transactions
Sooda supports the GetList()
method to get the list of objects matching the specified criteria. It is not possible
to calculate the list in memory because it would be very inefficient, therefore the criteria written in SOQL
(textual or typed) are converted to SQL and executed by the database engine.
Objects in transaction can be modified, created and deleted and RDBMS needs to be notified of all
such modifications or it will return an outdated list of matching objects.
Consider this example:
using (SoodaTransaction t = new SoodaTransaction()) { // create new person object but don't commit it Person p = new Person(); p.Name = "Nancy Newcomer"; // now get a list of all persons PersonList pl = Person.GetList(true); }
The condition true
passed to the GetList()
method is always met, so you expect
it to return all objects of class Person
including the newly created 'Nancy Newcomer' object.
GetList()
ultimately results in an SQL query, whose result should
include the mentioned object. For this reason, Sooda needs to issue an INSERT INTO Person
statement
before executing the SELECT
statement on the database.
In general, Sooda ensures that any modifications made in transaction, which can affect the query results, are saved to the database before the query is executed. Objects that need to be precommitted are:
Person
in the above example) and its subclassesWHERE
clause and their subclasses
For example, the following code causes all objects of Person
,
Group
and OrganizationUnit
classes to be precommitted because they are mentioned
in the WHERE
clause
using (SoodaTransaction t = new SoodaTransaction()) { Person p; Person.GetList(PersonField.Name == "Mary Manager" && PersonField.Group.Name == "Managers" && PersonField.OrgUnit.Name == "Sales Department"); }
To disable precommit, you need to pass the SoodaSnapshotOptions.NoWriteObjects
option
to the GetList()
method. To manually cause a precommit at any time, you can
invoke the SoodaTransaction.SaveObjectChanges()
method.
Note that if your query contains a RAWQUERY()
clause, determining the list of
objects to be precommitted may not be possible, so all objects are precommitted to ensure consistent
results.
Precommitting objects can become difficult if we have non-nullable fields in the database and data for these fields has not been provided yet (this is only possible when the object is in insert mode). When inserting such objects, Sooda needs to provide SOME value for the column, otherwise the insert will fail. You can specify what values should be written to the database for each column type, referenced table or you can set them on a per-field basis as described in section "3.10. Precommit Values" . It is important to specify correct precommit values for all referenced tables if you use foreign key constraints in the database. If you do not specify the precommit values, Sooda will insert the default value of zero into foreign key columns which may violate the constraints.
6.11.Caching
Sooda supports two caching levels:
First level of cache is totally transparent to the user, while the second level of cache is configurable and may need to be tuned for the best performance. There are two aspects of the cache mechanism that are tunable:
Caching policy determines what objects should be stored in cache and how long they are kept there. There are many possible approaches here:
Objects are considered Small
, Medium
or Large
depending on the
cardinality
setting in the mapping schema (3)
.
In addition to this, you can also create your own caching policy by creating a class that implements
the ISoodaCachingPolicy
interface. Your caching policy can use caching hints (cached
,
cacheCollections
, cardinality
as defined in the schema) or your algorithm to decide
which objects or collections should be put into cache.
To configure the caching policy for your application, just set the sooda.cachingPolicy
key in the
configuration file (5.1)
to one of the predefined policy names or use the name
of your own class which implements the ISoodaCachingPolicy
. To configure expiration timeout and
sliding expiration use sooda.cachingPolicy.expirationTimeout
(timeout in seconds) and
sooda.cachingPolicy.slidingExpiration
(boolean true
or false
) respectively.
The following example configures caching of small and medium objects, with the expiration timeout of one
hour and sliding expiration set to true:
<configuration> <appSettings> <!-- cache small and medium objects --> <add key="sooda.cachingPolicy" value="smallmedium"/> <!-- retain objects for 3600 seconds - one hour --> <add key="sooda.cachingPolicy.expirationTimeout" value="3600"/> <!-- each access to an object resets the one-hour timeout --> <add key="sooda.cachingPolicy.slidingExpiration" value="true"/> </appSettings> </configuration>
Alternatively you can configure caching in code, by setting the static property called CachingPolicy
of the SoodaCache
class:
// cache small and medium objects SoodaCacheSmallAndMediumPolicy policy = new SoodaCacheSmallAndMediumPolicy(); // retain objects for one hour policy.ExpirationTimeout = TimeSpan.FromHours(1); // each access to an object resets the one-hour timeout policy.SlidingExpiration = true; // make the caching policy active for new transactions SoodaCache.DefaultCachingPolicy = policy;
Newly created transactions default to the caching policy stored in SoodaCache.DefaultCachingPolicy
, but
you can override it for each transaction separately. To disable caching for a particular transaction, use this code:
using (SoodaTransaction t = new SoodaTransaction()) { // don't use caching in this transaction t.CachingPolicy = new SoodaNoCachingPolicy(); }
In addition to various caching policies, Sooda supports pluggable storage engines. By default Sooda comes with two:
You can implement other cache storage mechanism by creating a class that
implements the ISoodaCache
interface. It should be possible to integrate with commercial
cache engines through this interface for great scalability.
To configure cache storage mechanism, just set the sooda.cache
parameter in in the configuration file to
one of the mentioned caching implementations. You can also specify a fully qualified name of the class that implements
ISoodaCache
interface:
<configuration> <appSettings> <add key="sooda.cache" value="MySuperCache.SoodaCachingProvider, MySuperCache, PublicKeyToken=..., Version=..."/> </appSettings> </configuration>
You can achieve the same thing in code, by setting the DefaultCache
property of the SoodaCache
class:
MySuperCache.SoodaCachingProvider cache = MySuperCache.SoodaCachingProvider(); // configure 'cache' here - set some hypothetical properties cache.Size = 1000000; cache.Location = "c:\\mycache"; // make the cache active for new transactions SoodaCache.DefaultCache = cache;
As with the caching policies, you can also override the cache mechanism on a per-transaction basis:
using (SoodaTransaction t = new SoodaTransaction()) { // override default cache storage for this transaction t.Cache = cache; }
6.12.Working with Data Sources
Sooda supports pluggable data sources. In theory it should be possible to implement support backends other than relational databases, but current version of Sooda only supports SQL Data sources.
SQL Data source provides a way to programmatically execute SOQL queries against relational databases. It is also used internally by Sooda to provide low-level mapping functionality such as loading single objects, collections and saving changes back to the database. It currently supports the following RDBMSes:
mssql
- Microsoft SQL Server 2000, 2005, MSDE and SQL Server Expresspostgresql
- PostgreSQL 8.x and higheroracle
- Oracle 8i and highermysql4
- MySQL 4.x and higher
SQL Data Source is implemented in Sooda.Sql.SqlDataSource
class.
To use it in the mapping schema (3)
, declare a <datasource /> with a type
of Sooda.Sql.SqlDataSource
:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <datasource name="db" type="Sooda.Sql.SqlDataSource"/> </schema>
Note that at this point, you do not specify the database connection type, connection string or SQL dialect - these things are configured at runtime, so that applications can run unmodified with a database engine of choice. Data source runtime configuration is explained in section "5.2. Data Source Configuration" .
There are two ways to acquire a SqlDataSource
object, depending on whether you
have an open transaction. If you do, getting the SQL data source is a matter of calling OpenDataSource()
method on a SoodaTransaction
instance, passing the name of the data source to open.
SoodaTransaction manages a list of open data sources and will return the same data source object when OpenDataSource()
is called with the same data source name twice.
using (SoodaTransaction t = new SoodaTransaction()) { // get a reference to open datasource SqlDataSource ds = (SqlDataSource)t.OpenDataSource("default"); // you do not need to (and should not) close the data source // explicitly it will be closed when the transaction terminates. }
If you are not within the context of a transaction or you want to create a
detached SqlDataSource
object, create a new object with new
passing the data source name as an argument to the constructor. To make sure that the SQL Data Source
is properly closed, it is best to use the C# using()
statement:
using (SqlDataSource ds = new SqlDataSource("default")) { }
Before you can use the SqlDataSource
object, you need to initialize its properties.
If you pass the name of the datasource in the constructor invocation, default values for
the properties are read automatically from the configuration file as
described
in section "5.2. Data Source Configuration"
. Once the properties have been initialized,
you should call the Open()
method to connect to the database.
The following methods and properties of the SqlDataSource
class can be invoked from user code:
Open()
method.
Opens the data source which connects to the database and opens a new transaction (unlessDisableTransactions
has been set totrue
).
IsOpen
property.
Returnstrue
if the connection is open,false
otherwise
Rollback()
method.
Performs rollback of the open transaction and starts a new one. This method does nothing ifDisableTransactions
is set totrue
.
Commit()
method.
Performs commit of the open transaction and starts a new one.
Close()
method.
Closes the connection to the database.
IDataReader ExecuteQuery(SoqlQueryExpression query, SchemaInfo schema, object[] parameters)
method
Executes SOQLquery
represented asSoqlQueryExpression
, which can be retrieved from a textual representation by callingSoqlParser.ParseExpression(soqlText)
. Theschema
parameter is an instance of the SchemaInfo (8.5.1) class andparameters
is an array of values to be used for{0}
,{1}
,{2}
... parameter placeholders. See below for an example use of this method.
IDataReader ExecuteRawQuery(string queryText, object[] parameters)
method
Executes raw SQL query on an open data source.parameters
is an array of values to be used for{0}
,{1}
,{2}
... parameter placeholders. Other than that, this method is equivalent toIDbCommand.ExecuteReader()
.
int ExecuteNonQuery(string queryText, object[] parameters);
method
Executes raw SQL command that does not return any records (such as SQLINSERT
orUPDATE
) on an open data source.parameters
is an array of values to be used for{0}
,{1}
,{2}
... parameter placeholders. Other than that, this method is equivalent toIDbCommand.ExecuteNonQuery()
.
There are other public methods available in the SqlDataSource
class, but they are reserved for
Sooda internal use. The following example demonstrates the most typical use of SqlDataSource which is executing
an SOQL query and iterating over the result set:
using Sooda; using Sooda.Sql; using Sooda.Schema; using Sooda.QL; class Program { static void Main(string[] args) { // connect to the data source 'default' defined // in the configuration file using (SqlDataSource sds = new SqlDataSource("default")) { sds.Open(); // SOQL textual query string soqlQuery = @" SELECT Name, PrimaryGroup.Name, PrimaryGroup.Members.Count FROM Contact WHERE PrimaryGroup.Manager.Name = {0} OR Name = {1}"; // parse the query to a SoqlQueryExpression object SoqlQueryExpression queryExpression = SoqlParser.ParseQuery(soqlQuery); // get the schema reference SchemaInfo schema = Sooda.UnitTests.BaseObjects._DatabaseSchema.GetSchema(); // prepare parameters object[] parameters = new object[] { "Mary Manager", // positional parameter {0} "Eva Employee" // positional parameter {1} }; // execute query using (IDataReader reader = sds.ExecuteQuery(queryExpression, schema, parameters)) { // iterate the result set while (reader.Read()) { Console.WriteLine("name: {0} group: {1} members: {2}", reader.GetString(0), reader.GetString(1), reader.GetInt32(2)); } } } } } // class Program
When you run this example on the SoodaUnitTests
database that comes with Sooda, it displays the following results:
name: Mary Manager group: Group1 members: 4 name: Ed Employee group: Group1 members: 4 name: Eva Employee group: Group2 members: 1 name: Caroline Customer group: Group1 members: 4 name: Chuck Customer group: Group1 members: 4
6.13.Dynamic Fields
In addition to fields defined in the mapping schema, Sooda supports creation and deletion of fields at runtime.
Fields created at runtime are called dynamic.
Dynamic fields are defined per-class and have the following properties:
name, type (optionally a reference), nullability and, for selected types, size and precision.
Dynamic fields can be accessed using a string indexer (obj["MyField"]
)
or the .NET 4 dynamic
type. The indexer can be used in LINQ queries (7.6)
.
Definition of dynamic fields is stored in the database table called SoodaDynamicField
while field values are stored in separate tables, one table per dynamic field.
A table for dynamic field values is created when the field is added, and dropped when the field is removed.
The name of the table is the parent class and the field name combined with an underscore, e.g. MyClass_MyDynamicField
.
Dynamic field table usually contains two columns: id
and value
,
where id
is the object primary key and value
is the value of the dynamic field.
There are more columns (id2
, id3
, ...) if the primary key is composite.
To conserve storage, null values are not stored in the database. Therefore the value
column is never nullable.
6.13.1.Enabling Dynamic Fields
To enable dynamic fields in your project, first create the database table SoodaDynamicField
and grant permissions for the SQL user your application uses:
create table SoodaDynamicField ( class varchar(32) not null, field varchar(32) not null, type varchar(32) not null, nullable int not null, fieldsize int null, precision int null, constraint PK_SoodaDynamicField primary key (class, field) ); grant create table to soodatest grant references to soodatest -- SQL Server only
The next step is enabling dynamic fields support in your mapping schema. It is enabled per data source, e.g.
<datasource name="default" type="Sooda.Sql.SqlDataSource" dynamicFields="true"/>
The last step is re-generating stubs from the modified mapping schema.
Now your application supports dynamic fields.
The first time it creates a SoodaTransaction
, it will query the SoodaDynamicField
table
and use the dynamic fields defined there. When you add dynamic fields with the Sooda API, this table will get updated
and the dynamic field tables will get created or dropped.
6.13.2.Managing Dynamic Fields
To create a dynamic field, create a Sooda.Schema.FieldInfo
object and set the following properties:
ParentClass
- the class that the field is going to be added to.Name
- identifier of the field within the class. It must not collide with the fields already defined in the mapping schema.DataType
, References
, IsNullable
- type and nullability, see below for alternative ways.Size
- required for string fields, optional for decimal/float/double.Precision
- optional for decimal/float/double.
Instead of setting DataType
and References
, you can set TypeName
.
It is either the string representation of DataType
or the name of the referenced class without the namespace.
Another alternative is setting the Type
property. For value types, it also sets nullability, e.g. typeof(int)
is non-nullable
while typeof(int?)
is nullable. You still need to set IsNullable
for string and reference types.
Setting Type
to typeof(string)
will select the String
data type and never an AnsiString
.
typeof(bool)
selects BooleanAsInteger
and not Boolean
.
Once your FieldInfo
contains all the required information, pass it to the Add
method
of DynamicFieldManager
, together with a SoodaTransaction
:
Sooda.Schema.DynamicFieldManager.Add(fieldInfo, transaction);
Adding a dynamic field changes the internal structure of the objects. Therefore, until you are finished with adding dynamic fields, you must not use any objects of the class you modify. Currently this requirement is not verified by Sooda and failing to fulfill it can lead to exceptions and data corruption. An exception from this rule is that you can read objects which won't be modified in this transaction. Same applies to removing a dynamic field.
Adding and removing a dynamic field completely clears the Sooda Level 2 cache (6.11) .
The field is now available for use (see below).
For large volumes of data, filtered by the dynamic field, consider creating a database index for the new field:
Sooda.Schema.DynamicFieldManager.CreateIndex(fieldInfo, transaction);
Dynamic fields are available in the schema API (8.5) just like the fields defined in the mapping schema:
classInfo.LocalFields
includes dynamic fields.classInfo.UnifiedFields
includes dynamic fields, including the ones defined in base classes.classInfo.FindFieldByName()
can return a dynamic field.fieldInfo.IsDynamic
returns true
for a dynamic field.
After a dynamic field is added, you can only modify its nullability:
fieldInfo.IsNullable = true;
Sooda.Schema.DynamicFieldManager.Update(fieldInfo, transaction);
To remove a dynamic field, call:
Sooda.Schema.DynamicFieldManager.Remove(fieldInfo, transaction);
Beware that this removes all the existing field data by dropping the table.
6.13.3.Using Dynamic Fields
Dynamic fields can be accessed with an indexer, passing a field name as a string:
object value = obj["MyField"];
For value types, the indexer always returns a boxed object which can be unboxed by casting
to a raw value type or a Nullable
type. That is, the indexer never returns an SqlType
.
If a dynamic field is added when there are already objects of the parent class,
the field values for existing objects will be null
, even if the field is non-nullable.
When you assign a dynamic field:
obj["MyField"] = value;
the type and nullability of the field are validated.
ArgumentNullException
will be thrown when null
is assigned to a non-nullable field.
InvalidCastException
will be thrown when value type is invalid.
On setting field value, BeforeFieldUpdate(fieldName, oldValue, newValue)
and AfterFieldUpdate(fieldName, oldValue, newValue)
triggers will be called if enabled.
In addition to dynamic fields, the indexer can be also used to read (but not write) fields defined in the mapping schema.
Alternatively to the indexer, you can access dynamic fields with the .NET 4 dynamic
type:
dynamic dynamicObj = obj;
object value = dynamicObj.MyField;
...
dynamicObj.MyField = newValue;
The indexer syntax can be used in LINQ (7.6) queries, e.g.:
MyObject.Linq().Where(o => o["MyField"] != null); MyObject.Linq().Where(o => (int) o["MyField"] == 42);
7.Query Languages
Sooda supports a query language named SOQL which is similar to SQL (Structured Query Language) enhanced with path expressions, collections, named constants and inheritance support. Only SELECT operation is supported which means no INSERT/UPDATE/DELETE support exists in SOQL.
7.1.SOQL Queries
SOQL queries have syntax similar to SQL SELECT statements. Each query has the following form:
SELECT [TOP number] [DISTINCT] select_expressions FROM from_expressions [WHERE boolean_expression] [GROUP BY groupby_expressions] [HAVING boolean_expression] [ORDER BY sort_expressions]
SOQL queries are based on classes and fields as opposed to tables and columns. You should use names defined in the mapping schema (3)
with <class name="..." /> and <field name="..." />. Let's assume you have the following class definition that
maps table ctct
to class Contact
and uses field names different from column names:
<class name="Contact"> <table name="ctct"> <field name="ID" dbcolumn="contact_id" type="Integer" primaryKey="true"/> <field name="Name" dbcolumn="contact_name" type="String" size="40"/> </table> </class>
You can write the following SOQL statement that gets the name of some particular contact:
select Name from Contact where ID=123
It is equivalent to the following SQL query:
select contact_name from ctct where contact_id = 123
SQL-like query from multiple classes with JOIN specified in the where clause is supported but rarely needed because Sooda supports path expressions (7.2.1) which greatly simplify query code. There are other useful extensions to SQL, as described in section "7.2. SOQL Language Elements" .
7.2.SOQL Language Elements
SOQL supports the following language constructs known from SQL:
=
, !=
, <
, <=
, >=
, >
, LIKE
)AND
, OR
)EXISTS()
, IN()
, IS NULL
and IS NOT NULL
predicates+
,-
,*
,/
and %
) with natural precedence and associativity-
) and boolean (NOT
) negationIn addition the following new constructs are supported:
t.PrimaryGroup.Manager.Name
)Count
and Contains
operationsTRUE
and FALSE
literalsSoodaClass
operator
SOQL is case-insensitive on keywords, class names and property names.
For readability, it is recommended to use the original casing when
referring to fields and classes. Case sensitivity of string comparisons and SOQL LIKE
operator are database-dependent.
7.2.1.Path Expressions
Path expressions let you traverse many-to-one and one-to-one relationships without JOIN operators. To access a property of a related object you simply prefix the name of the property with the path of dot-separated property names that leads to the object. Path length is unlimited.
Here is an example of a SOQL query that retrieves all contacts who belong to groups managed by 'Mary Manager':
SELECT * FROM Contact WHERE PrimaryGroup.Manager.Name = 'Mary Manager'
7.2.2.Collections
Sooda supports one-to-many and many-to-many collections in queries. SOQL queries can get the size of a collection as well as check if a particular object is contained in the collection. The following operations are supported:
collection.Count
- get the size of a collectioncollection.Contains(object)
- check whether a particular object is included in the collectioncollection.Contains(ClassName where condition)
- check whether any object from the collection matches the specified condition
To get the number of elements in the collection, just append the .Count
after the collection name:
-- select all contacts that have more than one role SELECT * FROM Contact WHERE Roles.Count > 1
Using Count
is supported in select expressions, not just in the WHERE clause:
-- return the name of each person followed by the number -- of members in his group select Name, PrimaryGroup.Members.Count from Contact
To check if a given collection contains a particular object, use the Contains(object)
.
The object
can be a value of a object field, a named constant or a literal value (an integer or a string):
-- get the names of all groups that Mary Manager belongs to -- Contact.Mary is a named constant select Name fromGroup where Members.Contains(Contact.Mary);
The same condition can be specified by using the value of 1
(the primary key) instead of Contact.Mary
.
-- get the names of all groups including Mary Manager select Name fromGroup where Members.Contains(1);
More complicated queries are possible of course:
-- return all groups where the manager -- is not one of the members select * fromGroup wherenot Members.Contains(Manager)
The argument of Contains()
may be a subquery or a simplified sub-query (7.2.4)
.
The following query returns a list of all groups which have at least one female member:
select * fromGroup where Members.Contains(Contact where Sex = 'F')
Contains() expressions can be nested:
-- find all groups which have at least one contact who is -- in a role whose name starts with 'Admin' select * fromGroup where Members.Contains(Contact where Roles.Contains(Role where Name like 'Admin%'))
You can see that Count
and Contains()
are
quite powerful constructs that let you write simple and complex queries without
the use of explicit joins. Join-free SOQL queries can be translated to SQL
thanks to a translator provided by Sooda described
in section "8.3. SOQL to SQL Conversion"
.
7.2.3.SoodaClass
SoodaClass
is a virtual property that is present in all Sooda classes. It returns name
of the class as defined in the mapping schema (3)
.
It can be used to distinguish between subclasses in polymorphic queries.
-- select vehicles which are bikes or super bikes select * from Vehicle v where v.SoodaClass = 'Bike' or v.SoodaClass = 'SuperBike'
7.2.4.Simplified Subqueries
SOQL provides a convenient notation for specifying subqueries that are used as arguments to EXISTS()
and Contains()
SELECT id FROM class WHERE condition
can be shortened to Class WHERE condition
. Thus the following two
SOQL queries are equivalent:
-- Using full subqueries select * from Group where Members.Contains(select ID from Contact where Name = 'Mary Manager')
-- Using simplified subqueries: select * from Group where Members.Contains(Contact where Name = 'Mary Manager')
The same rule applies to EXISTS():
select * from Group g where exists (Group g2 where g.ID <> g2.ID and g.Name = g2.Name)
7.2.5.Raw Queries
Raw queries can be used to take advantage of RDBMS-specific SQL constructs, such as full-text-search, custom operators and built-in functions. Raw queries are passed unmodified by the SOQL to SQL Conversion (8.3) .
To write a raw query, simply use RAWQUERY(text)
. The text
can be any SQL, but you
must be careful to correctly balance parenthesis. RAWQUERY()
may also be a potential security risk
if you pass unvalidated user input as raw queries.
// use SQL Server-specific full-text search function "contains()" select * from Contact where rawquery(contains(*,'quick'))
Raw queries may include translated field and class names. You can escape back to SOQL by using the SOQL{{...}}
notation:
-- find all contacts employed earlier than 10 years ago select * from Contact where rawquery(datediff(y, SOQL{{HireDate}}, getdate() > 10)
Double braces must be used because {0},{1},{2} notation is used to reference positional parameters.
7.3.SOQL Grammar
Formal syntax of SOQL is presented below. The following notation is used:
<symbol> ::= expression
- production[X]
- X is optional{X}
- X can be repeated zero or more timesa | b
- either a or b'token'
or TOKEN
- terminal symbolquery_expression ::= SELECT [TOP number] [DISTINCT] select_expressions FROM from_expressions [WHERE boolean_expression] [GROUP BY groupby_expressions] [HAVING boolean_expression] [ORDER BY sort_expressions] select_expressions ::= select_expression { ',' select_expression } from_expressions ::= from_expression { ',' from_expression } groupby_expressions ::= expression { ',' expression } sort_expressions ::= sort_expression { ',' sort_expression } select_expression := expression [ AS alias ] from_expression ::= class_name [ AS alias ] expression ::= boolean_or boolean_or ::= boolean_and { OR boolean_and } boolean_and ::= boolean_predicate { AND boolean_predicate } boolean_predicate ::= EXISTS '(' query_expression ')' | EXISTS '(' simplified_query ')' | boolean_relation boolean_expression ::= boolean_or class_or_relation_name ::= class_name | relation_name sort_expression := expression [ ASC | DESC ] boolean_relation ::= additive_expression | additive_expression '=' additive_expression | additive_expression '==' additive_expression | additive_expression '<>' additive_expression | additive_expression '!=' additive_expression | additive_expression '<' additive_expression | additive_expression '>' additive_expression | additive_expression '<=' additive_expression | additive_expression '>=' additive_expression | additive_expression LIKE additive_expression | additive_expression IS [NOT] NULL | additive_expression IN '(' expression { ',' expression } ')' additive_expression ::= multiplicative_expression | additive_expression '+' multiplicative_expression | additive_expression '-' multiplicative_expression multiplicative_expression ::= literal_expression | multiplicative_expression '*' literal_expression | multiplicative_expression '/' literal_expression | multiplicative_expression '%' literal_expression literal_expression ::= number string positional_parameter '(' query_expression ')' '(' expression ')' NULL TRUE FALSE SOODACLASS RAWQUERY '(' raw_query ')' function_call path_expression '(' class_name WHERE boolean_expression ')' '-' literal_expression NOT boolean_expression positional_parameter ::= '{' number [ ':' parameter_modifiers ] '}' parameter_modifiers ::= type_name function_call ::= function_name '(' [ function_arguments ] ')' function_arguments ::= expression { ',' expression } simplified_query ::= class_name WHERE boolean_expression path_expression ::= field_name | path_expression '.' field_name | path_expression '.' CONTAINS '(' expression ')' | path_expression '.' CONTAINS '(' query_expression ')' | path_expression '.' CONTAINS '(' simplified_query ')' | path_expression '.' COUNT | path_expression '.' SOODACLASS | '*' | path_expression '.' '*' -- the string may contain SOQL{{...}} fragments which will be -- inserted into the SQL sent to RDBMS raw_query ::= string alias ::= name class_name ::= name relation_name ::= name name ::= /* Non-empty sequence of letters, numbers and underscore characters, which cannot start with a digit */ number ::= /* Non-empty quence of digits with an optional decimal point: 123 or 3.141592 */ string ::= /* String literal in apostrophes such as 'Mary Manager'. You can represent the apostrophe character itself by doubling it thus d'Artagnan needs to be written as 'd''Artagnan' */
7.4.SOQL Where Clauses
SOQL expressions can be used to filter objects returned by the GetList()
or
LoadSingleObject()
methods. You simply pass the WHERE
clause as a string
argument to the constructor of the SoodaWhereClause
class and pass the
constructed where clause object to the appropriate method. You cannot specify a list of columns here, GetList()
and LoadSingleObject()
always operate on objects.
ContactList cl; // get the list of contacts whose names start with 'Mary' cl = Contact.GetList(new SoodaWhereClause("Name like 'Mary%'"));
Manually concatenating string literals can be dangerous and lead
to cross-site scripting vulnerabilities (http://en.wikipedia.org/wiki/Cross-site_scripting) in web applications.
Fortunately Sooda lets you separate string literals from the rest of the query code.
You can use the {0}, {1}, {2}
notation similar to Console.WriteLine()
.
Note that you do not need to quote the strings yourself nor surround {0}
with apostrophes. In fact
this would cause the '{0}' string to be treated literally and not as a parameter reference.
ContactList cl; string lookFor = "Mary Manager"; // get the list of contacts matching the specified name // note that {0} is not enclosed in apostrophes. cl = Contact.GetList(new SoodaWhereClause("Name = {0}", lookFor));
7.5.Typed SOQL Queries
Textual queries passed to SoodaWhereClause
have one deficiency: property/field names
that you use are not validated at compilation time. It is possible to write code that contains
references to non-existing fields. The compilation will succeed, but you will get runtime
errors.
When developing large systems that change often (such as workflow applications with ever-changing customer requirements) it may be beneficial to statically validate all queries used in the application to make sure that you only reference correct fields. This way, when you remove a field from the schema or change its type, the application will simply not compile and the compiler will show you statements that need to be corrected because of the original change.
Sooda implements typed SOQL queries, which let you write SOQL-like expressions in pure C# using a technique
called operator overloading. Sooda comes with a large set of classes that represent nodes in query expression
trees (such as relational operators, path expressions, Count
, Contains
, boolean and
,
or
, not
and so on).
These classes overload standard C# operators such as +
,-
,&&
,
||
so that they return elements of the syntax tree instead of performing actual computations.
To properly support path expressions, some schema-specific code needs to be generated as well which SoodaStubGen utility (8.1.1) takes care of. It generates a set of classes that let you build typed path expressions and typed collection expression builders. They both let you express almost all SOQL features. In some rare cases (such as SQL functions or subqueries) you can resort to text-based SOQL and even combine the two approaches in a single query.
Typed queries are very concise, because you do not need to wrap them with strings or use
any special objects (such as SoodaWhereClause
) that will parse them. You do not need
to use any form of string escaping and because there is no parsing involved whatsoever you can be
sure that the code is cross-site-scripting-safe.
The following table compares features of typed queries and text-based SOQL. The syntax for typed queries and operator precedence is based on the C# language. Note that other languages that support operator overloading may use different notation and/or different operator precedence.
SOQL | Typed Query | Remarks |
---|---|---|
path expressions | path expressions prefixed with ClassNameField |
You need to prefix all path expressions in typed queries with the ClassNameField where ClassName is
the name of the class the expression applies to. For example:
ContactField.PrimaryGroup.Manager.Name is equivalent to the following SOQL expression: PrimaryGroup.Manager.Name |
= | == | Equality operator |
<> | != | Inequality operator |
<= | <= | Less than or equal operator |
< | < | Less than |
> | > | Greater than |
>= | >= | Greater than or equal to |
and | && | Boolean conjunction |
or | || | Boolean alternative |
not | ! | Boolean negation |
+ | + | Addition or string concatenation |
- | - | Subtraction |
* | * | Multiplication |
/ | / | Division |
% | % | Modulus |
- | - | Unary minus |
like | Like() method | LIKE relational operator. Because C# does not support the definition of extra operators this is a method.
The following C# expression:
ContactField.Name.Like("Mary%");
is equivalent to the expression in SOQL:
Name like 'Mary%' |
Count | Count | Gets the number of elements in a collection. |
Contains(obj) | Contains(obj) | Determines whether obj is contained in the collection. |
Contains(X where condition) | ContainsXWhere( condition ) | Determines whether any object contained in the collection meets the specified condition. |
is null | IsNull() method | Determines whether the expression is null. This typed query predicate can be only used on nullable fields. It fails to compile when used on fields that are known to be not-null.
The following C# expression:
ContactField.PrimaryGroup.Manager.IsNull()is equivalent to the expression in SOQL: PrimaryGroup.Manager is null |
is not null | IsNotNull() method | Determines whether the expression is not null. This typed query predicate can be only used on nullable fields. It fails to compile when used on fields that are known to be not-null.
The following C# expression:
ContactField.PrimaryGroup.Manager.IsNoNull()is equivalent to the expression in SOQL: PrimaryGroup.Manager is not null |
in (a1,a2,...,aN) | In(a1,a2,...,aN) method | Determines whether the left hand side of the expression equals to any value on the right hand side
of the expression.
The following C# expression:
ContactField.PrimaryGroup.Manager.FirstName.In("Mary","Edmund")is equivalent to the expression in SOQL: PrimaryGroup.Manager.FirstName in ('Mary','Edmund') |
rawquery (s) | Soql.RawQuery(s) | Pass s unmodified in an SQL query sent to the RDBMS.
The following example shows a typed query which uses MSSQL-specific
full-text search function
Contact.GetList(Soql.RawQuery("contains(*,'quick')")); It is equivalent to this SOQL query: select * from Contact where rawquery(contains(*,'quick')) |
soodaclass | SoodaClass property | Gets the concrete class name of the object as defined in the mapping schema (3)
.
The following typed query retrieves a list of vehicles which are bikes or boats. Vehicle.GetList(VehicleField.SoodaClass.In("Bike","Boat")) It is equivalent to this SOQL query: select * from Vehicle where SoodaClass in ('Bike','Boat') |
in (subquery) | not supported | This feature is not supported in typed queries. You can use SoqlParser to parse it from a textual representation. |
exists (subquery) | not supported | This feature is not supported in typed queries. You can use SoqlParser to parse it from a textual representation. |
positional parameters: {0}, {1}, {2} | Soql.Param(0), Soql.Param(1), Soql.Param(2) | Reference to the positional parameter passed to the SoodaWhereClause constructor.
Positional parameters are not usually needed in typed queries because string literals
are safe by default. |
Examples of queries that can be formulated with typed queries include:
// find all contacts named 'Mary Manager' Contact.GetList(ContactField.Name == "Mary Manager"); // find all contacts whose name starts with 'Mary' (using LIKE operator) Contact.GetList(ContactField.Name.Like("Mary%")); // find all employees hired in the last year Employee.GetList(EmployeeField.HireDate > DateTime.Now.AddYears(-1)); // find all groups having at least one contact Group.GetList(GroupField.Members.Count > 1); // find all groups that Mary Manager is a member of Group.GetList(GroupField.Members.ContainsContactWhere(ContactField.Name == "Mary Manager")); // find all employees Employee.GetList(1 == 1); Employee.GetList(true); // return an empty list of employees by executing // a database query with a WHERE clause that is never true. // The same thing can be achieved without a database query // by creating a new EmployeeList() object. Employee.GetList(false); // return a list of contacts who do not belong to any group Contact.GetList(ContactField.PrimaryGroup.IsNull()); // find all contacts where Name is different from (FirstName + ' ' + LastName) Contact.GetList(ContactField.Name != ContactField.FirstName + " " + ContactField.LastName); // find all groups managed by Mary Manager (using named constant) Group.GetList(GroupField.Manager == Contact.Mary);
7.6.Language Integrated Query - LINQ
Sooda now supports LINQ, which has become the standard query language in .NET 3.5. LINQ has all the benefits of Typed Queries, which existed in Sooda before LINQ appeared. It is recommended that you use LINQ for all new code. Typed Queries remain in Sooda for backward compatibility.
7.6.1.LINQ sources
Each Sooda class contains a method called Linq()
which is the root of all LINQ queries.
If not followed by LINQ methods, Linq()
returns all objects of the given class:
foreach (Contact c in Contact.Linq()) Console.WriteLine(c.Name);
The Linq()
method is overloaded and you can pass a SoodaTransaction
and SoodaSnapshotOptions (6.3.2)
to it:
foreach (Contact c in Contact.Linq(SoodaSnapshotOptions.NoWriteObjects)) Console.WriteLine(c.Name);
foreach (Contact c in Contact.Linq(transaction, SoodaSnapshotOptions.Default)) Console.WriteLine(c.Name);
The AllQuery
property is a synonim for the Linq()
method:
foreach (Contact c in Contact.AllQuery) Console.WriteLine(c.Name);
Each 1-N and N-N relation has a corresponding LINQ source exposed as a property composed
of the relation name with the Query
suffix:
Contact boss = Contact.GetRef(1); foreach (Contact c in c.SubordinatesQuery) Console.WriteLine(c.Name);
LINQ methods described below become available on LINQ sources
as you import the System.Linq
namespace
(note System.Linq
and not Sooda.Linq
).
This is necessary for all LINQ queries except for the trivial cases such as the above.
7.6.2.LINQ queries
There are two ways of writing LINQ queries: method calls and LINQ keywords. These two are equivalent: the C#/VB.NET compiler translates LINQ keywords to method calls.
Let's start with method calls. Most LINQ methods accept lambda expressions where the only parameter is the so called range variable.
Contact.Linq().Where(c => c.Name == "Mary Manager");
In the above code, the Where()
method is called with the lambda expression
c => c.Name == "Mary Manager"
.
In this expression, c
is the range variable and
c.Name == "Mary Manager"
is the body of the lambda expression.
This code is equivalent to:
Contact.GetList(ContactField.Name == "Mary Manager");
The body of the lambda expression is specified in C#. Therefore you get the advantages of compile-time type checking which can detect errors such as misspelled property names and type mismatch (e.g. comparing an integer field to a string). However, you need to be aware that this code gets translated to SQL and executed by the database engine. If you use a construct that cannot be translated to SQL, you will get a runtime error. Therefore you must write lambda expressions carefully. A section below (7.6.6) lists the supported constructs. When in doubt, always test your code.
The Where()
method that we saw in the example is used to filter rows,
just like SQL WHERE
.
To sort results, use OrderBy()
and OrderByDescending()
:
Contact.Linq().OrderBy(c => c.Name); Contact.Linq().OrderByDescending(c => c.ContactId);
If you chain OrderBy()
calls, they will be executed in order:
Contact.Linq().OrderByDescending(c => c.ContactId).OrderBy(c => c.Name);
Here, the list is first sorted by ContactId
and then sorted again by Name
.
The result is that Name
has priority for ordering.
To write multiple order expressions as you are used to,
use ThenBy()
and ThenByDescending()
:
Contact.Linq().OrderBy(c => c.Name).ThenByDescending(c => c.ContactId);
// same result as in the previous example
Or, using LINQ keywords (more about them later):
from c in Contact.Linq() orderby c.Name, c.ContactId descending select c; // ditto
Select()
can be used to transform Sooda objects into any .NET object you want:
Contact.Linq().Select(c => c.Name);
Contact.Linq().Select(c => new { Id = c.ContactId, Name = c.Name);
Select()
fetches only the mentioned data from the database
and skips the Sooda collection cache.
To limit the number of rows, use Skip()
and Take()
:
Contact.Linq().OrderBy(c => c.Name).Skip(30).Take(10);
There are syntax extensions to C# and VB.NET that make LINQ queries look like SQL queries:
from c in Contact.Linq()
where c.Active && c != Contact.Mary
orderby c.LastSalary.Value descending, c.Name
select c.ContactId;
from
must come first, and select
must come last.
The C# compiler will replace the above query with method calls:
Contact.Linq() .Where(c => c.Active && c != Contact.Mary) .OrderByDescending(c => c.LastSalary.Value).ThenBy(c => c.Name) .Select(c => c.ContactId);
There are LINQ keywords for some, but not all LINQ methods. Sooda supports:
from ... in ...
- doesn't correspond to any method,
but merely introduces a LINQ source and a range variablewhere
- corresponds to Where()
orderby
, ascending
(default), descending
- correspond to OrderBy()
, OrderByDescending()
,
ThenBy()
, ThenByDescending()
select
- corresponds to Select()
,
except when given just the range variable
(select
is mandatory in the C# LINQ keyword syntax)group ... by ... into ...
- corresponds to GroupBy()
Sooda doesn't support join ... in ... on ... equals ...
nor let
LINQ keywords.
These were the most commonly used LINQ query methods. The methods below might be useful especially when you build queries dynamically.
Reverse()
reverses the order of results.
OfType<NewType>()
filters results by their type, for example:
var vehicles = Vehicle.Linq().Where(v => v.Owner == Contact.Mary); var cars = vehicles.OfType<Car>(); var bikes = vehicles.OfType<Bike>();
Except()
, Intersect()
and Union()
are set operations on two queries:
Contact.Linq().Where(c => c.Subordinates.Any()) .Union(Contact.Linq().Where(c => c.LastSalary.Value > 100));
or a query and a collection of objects:
Contact.Linq().Where(c => c.Name.Like("E%") .Except(new Contact[] { Contact.Ed });
Select()
has an overload that passes a zero-based row index
to the lambda expression:
Contact.Linq().Select((c, i) => new { RowNum = i, Name = c.Name });
Distinct()
can be applied on the result of Select()
in order to filter out duplicates:
IEnumerable<Contact> owners = Vehicle().Linq().Select(v => v.Owner).Distinct();
Use GroupBy()
for aggregate queries. GroupBy()
must be followed by a Select()
and possibly OrderBy()
and Where()
. The result of GroupBy()
is a sequence
of IGrouping<TKey, TElement>
, of which you can access:
Key
- the grouping keyCount()
, Count(predicate)
, Min(selector)
, Max(selector)
,
Sum(selector)
, Average(selector)
, Any(predicate)
, All(predicate)
var contactTypes = Contact.Linq().GroupBy(c => c.Type.Code).OrderBy(g => g.Key).Select(g => new { Type = g.Key, Count = g.Count() }); var contactTypes2 = from c in Contact.Linq() group c by c.Type.Code into g orderby g.Key select new { Type = g.Key, Count = g.Count() }; // same
var averageSalaryByContactType = from c in Contact.Linq() group c by c.Type.Code into g orderby g.Key select new { Type = g.Key, AvgSalary = g.Average(c => c.LastSalary.Value) };
To group by multiple expressions, use anonymous type as the key:
var cheapestProducts = from p in Product.Linq() group p by new { p.Category, p.Vendor } into g orderby g.Count() descending, g.Key.Category, g.Key.Vendor select new { g.Key.Category, g.Key.Vendor, g.Count() };
7.6.3.Deferred query execution
It is important to understand when LINQ queries are executed.
Methods such as Where()
, Select()
and OrderBy()
construct queries but do not execute them nor even translate to SQL.
{
IEnumerable<Contact> ce = Contact.Linq().Where(c => c.Name == "Mary Manager");
}
This code constructed a query, but didn't execute it.
Queries that return a sequence of objects (IEnumerable<T>
)
get translated to SQL and executed as late as the sequence is enumerated.
IEnumerable<Contact> ce = Contact.Linq().Where(c => c.Subordinates.Any()); // no database access yet // ... foreach (Contact c in ce) // query gets translated to SQL and executed as soon as we reach "foreach" { Console.WriteLine(c.Name); }
IEnumerable<Contact> ce = Contact.Linq().Where(c => c.Subordinates.Any()); for (int i = 0; i < 1000; i++) { // BAD idea: 1000 queries (subject to Sooda cache) foreach (Contact c in ce) { Console.WriteLine(c.Name); } }
// CORRECT: one query inside ToList() List<Contact> cl = Contact.Linq().Where(c => c.Subordinates.Any()).ToList(); for (int i = 0; i < 1000; i++) { foreach (Contact c in cl) { Console.WriteLine(c.Name); } }
// even better: only fetch data we need List<string> sl = Contact.Linq().Where(c => c.Subordinates.Any()).Select(c => c.Name).ToList(); for (int i = 0; i < 1000; i++) { foreach (string s in sl) { Console.WriteLine(s); } }
As a rule of thumb, if you need to iterate LINQ query results more than once,
you should materialize the results to a collection and then iterate over this collection.
The most efficient way is calling ToList()
to get a generic list.
Alternatively you may call ToArray()
:
Contact[] ca = Contact.Linq().OrderBy(c => c.Name).ToArray();
Using LINQ with Sooda lists, such as ContactList
, is discouraged.
However you may need them for interoperability with legacy code.
To convert LINQ query results to a Sooda list, import Sooda.Linq
(in addition to the usual System.Linq
), call ToSoodaObjectList()
and pass the result to a Sooda list constructor:
using Sooda.Linq; ContactList cl = new ContactList(Contact.Linq().OrderByDescending(c => c.ContactId).ToSoodaObjectList());
This technique is not limited to LINQ.
You may use ToSoodaObjectList()
to convert any generic collections,
such as List<Contact>
, to Sooda lists.
Another interesting fact is that .NET execution of the lambda expressions takes place during query execution, not construction:
string nameFilter = "Unused"; var ce = Contact.Linq().Where(c => c.Name == nameFilter); nameFilter = "Mary Manager"; Assert.AreEqual("Mary Manager", ce.First().Name); // First() (see below) executes the query and uses current values of variables
var ce = Contact.Linq().Where(c => c.Name == MyMethod()); // MyMethod() not called yet string result = ce.First().Name; // MyMethod() called from First()
Sometimes you may want to build LINQ queries dynamically, e.g.
var query = Contact.Linq().OrderBy(c => c.Id); if (nameFilter != null) query = query.Where(c => c.Name == nameFilter); foreach (Contact c in query) { ... }
The key thing here is the type of the query
variable.
If you want LINQ methods to build the query for the database,
use var
or IQueryable<...>
.
In contrast, the following code will fetch all Contacts
from the database and do the filtering on the .NET side
(using LINQ2Objects):
IEnumerable<Contact> query = Contact.Linq().OrderBy(c => c.Id); if (nameFilter != null) { // BAD idea: this is Enumerable.Where method // which will execute the above query first // and then do the filtering in memory. query = query.Where(c => c.Name == nameFilter); } foreach (Contact c in query) { ... }
Deferred query execution applies only to methods that return a sequence of objects. Below are described methods that return single objects (7.6.4) and scalar results (7.6.5) - these methods actually execute queries instead of constructing it.
7.6.4.Querying for single objects
Often you need a single object that meets the specific criteria. There are LINQ methods that return a single object instead of a list.
Contact c = Contact.Linq().OrderByDescending(c => c.LastSalary.Value).First();
This code will return a person with highest last salary.
It throws InvalidOperationException
if there are no Contacts.
If you want a null
instead, use FirstOrDefault()
(origin of this name: default value for reference objects is null
).
You may also want to use Last()
or LastOrDefault()
:
Contact c = Contact.Linq().OrderBy(c => c.LastSalary.Value).LastOrDefault();
To make sure there is exactly one object that meets the criteria,
use Single()
:
Contact c = Contact.Linq().Where(c => c.Name == "Mary Manager").Single();
Single()
throws an InvalidOperationException
if there isn't one match. SingleOrDefault
will return
null
if there are no matches, but still throw
InvalidOperationException
if there is more than one match.
You almost certainly need a filter for Single()
,
thus this method is overloaded:
Contact c = Contact.Linq().Single(c => c.Name == "Mary Manager");
There are also overloads of First()
, FirstOrDefault()
,
Last()
, LastOrDefault()
that accept a filter.
In total, there are six methods, each with two overloads. The pattern is:
{First|Last|Single}[OrDefault]([filter])
The "OrDefault" methods can be used in subqueries.
The following code lists a name of the top paid Contact
for each ContactType
:
IEnumerable<string> se = from t in ContactType.Linq() orderby t.Code select (from c in Contact.Linq() where c.Type == t orderby c.LastSalary.Value select c.Name).LastOrDefault();
Note how the subquery uses the range variable of the outer query (t
in this case).
7.6.5.Scalar queries
Many times you want to ask about the number of objects,
not caring about their contents. Use Count()
for that:
int c = Contact.Linq().Where(c => c.LastSalary.Value > 100).Count();
Count()
also has an overload that accepts a filter:
int c = Contact.Linq().Count(c => c.LastSalary.Value > 100); // same as above
If all you care is whether the count is zero or not,
Any()
performs better:
bool b = Contact.Linq().Where(c => c.LastSalary.Value > 100).Any();
bool b = Contact.Linq().Any(c => c.LastSalary.Value > 100);
You may also ask if all objects meet some criteria:
bool b = Contact.Linq().All(c => c.LastSalary.Value > 100);
Use Contains()
to check if the query would return a specific Sooda object:
bool b = Contact.Linq().Where(c => c.LastSalary.Value > 100).Contains(Contact.Mary); // this example wasn't clever, because you could simply write: Contact.Mary.LastSalary.Value > 100
There are also aggregate functions: Average()
, Min()
,
Max()
and Sum()
:
decimal avgSalary = Contact.Linq().Average(c => c.LastSalary.Value); decimal minEmployeeSalary = Contact.Linq().Where(c => c.Type == ContactType.Employee).Min(c => c.LastSalary.Value); decimal maxManagerSalary = Contact.Linq().Where(c => c.Type == ContactType.Manager).Max(c => c.LastSalary.Value); decimal maxManagerSalary2 = (from c in Contact.Linq() where c.Type == ContactType.Manager select c.LastSalary.Value).Max(); // same as above decimal totalSalary = Contact.Linq().Sum(c => c.LastSalary.Value);
If there are no objects for calculation, Average()
, Min()
and Max()
throw InvalidOperationException
whereas Sum()
returns zero.
Count()
, Any()
, All()
, Contains()
,
Average()
, Min()
, Max()
and Sum()
can also be used in subqueries.
The following query calculates top salary per ContactType
:
var q = from t in ContactType.Linq() orderby t.Code select new { ContactType = t.Code, TopSalary = Contact.Linq().Where(c => c.Type == t).Max(c => c.LastSalary.Value) };
7.6.6.LINQ expressions
This section describes the subset of C# which Sooda can translate to SQL.
Path expressions are same as in SOQL, except that they start with a range variable:
Contact.Linq().Where(c => c.Name == "Mary Manager");
Contact.Linq().Where(c => c.Manager.PrimaryGroup.Id == 10);
For comparisons, use C# operators: ==, !=, <, <=, >, >=.
However, if your field is of a SqlType, you need to compare the Value
property:
Contact.Linq().Where(c => c.LastSalary.Value > 123);
Logical operators are: &&, ||, ! and a?b:c. On numbers you may use arithmetic operators: +, -, *, /, % (modulo) and unary negation (-). You can concatenate strings with the + operator.
The syntax of null tests depends on the field type. You do the tests just like you would normally do in C#:
null
.IsNull
property.HasValue
property.Contact.Linq().Where(c => c.Manager != null); Contact.Linq().Where(c => !c.LastSalary.IsNull); // if using SqlTypes Contact.Linq().Where(c => c.LastSalary.HasValue); // if using Nullable
Sooda detects subexpressions that can be evaluated on the .NET side, before constructing the SQL. For example:
Contact.Linq().Where(c => c.Name == myVariable.MyMethod().MyOtherMethod());
Here the SQL translation of myVariable.MyMethod().MyOtherMethod()
is just a string literal
that is the result of this expression computed in .NET.
Remember about deferred query execution (7.6.3)
.
To use the SQL LIKE operator, you need to import the Sooda.Linq
namespace
(in addition to the usual System.Linq
):
using Sooda.Linq; Contact.Linq().Where(c => c.Name.Like("C%")); // as a bonus, the Like() method becomes available on all .NET strings bool ok = "Foo".Like("%o"); // computed in .NET, not the database engine
Collection sizes are available either through the Count
property
or the Count()
LINQ extension method:
Contact.Linq().Where(c => c.Subordinates.Count == 2);
Contact.Linq().Where(c => c.Subordinates.Count() == 2); // same
It makes no difference if you query for collection properties with or without the Query
suffix:
Contact.Linq().Where(c => c.SubordinatesQuery.Count() == 2); // same as above
You may test if collections contain a specific object:
Contact.Linq().Where(c => c.Subordinates.Contains(Contact.Ed));
For more sophisticated collection tests, use Any()
:
Contact.Linq().Where(c => c.Subordinates.Any(s => s.Name == "Ed Employee"));
The above code was the LINQ counterpart to:
Contact.GetList(ContactField.Subordinates.ContainsContactWhere(ContactField.Name == "Ed Employee"));
The SQL/SOQL IN
operator is expressed with the .NET Contains()
method:
Contact.Linq().Where(c => new Contact[] { Contact.Mary, Contact.Ed }.Contains(c.Manager));
which is equivalent to the Typed Query:
Contact.GetList(ContactField.Manager.In(Contact.Mary, Contact.Ed));
The SoodaClass (7.2.3)
property known from SOQL is written in LINQ as GetType().Name
:
Vehicle.Linq().Where(c => c.GetType().Name == "Car");
The following constructs are unique to LINQ and have no SOQL counterparts.
?? is the C#'s null-coalesce operator. When used in queries, it gets translated to SQL COALESCE
:
Contact.Linq().Where(c => (c.Manager ?? c.PrimaryGroup.Manager) == Contact.Mary);
You may use the "is" type-test operator:
Contact.Linq().Where(c => c.Vehicles.Any(v => v is Bike || c == Contact.Mary));
Instead of comparing collection size against zero, you may use Any()
:
Contact.Linq().Where(c => c.Subordinates.Any());
// same as Contact.Linq().Where(c => c.Subordinates.Count > 0);
All()
is available for collections as well:
Contact.Linq().Where(c => c.Subordinates.All(s => s.LastSalary.Value > 100));
GetPrimaryKeyValue()
is a generic way to access the primary key.
GetLabel()
returns SoodaObject's label, as specified in the database schema (3.2)
.
The boolean argument to GetLabel()
is ignored in LINQ queries.
Contact.Linq().Select(c => new { ClassName = c.GetType().Name, Id = c.GetPrimaryKeyValue(), Label = c.GetLabel(false) );
Contact.Linq().Where(c => (int) c.GetPrimaryKeyValue() > 3);
The following string methods may be used: Concat()
,
IsNullOrEmpty()
, Remove()
,
Substring(startIndex, length)
,
Replace()
, ToLower()
and ToUpper()
.
StartsWith()
, EndsWith()
and Contains()
can be used with the restriction that the argument cannot come from the database.
You can call ToString()
on int, long, double, decimal and bool expressions.
Also, some static methods from the System.Math
class are supported:
Abs()
, Acos()
, Asin()
, Atan()
,
Cos()
, Exp()
, Floor()
, Pow()
,
Round()
, Sign()
, Sin()
, Sqrt()
,
Tan()
, however their support in databases other than SQL Server hasn't been verified.
Methods not mentioned above have no translation to SQL.
It is safe to call them if they can be evaluated before the SQL translation,
and in Select()
.
You can also provide an extra method/property that returns a LINQ expression corresponding to your custom method/property. This LINQ expression can then be translated to SQL.
Let's start with properties, which are simpler because they don't have parameters.
To specify an expression that corresponds to a property, create a public static
property
which is by convention named the same as your custom property plus the Expression
suffix.
The getter of the expression property must return Expression<Func<TClass,TResult>
where TClass
is the class the properties are defined in and TResult
is the type of your custom property.
Example for a property in the Contact
class:
public string NameAndType { get { return Name + " (" + Type.Code + ")"; } } public static Expression<Func<Contact, string>> NameAndTypeExpression { get { return t => t.Name + " (" + t.Type.Code + ")"; } }
With the above code you can use NameAndType
anywhere in LINQ and it will get translated to SQL.
Defining expressions for custom methods is similar: you write a method named with the Expression
suffix.
If your custom method is not overloaded, you can keep the expression method parameterless.
Otherwise, use same parameter types as in your custom method.
Never use the parameters in the expression method. Instead, specify the corresponding parameters in the lambda expression.
public static double CircleArea(double r) { return Math.PI * r * r; } public static Expression<Func<double, double>> CircleAreaExpression(double dummyR) { return r => Math.PI * r * r; }
If the custom method is not static, the first lambda parameter must correspond to this
:
public bool EarnsMoreThan(Contact other) { return this.LastSalary.HasValue && other.LastSalary.HasValue && this.LastSalary.Value > other.LastSalary.Value; } public static Expression<Func<Contact, Contact, bool>> EarnsMoreThan() { return (t, other) => t.LastSalary.HasValue && other.LastSalary.HasValue && t.LastSalary.Value > other.LastSalary.Value; }
8.Advanced Topics
Information provided in the following sections can be useful if you want to understand the inner workings of Sooda or to perform advanced tasks using the command line tools that come with Sooda.
8.1.Tools
Sooda provides command-line tools that are used during the build process (4.1) to create a Data Access Layer library and some additional tools that can help you manage mapping schema files (3) . The following sections explain each tool and provide guidelines for their use.
8.1.1.SoodaStubGen
SoodaStubGen utility is responsible for generating stub code for a Data Access Layer. The generated code needs to be compiled with the rest of the application code as described in section "4.1. Compilation Process" . You usually invoke SoodaStubGen as part of your automated build process to ensure that the generated code (8.4) is in sync with the mapping schema (3) . Visual Studio 2003 and 2005 are also supported.
This section describes command line options supported by SoodaStubGen and provides guidelines on usage of this tool.
8.1.1.1.Install Location
By default SoodaStubGen is installed in:
%SOODA_DIR%\bin\net-2.0\SoodaStubGen.exe
%SOODA_DIR%
is Sooda installation path. Sooda Installer registers the SOODA_DIR
environment variable to point to this directory so you can use %SOODA_DIR%
instead
of specifying the exact path in your build file.
If your project relies on a specific version of Sooda it is recommended to create a local copy
of the %SOODA_DIR%\bin\net-X.Y
directory within your source code structure and
use relative paths.
8.1.1.2.Command-line options
Options to SoodaStubGen can be passed in a *.soodaproject (4.2) file or directly as command line parameters. The latter option is available for compatibility with older versions of Sooda and using Sooda Project files is generally recommended.
SoodaStubGen.exe usage is:
SoodaStubGen.exe path_to_soodaproject_file
path_to_soodaproject_file
is a full path to a *.soodaproject file. Sooda Project files are described
in section "4.2. Sooda Project Files - *.soodaproject"
.
If you prefer to use command-line options, the usage is:
SoodaStubGen.exe [options]
[options]
represents a whitespace-separated sequence of options.
Most options have equivalent Sooda Project settings which are described
in section "4.2. Sooda Project Files - *.soodaproject"
.
The following options are available:
--schema schemafile.xml
- path to schema file - equivalent to <schema-file />--namespace NS
- output namespace - equivalent to <output-namespace />--output PATH
- output path - equivalent to <output-path />--lang N
or -l N
- sets the stub language to N
- equivalent to <language />--assembly-name assemblyname
- output assembly name - equivalent to <output-assembly />--project T
or -p T
- sets external project type to T
- equivalent to <external-projects />--separate-subs
- enables separate compilation of stubs (4.1.2)
- equivalent to <stubs-compiled-separately>true</stubs-compiled-separately>--merged-subs
- enables standard compilation of stubs (4.1.1)
<stubs-compiled-separately>false</stubs-compiled-separately>--schema-embed-xml
- generate _DBSchema.xml
to be embedded in the output assembly - equivalent to <embedded-schema-type>Xml</embedded-schema-type>--schema-embed-bin
- generate _DBSchema.bin
to be embedded in the output assembly - equivalent to <embedded-schema-type>Binary</embedded-schema-type>--nullable-as Boxed|Raw|RawWithIsNull|SqlType|Nullable
- sets the representation (4.2.1)
of nullable fields - equivalent to <nullable-representation />--not-null Boxed|Raw|RawWithIsNull|SqlType|Nullable
- sets the representation (4.2.1)
of not-null fields - equivalent to <not-null-representation />--base-class N
- use N as a base class for all generated stubs - equivalent to <base-class-name />--null-propagation
- enable null propagation - equivalent to <null-propagation>true</null-propagation>--no-null-propagation
- disable null propagation - equivalent to <null-propagation>false</null-propagation>The following options have no project file equivalents:
--rebuild-if-changed
- rebuilds the stubs if the output files do not exist or are older than
the source file (schema and included files). This is the default behavior.--force-rebuild
- rebuilds stub files unconditionally--rewrite-skeletons
- rewrite all skeletons. This option destroys all customizations made to skeleton classes. Use with caution.--rewrite-projects
- rewrite external project files instead of updating them This option destroys all non-standard project settings. Use with caution.--write-project FN
- write a *.soodaproject file to the specified file8.1.1.3.SoodaCompileStubs
SoodaCompileStubs utility can be used to precompile generated stubs into a separate DLL. Separate stub compilation is an advanced feature of Sooda and should be used with caution as described in section "4.1.2. Separate Stub Compilation" .
The utility is located in the same directory as SoodaStubGen (8.1.1.1) . It is a commmand-line program (no GUI is provided) that is meant to be part of an automated build process. You should run it after SoodaStubGen to automatically compile stub code generated by it.
The usage is:
SoodaCompileStubs assembly_base_name stubs_dir [extra_files]
assembly_base_name
- is the base name of the output assembly.
SoodaCompileStubs will generate assembly_base_name.Stubs.dll
that contains stubs for
skeleton classes contained in assembly_base_name.dll
.
stubs_dir
- is the path to the generated Stubs
directory. The directory contains the following files:
true
or when --separate-stubs
command line option is passed to it.
extra_files
- is a list of additional source files to be compiled with the skeleton classes.
The files should be written in C# and they should include the same assembly-level attributes as
assembly_base_name.dll
will have. It is recommended to reuse the same AssemblyInfo.cs
file for separate stub compilation and normal compilation of assembly_base_name.dll
.
Assuming your output DAL assembly is DAL.dll and the source files are located in the DAL
directory,
typical command line options to generate DAL.Stubs.dll are:
SoodaCompileStubs DAL source\DAL\Stubs source\DAL\AssemblyInfo.cs
When compiling the DAL.dll you should reference source\DAL\Stubs\DAL.Stubs.dll
. It is important to
not to remove the source\DAL\Stubs\DAL.dll
file as it is necessary for compilation.
SoodaCompileStubs checks modification dates on source files in the Stubs directory and recompiles the output only if the files are newer than the resulting DLL or if the DLL does not exist.
8.1.1.4.SoodaFixKeygen
There are situations where you need to synchronize the contents of the KeyGen
table (6.9)
to match actual primary key values. Do this after running any SQL script that inserts new rows
into mapped tables. If you fail to synchronize the KeyGen
table, Sooda will generate non-unique primary
keys which will cause primary key constraints to be violated.
For example, assume that the KeyGen
table contains a record where key_name="Foo" and key_value="100".
You now run an SQL script that inserts new records into Foo
table:
insert into Foo(id,...) values (101,...); insert into Foo(id,...) values (102,...); insert into Foo(id,...) values (103,...); insert into Foo(id,...) values (104,...); ... insert into Foo(id,...) values (999,...);
After you do this, you need to update the appropriate KeyGen record to have key_value = 1000, because this is the next value that should be generated for the primary key. If you fail to do so, newly created objects will get IDs of 100, 101, 102 which are invalid.
SoodaFixKeygen is a command-line utility that reads a mapping schema (3) file and generates an SQL script to properly update the KeyGen table. The SQL is written to the specified file.
Usage is:
SoodaFixKeygen soodaschema.xml outputFile.sql
After running the command, outputFile.sql
will contain statements similar to:
delete from KeyGen; insert into KeyGen select 'Role',coalesce(max(id),0) + 1 from _Role union select 'Contact',coalesce(max(id),0) + 1 from Contact union select 'Group',coalesce(max(id),0) + 1 from _Group union select 'Vehicle',coalesce(max(id),0) + 1 from Vehicle union select 'Car',coalesce(max(id),0) + 1 from Vehicle union select 'Bike',coalesce(max(id),0) + 1 from Vehicle
In order to properly update the KeyGen table, you should run the generated SQL code using SQL Query Analyzer or any other SQL client utility. Note that the SQL generated is specific to SQL Server, other databases may require some post-processing.
8.1.1.5.SoodaSchemaTool
SoodaSchemaTool utility can be used to perform various tasks related to mapping schema files (3) .
Command-line Usage:
SoodaSchemaTool command [command_options]
command
is the action that should be performed, command_options
are command-specific
options. The following commands are supported:
help
- displays an overview of available commandsgenddl
- generates DDL (to create tables, public keys, foreign keys) from the mapping schemagenschema
- reverse-engineer database to create the approximate mapping schemaupdateschema
- reverse-engineer database and update the mapping schema with changes made to the tablesThe following sections describe command line options and usage information for all supported commands:
8.1.1.5.1.help
Usage:
SoodaSchemaTool help [commandName]
Displays a list of available commands. When commandName
is provided,
a description of the command and a list of available options are displayed.
8.1.1.5.2.genddl
Usage:
SoodaSchemaTool genddl SoodaSchema.xml > output.sql
Generates DDL commands to create tables, public keys, foreign keys based on the mapping schema and writes them to the standard output. The SQL uses T-SQL (Microsoft SQL-Server specific) syntax. You should use shell redirection mechanism to output the SQL to a file.
8.1.1.5.3.genschema
Usage:
SoodaSchemaTool genschema -databaseType DBTYPE -connectionString CS -outputFile OF
Connects to the specified database and generates an approximate mapping schema file. The following parameters are accepted.
-databaseType DBTYPE
- specifies database type. Currently only mssql
is supported which
is also the default value, so you can omit this parameter when connecting to MS SQL Server-connectionString CS
- specifies connection string. The connection string is database-dependent.
For Microsoft SQL, the connection string used for the local database named MyDatabase is:
Integrated Security=true;Initial Catalog=MyDatabase;Server=.
-outputFile OF
- name of the output mapping schema file (3)
.Schema generation follows the following rules to convert between the database structure and the schema file:
primaryKey="true"
declarations to appropriate fields.8.1.1.5.4.updateschema
Usage:
SoodaSchemaTool updateschema -databaseType DBTYPE -connectionString CS -schemaFile IF [-outputSchemaFile OF] [-updateTypes] [-updateSizes] [-updateNullable] [-updatePrimaryKeys] [-tableName T]
Connects to the specified database and updates the mapping schema file. The following parameters are accepted.
-databaseType DBTYPE
- specifies database type. Currently only mssql
is supported which
is also the default value, so you can omit this parameter when connecting to MS SQL Server-connectionString CS
- specifies connection string. The connection string is database-dependent.
For Microsoft SQL, the connection string used for the local database named MyDatabase is:
Integrated Security=true;Initial Catalog=MyDatabase;Server=.
-schemaFile IF
- name of the mapping schema file (3)
to read.-outputSchemaFile OF
- name of the output file to write. If not provided, the schema file is
updated in place.-updateTypes
- update type
attribute of each <field /> based on
the actual database field types.-updateSizes
- update size
attribute of each <field /> based on
the actual database field sizes.-updateNullable
- update nullable
attribute of each <field /> based on
the actual database field nullability.-updatePrimaryKeys
- update primaryKey
attribute of each <field /> based on
the actual primary key constraints.-tableName T
- limit updates to the table T
. If you omit this parameter
all <table /> elements will be updated8.2.Logging
Logging in Sooda can be used to trace Sooda components such as transaction management, caching, data sources and SOQL to SQL conversion. Having a trace file in place can be useful for post-execution analysis of your program and to understand the inner workings of Sooda.
The following logging mechanisms are supported:
console
- write all log messages to the consolenull
- no log messages are writtennlog
- use NLog (http://www.nlog-project.org/) to manage logslog4net
- use log4net (http://logging.apache.org/log4net) to manage logs
To configure logging, add the sooda.logging
entry to the appropriate configuration file, an application configuration file:
<configuration> <appSettings> <add key="sooda.logging" value="console"/> </appSettings> </configuration>
or a shared XML configuration file:
<configuration> <sooda> <logging> console </logging> </sooda> </configuration>
To enable logging in code, you need to set the static Implementation
property
of the Sooda.Logging.LogManager
class to an instance of appropriate logging implementation class.
ConsoleLoggingImplementation
- for console loggingNullLoggingImplementation
- for no loggingNLogLoggingImplementation
- for logging with NLogLog4NetLoggingImplementation
- for logging with log4netusing Sooda.Logging; class Program { static void Main(string[] args) { // turn on console logging LogManager.Implementation = new ConsoleLoggingImplementation(); } }
When using NLog or Log4Net you need to provide additional configuration files. Refer to the documentation of the product in question for more information.
To write your own logging provider, you need to create a class that implements the
Sooda.Logging.ILoggingImplementation
interface defined by Sooda and
put the fully qualified name of the class in sooda.logging
configuration setting:
<configuration> <appSettings> <add key="sooda.logging" value="MyNamespace.MyLoggingProvider, MyAssembly, Version=..., PublicKeyToken=..."/> </appSettings> </configuration>
Each logging message in Sooda is associated with a source that identifies the component that emitted the log. When using NLog or log4net, you can use the source to filter log messages. When console logging is used, there is no way to specify which messages are emitted so it is recommended to use one of the mentioned log routing packages or disable logging entirely, because emitting large amounts of tracing information can severely slow down the application.
The following sources are defined:
Sooda.Cache
- Level-2 caching related messagesSooda.Config
- configuration-related messagesSooda.FactoryCache
- factory cache messagesSooda.ListSnapshot
- GetList()-related messagesSooda.ManyToManyCollection
- many-to-many collections supportSooda.OneToManyCollection
- one-to-many collections supportSooda.Object
- per-object diagnostics messages (lazy loading, caching)Sooda.Transaction
- transaction managementSooda.SqlDataSource
- messages specific to SQL data sourceSooda.SQL
- SQL queries sent down to the RDBMS
Most logging messages are emitted with the lowest severity available (Trace
for NLog,
Debug
for log4net), but Sooda also uses the Error
and Warning
severities as necessary.
Typically you should keep detailed logging disabled, with the possible exception of tracing SQL queries
emitted through Sooda.SQL
and increase log level to diagnose and fix bugs in your code
or Sooda code. Error and warning messages should never be filtered out.
8.3.SOQL to SQL Conversion
Relational databases do not understand Sooda schemas nor SOQL language,
therefore a translation from SOQL to SQL is necessary before any query can be executed.
Sooda comes with a SoqlToSqlConverter
class that does the necessary translation.
The result of the conversion is dependent on the mapping schema (3)
, because SOQL to SQL converter
must translate classes to database table names and named constants to literals. As database engines implement
slightly different SQL dialects, you must also pass
an instance of an SQL builder class that implements ISqlBuilder
interface and abstracts the SQL dialect
detail. Sooda comes with the following SQL builders for the most popular databases:
SqlServerBuilder
- Microsoft SQL Server 2000 and 2005OracleBuilder
- Oracle 8i and aboveMySqlBuilder
- MySQL 4.x and abovePostgreSqlBuilder
- PostgreSQL 7.3 and aboveBy default Sooda does the SOQL to SQL conversion automatically for all GetList-based queries. Data Source (6.12) classes provide methods that can be used to issue SOQL queries on a database. There are situations where you might want to do the conversion by hand, for example when you need to modify/instrument the result of conversion before executing it.
To start the conversion, you need to pass the tree representation of your SOQL query,
namely a SoqlQueryExpression
object, obtained it by calling SoqlParser.ParseQuery()
.
SoqlToSqlConverter writes the result of conversion to the specified TextWriter
object - you can use an
instance of the StringWriter
class to store the result in a string variable.
string soqlQueryText = "soql query"; SchemaInfo schemaInfo; // parsed schema // SQL-Server specific SQL Builder object ISqlBuilder builder = new SqlServerBuilder(); // parse the query SoqlQueryExpression query = SoqlParser.ParseQuery(soqlQueryText); // create a StringWriter to write the results to StringWriter sw = new StringWriter(); // create an instance of converter SoqlToSqlConverter converter = new SoqlToSqlConverter(sw, schemaInfo, builder); // set various options converter.IndentOutput = this.IndentQueries; converter.GenerateColumnAliases = false; converter.ConvertQuery(queryExpression); // put the results in a string string sqlText = sw.ToString();
The following sections provide an overview of the SOQL to SQL conversion process. Knowing the SQL that is generated from various SOQL constructs may be useful to fully understand the cost of some Sooda features, but it is not generally needed for normal use of Sooda.
8.3.1.Path Expressions
During the SOQL to SQL Translation, path expressions are converted to ordinary SQL joins. Consider this example SOQL query:
SELECT * FROM Contact WHERE PrimaryGroup.Manager.Name = 'Mary Manager'
During conversion, each path expression prefix gets a unique table alias and appropriate joins are generated:
select t0.id as [ContactId], t0.primary_group as [PrimaryGroup], t0.type as [Type], t0.name as [Name], t0.active as [Active], t0.last_salary as [LastSalary] from Contact t0 left outer join _Group t1 on (t0.primary_group = t1.id) left outer join Contact t2 on (t1.manager = t2.id) where (t2.name = 'Mary Manager')
We have the following prefixes:
t0
t1
t2
The number of joins required is always equal to the number of dots in the path expression. SOQL to SQL converter is smart enough to use the minimal number of joins when you reuse path expression prefixes. Consider a query that finds all contacts who belong to groups managed by 'Mary Manager' or whose managers earn more than one million is:
select * from Contact where PrimaryGroup.Manager.Name = 'Mary Manager' or PrimaryGroup.Manager.LastSalary > 1000000
The same query translated to SQL has only two joins because both LastSalary and Name use the same prefix 't2':
select t0.id as [ContactId], t0.primary_group as [PrimaryGroup], t0.type as [Type], t0.name as [Name], t0.active as [Active], t0.last_salary as [LastSalary] from Contact t0 left outer join _Group t1 on (t0.primary_group = t1.id) left outer join Contact t2 on (t1.manager = t2.id) where ((t2.name = 'Mary Manager') OR (t2.last_salary > 1000000))
8.3.2.Collections
The result of SOQL to SQL conversion of collection operators depends on the type of collection and the operation used. There are 3 possible operations on collections:
Count
Contains(obj)
- where obj is an objectContains(subquery)
The following sections demonstrate the result of SOQL to SQL conversion for one-to-many (8.3.2.1) and many-to-many (8.3.2.2) collections.
8.3.2.1.One-To-Many Collections
Operations on one-to-many collections (such as Group.Members
where each person may belong to one group),
are translated to subqueries on the "child" table (Contact
in this example).
The Count
operation gets converted to a subquery containing the select count(*)
expression.
Consider this query:
select * fromGroup where Members.Count > 3
It translates to:
select t0.id as [Id], t0.manager as [Manager], t0.name as [Name] from _Group t0 where ((select count(*) from Contact where primary_group=t0.id) > 3)
Converting Contains()
on one-to-many collections is easy, because the expression
parentObject.collection.Contains(childObject)
can be translated to
childObject.ReferenceField = parentObject
, so this query:
select * fromGroup where Members.Contains(Contact.Mary)
translates to
select t0.id as [Id], t0.manager as [Manager], t0.name as [Name] from _Group t0 whereexists (select * from Contact where primary_group=t0.id and id in (1))
As you can see Contains()
has
been translated to SQL EXISTS()
clause. Note that Contact.Mary
has been replaced with 1
.
Contains(subquery)
translates to similar EXISTS()
expression with numeric literals replaced
with the translated subquery. SOQL code:
select * fromGroup where Members.Contains(Contact where Name like 'Mary%')
And the resulting SQL query is:
select t0.id as [Id], t0.manager as [Manager], t0.name as [Name] from _Group t0 whereexists (select * from Contact where primary_group=t0.id and id in ( select t1.id as [ContactId] from Contact t1 where (t1.name like 'Mary%')))
8.3.2.2.Many-To-Many Collections
Operations on many-to-many collections (such as Contact.Roles
where each contact may hold many roles and
a role can be held by many persons), are translated to subqueries on the resolving table
(declared with the <relation /> element in the mapping schema (3)
).
The Count
operation gets converted to ta subquery containing the select count(*)
expression.
Consider this query:
select * from Contact where Roles.Count > 3
It translates to the following SQL:
select t0.id as [ContactId], t0.primary_group as [PrimaryGroup], t0.type as [Type], t0.name as [Name], t0.active as [Active], t0.last_salary as [LastSalary] from Contact t0 where ((select count(*) from ContactRole where contact_id=t0.id) > 3)
Translating Contains()
that operates on single objects is only slightly more complicated
than in the one-to-many case. It gets converted to an SQL EXISTS()
clause,
but it requires additional expression to join back to the "parent" table:
Given this SQOL:
select * from Contact where Roles.Contains(Role.Manager)
SOQL to SQL Converter produces:
select t0.id as [ContactId], t0.primary_group as [PrimaryGroup], t0.type as [Type], t0.name as [Name], t0.active as [Active], t0.last_salary as [LastSalary] from Contact t0 whereexists (select * from ContactRole where contact_id=t0.id and role_id in (2))
Contains()
with subquery as an argument produces the most complicated
result, still using SQL EXISTS()
clause and a subquery.
select * from Contact where Roles.Contains(Role where Name like 'Admin%')
Produces:
select t0.id as [ContactId], t0.primary_group as [PrimaryGroup], t0.type as [Type], t0.name as [Name], t0.active as [Active], t0.last_salary as [LastSalary] from Contact t0 whereexists (select * from ContactRole where contact_id=t0.id and role_id in ( select t1.id as [Id] from _Role t1 where (t1.name like 'Admin%')))
8.3.2.3.Named constants
Named constants in SOQL are simply replaced by their numeric or string values. In the following
example, named constant Contact.Mary
gets replaced with its numeric value of one.
SOQL:
select * from Contact where PrimaryGroup.Manager = Contact.Mary
SQL:
select t0.id as [ContactId], t0.primary_group as [PrimaryGroup], t0.type as [Type], t0.name as [Name], t0.active as [Active], t0.last_salary as [LastSalary] from Contact t0 left outer join _Group t1 on (t0.primary_group = t1.id) where (t1.manager = 1)
8.3.2.4.SoodaClass and Inheritance
SoodaClass
returns class name as a string. In cases where the class is known statically (when the class
is known to have no subclasses), it is emitted directly into the SQL output. When the class has
subclasses, Sooda needs to resort to runtime evaluation using SQL CASE ... WHEN ... END
construct.
For example, this SOQL code:
select SoodaClass from Contact
Is equivalent to the following SQL, because class Contact
does not have any subclasses.
select'Contact' as SoodaClass from Contact t0
On the contrary, the class Vehicle
has many subclasses, so translating a similar query
returns very different result:
select SoodaClass from Vehicle
The result of the translation is shown below. Note the CASE
expression that
returns 'Car'
when type is 1
, 'Bike'
when type is 2
,
and so on. The mapping is taken from subclassSelectorValue
attributes
defined in the mapping schema (3.9)
.
select (case t0.type when 1 then 'Car' when 2 then 'Bike' when 3 then 'SuperBike' when 4 then 'MegaSuperBike' when 5 then 'ConcreteMegaSuperBikeA' when 6 then 'ConcreteMegaSuperBikeB' when 7 then 'ExtendedBike' else null end) as SoodaClass from Vehicle t0 where t0.type in (1,2,3,4,5,6,7)
Pay attention to the extra WHERE
clause that has been added by the conversion routine. This is needed to
ensure that the result of this query will be limited to instances of Vehicle
and Bike
subclasses. The additional where clause always includes the identifiers of all concrete types that
may be returned. If we wanted to limit ourselves to the bikes, we cold have written:
select * from Bike
The result of the translation would be (note that the identifiers of possible subclasses are different from the previous case, even though the Bike class is based on the same database table):
select t0.id as [Id], t0.type as [Type], t0.name as [Name], t0_pkjoin_Bike.two_wheels as [TwoWheels] from Vehicle t0 left outer join Bike t0_pkjoin_Bike on (t0.id = t0_pkjoin_Bike.id) where t0.type in (3,4,5,6,7,2)
Please also note how additional fields from the Bike class are made available in the results. There is a special
join, called "primary key join" and a special table alias "tk_pkjoin_Bike" which gives access to two_wheels
column in the Bike
table.
8.4.Generated Code
8.4.1.Classes
Each <class /> declaration in the mapping schema (3) translates into at least five classes:
ISoodaObjectList
8.4.1.1.Stub Classes
Each stub class NNN_Stub
generated by SoodaStubGen corresponds to a <class name="NNN" /> declaration in the mapping schema (3)
.
The stub class is enclosed in the output namespace defined in the Sooda Project File (4.2)
followed by .Stubs
. It ultimately
derives from the SoodaObject
class as described
in section "8.4.2. Class hierarchy"
:
namespace OutputNamespace.Stubs { public class NNN_Stub : SoodaObject { } }
Each stub class has:
GetClassInfo()
method that returns the ClassInfo (8.5.2)
object
describing mapping metadataGetFieldHandler(int ordinal)
method that returns a field handler given for a field ordinalGetKeyGenerator()
method that returns the key generator for the classInitNewObject()
method that initializes the primary key of the newly created objects from the key generatorLoader methods:
LoadSingleObject()
method overloadsGetAllObjects()
method overloadsGetList()
method overloadsLoad()
method overloadsGetRef()
method overloadsTryGet()
method overloads
Note that these methods are located in a separate class named NNNLoader
when the <loader-class /> option is set to true
in the Sooda Project File (4.2)
.
8.4.2.Class hierarchy
Each <class /> element defined in the mapping schema (3)
is represented
in generated code by two classes: skeleton and stub. Stub classes typically inherit from the SoodaObject
class, but when Sooda class inheritance is used, the stub classes may inherit from parent skeleton classes.
In addition to this, you may "inject" your own class into the inheritance hierarchy at two levels:
extBaseClassName
attribute.
The class you inject must provide 2 constructors required by Sooda and must ultimately derive from SoodaObject
.
Typical declaration of the injectable class looks like this:
public class SoodaObjectExtensions: SoodaObject { public SoodaObjectExtensions(SoodaTransaction t): base(t){} protected SoodaObjectExtensions(SoodaConstructor c): base(c){} }
Consider a simple schema with five classes (fields and tables omitted for brevity):
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="Vehicle" subclassSelectorField="Type" subclassSelectorValue="0"> </class> <class name="Bicycle" inheritFrom="Vehicle" subclassSelectorValue="1"> </class> <class name="Car" inheritFrom="Vehicle" subclassSelectorValue="2"> </class> <class name="Boat" inheritFrom="Vehicle" subclassSelectorValue="3"> </class> <class name="Person"> </class> </schema>
Assuming the Sooda Project File sets the <base-class-name /> option:
<sooda-project xmlns="http://www.sooda.org/schemas/SoodaProject.xsd"> <base-class-name> AdditionalBaseClass </base-class-name> </sooda-project>
The generated class hierarchy is presented in the following figure.
Note how all classes ultimately derive from the SoodaObject
class
and how Bicycle_Stub
, Car_Stub
and Boat_Stub
classes inherit from Vehicle
skeleton class.
8.4.3.Properties
SoodaStubGen generates one property for each <field /> declared in the mapping schema. The
name of the property is taken from the name
attribute and the property type
depends on two factors:
If the field is nullable, SoodaStubGen uses the representation passed in <nullable-representation /> parameter. If the field is not nullable, the representation passed in <not-null-representation /> is used. Particular representations are presented in the following sections.
8.4.3.1.Boxed
Boxed
representation causes the property to have System.Object
return type
which maps to object
keyword in C#, so the property declaration looks like this:
class NNN_Stub { // Boxed object representation, note that there is // no static type checking public object Field1 { get { ... } set { ... } } }
8.4.3.2.SqlTypes
SqlTypes
representation causes the property to have the type that is a structure from the
System.Data.SqlTypes
namespace:
System.Data.SqlTypes.SqlInt32
- for the Integer
data typeSystem.Data.SqlTypes.SqlInt64
- for the Long
data typeSystem.Data.SqlTypes.SqlString
- for the String
and AnsiString
data typesSystem.Data.SqlTypes.SqlBoolean
- for Boolean
and BooleanAsInteger
data typesSystem.Data.SqlTypes.SqlDateTime
- for the DateTime
data typeSystem.Data.SqlTypes.SqlDecimal
- for the Decimal
data typeSystem.Data.SqlTypes.SqlSingle
- for the Float
data typeSystem.Data.SqlTypes.SqlDouble
- for the Double
data typeSystem.Data.SqlTypes.SqlBinary
- for the Blob
data typeSystem.Data.SqlTypes.SqlGuid
- for the Guid
data typeExample:
class NNN_Stub { // SqlTypes object representation public System.Data.SqlTypes.SqlInt32 Field1 { get { ... } set { ... } } }
8.4.3.3.Raw
Raw
property representation does not care about null values at all. The property type is simply
the raw type of the field:
System.Int32
- for the Integer
data typeSystem.Int64
- for the Long
data typeSystem.String
- for the String
and AnsiString
data typesSystem.Boolean
- for Boolean
and BooleanAsInteger
data typesSystem.DateTime
- for the DateTime
data typeSystem.Decimal
- for the Decimal
data typeSystem.Single
- for the Float
data typeSystem.Double
- for the Double
data typebyte[]
- for the Blob
data typeSystem.Drawing.Image
- for the Image
data typeSystem.Guid
- for the Guid
data typeExample:
class NNN_Stub { // Raw object representation public System.Int32 Field1 { get { ... } set { ... } } }
8.4.3.4.RawWithIsNull
RawWithIsNull
generates a pair of properties: P
and P_IsNull
,
the latter being read-only returning true
when the field value is null
. In addition to this
the P_SetNull()
method is generated that can be used to set the field value to null.
System.Int32
- for the Integer
data typeSystem.Int64
- for the Long
data typeSystem.String
- for the String
and AnsiString
data typesSystem.Boolean
- for Boolean
and BooleanAsInteger
data typesSystem.DateTime
- for the DateTime
data typeSystem.Decimal
- for the Decimal
data typeSystem.Single
- for the Float
data typeSystem.Double
- for the Double
data typebyte[]
- for the Blob
data typeSystem.Drawing.Image
- for the Image
data typeSystem.Guid
- for the Guid
data typeExample:
class NNN_Stub { // RawWithIsNull object representation public System.Int32 Field1 { get { ... } set { ... } } public bool Field1_IsNull { get { ... } } public void Field1_SetNull() { ... } }
8.4.3.5.Nullable
Nullable
property uses System.Nullable<T>
generic type available in .NET 2.0.
The actual type of the property depends on the field data type:
Nullable<Int32>
- for the Integer
data typeNullable<Int64>
- for the Long
data typeNullable<Boolean>
- for Boolean
and BooleanAsInteger
data typesNullable<DateTime>
- for the DateTime
data typeNullable<Decimal>
- for the Decimal
data typeNullable<Single>
- for the Float
data typeNullable<Double>
- for the Double
data typeNullable<Guid>
- for the Guid
data type
Note that String
, AnsiString
, Blob
and Image
types are handled in a special way, because nullable
types cannot be used to "wrap" reference types):
string
- for the String
and AnsiString
data typesbyte[]
- for the Blob
data typeSystem.Drawing.Image
- for the Image
data typeExample:
class NNN_Stub { // Nullable object representation public Nullable<System.Int32> Field1 { get { ... } set { ... } } }
8.4.3.6.Reference fields
For reference fields, the property return type is the class that is referred. Null reference is
passed as null
.
class NNN_Stub { public ReferencedClass RefField1 { get { ... } set { ... } } }
8.4.4.Collections
Each collection (both one-to-many and many-to-many) is represented as a read-only property whose type is
MMMList
where MMM
is the type of the collection element. The MMMList
is a
list wrapper described
in section "8.4.7. List wrappers"
.
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="NNN"> <collectionOneToMany name="Members" class="MMM" foreignField="..."/> </class> </schema>
The following code is generated from the above declaration:
class NNN_Stub { public MMMList Members { get { ... } } }
8.4.5.Triggers
Sooda generates two methods for each <field />, one called before and the other called after the field is modified is modified. Assuming the following schema:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="NNN"> <table name="Table1"> <field name="Field1" type="Integer"/> </table> </class> </schema>
The trigger method signatures are:
class NNN_Stub { protected virtual void BeforeFieldUpdate_Field1(object oldValue, object newValue) { } protected virtual void AfterFieldUpdate_Field1(object oldValue, object newValue) { } }
oldValue
and newValue
passed to these methods are old and new values of the field
being set, respectively. The values are boxed to represent the null
. For reference fields, the type
of objValue
and newValue
arguments is the referenced class itself.
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="NNN"> <table name="Table1"> <field name="RefField1" type="Integer" references="ReferencedClass"/> </table> </class> </schema>
This schema causes the following pair of trigger methods to be generated:
class NNN_Stub { protected virtual void BeforeFieldUpdate_RefField1(ReferencedClass oldValue, ReferencedClass newValue) { } protected virtual void AfterFieldUpdate_RefField1(ReferencedClass oldValue, ReferencedClass newValue) { } }
The methods are declared protected virtual
so that you can override them in your skeleton classes
but you cannot call them from outside of the class.
8.4.6.Constants/Enumerations
Constants declared with <const /> are generated as public static properties of the stub class:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="NNN"> <const name="MyConstant1" key="1"/> </class> </schema>
The above declaration translates to:
class NNN_Stub { public static NNN MyConstant1 { get { ... } } }
8.4.7.List wrappers
List wrappers are type-safe wrappers around ISoodaObjectList
that can be used in all places
where the interface is expected. They provide typed Add()
, Remove()
and Contains()
methods and a number of convenience methods. The following wrapper is generated for each class NNN:
public class NNNList : Sooda.ObjectMapper.SoodaObjectCollectionWrapperGeneric<NNN> { public NNNList() { } public NNNList(ISoodaObjectList list) : base(list) { } public new NNN this[int pos] { get { return ((NNN)(base.GetItem(pos))); } } public int Add(NNN obj) { return base.Add2(obj); } public void Remove(NNN obj) { base.Remove2(obj); } public bool Contains(NNN obj) { return base.Contains2(obj); } public NNNList Sort(IComparer comparer) { return new NNNList(base.Sort2(comparer)); } public NNNList Sort(string sortOrder) { return new NNNList(base.Sort2(sortOrder)); } public NNNList Sort(Sooda.QL.SoqlExpression sortExpression) { return new NNNList(base.Sort2(sortExpression)); } public NNNList Sort(Sooda.QL.SoqlExpression sortExpression, Sooda.SortOrder sortOrder) { return new NNNList(base.Sort2(sortExpression, sortOrder)); } public NNNList SelectFirst(int count) { return new NNNList(base.SelectFirst2(count)); } public NNNList SelectLast(int count) { return new NNNList(base.SelectLast2(count)); } public NNNList SelectRange(int _from, int _to) { return new NNNList(base.SelectRange2(_from, _to)); } public NNNList Filter(SoodaObjectFilter f) { return new NNNList(base.Filter2(f)); } public NNNList Filter(Sooda.QL.SoqlBooleanExpression sortExpression) { return new NNNList(base.Filter2(sortExpression)); } public NNNList Filter(SoodaWhereClause whereClause) { return new NNNList(base.Filter2(whereClause)); } public NNNList GetSnapshot() { return new NNNList(base.GetSnapshot2()); } }
The class that NNNList
inherits from
is Sooda.ObjectMapper.SoodaObjectCollectionWrapperGeneric<NNN>
for .NET 2.0 and above
and Sooda.ObjectMapper.SoodaObjectCollectionWrapper
for .NET 1.x.
8.4.8.Find Methods
The FindByNNN
and FindListByNNN
methods are used to quickly locate one or all objects such that
their NNN
is equal to the provided value. The finders are generated as public static methods of
stub classes:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="NNN"> <table name="Table1"> <field name="Field1" type="Integer" find="true"/> <field name="Field2" type="String" findList="true"/> </table> </class> </schema>
class NNN_Stub { public static void NNN FindByField1(Int32 field1) { } public static void NNNList FindByField2(String field2) { } }
8.4.9.Abstract Factories
Each Sooda class has an associated abstract factory that can create instances of the class and
return mapping metadata. The abstract factory is a class that
implements ISoodaObjectFactory
. The following factory code is generated for
each class NNN
:
namespace OutputNamespace.Stubs { [SoodaObjectFactoryAttribute("NNN", typeof(NNN))] public class NNN_Factory : ISoodaObjectFactory { public NNN_Factory() { } public static NNN_Factory TheFactory { get { ... } } public virtual Type TheType { get { ... } } public static ClassInfo TheClassInfo { get { ... } } public virtual SoodaObject GetRef(SoodaTransaction tran, object keyValue) { } public virtual SoodaObject TryGet(SoodaTransaction tran, object keyValue) { } public virtual IList GetList(SoodaTransaction tran, SoodaWhereClause whereClause, SoodaOrderBy orderBy, SoodaSnapshotOptions options) { } public virtual ClassInfo GetClassInfo() { } public virtual SoodaFieldHandler GetPrimaryKeyFieldHandler() { } public virtual SoodaFieldHandler GetFieldHandler(int ordinal) { } internal static SoodaFieldHandler InternalGetFieldHandler(int ordinal) { } public virtual SoodaObject CreateNew(SoodaTransaction tran) { } public virtual SoodaObject GetRawObject(SoodaTransaction tran) { } } }
Thanks to factory classes, Sooda can create instances of materialized objects without using reflection, which helps speed things up.
8.4.10.Schema
SoodaStubGen generates the _DatabaseSchema
schema that provides access to runtime
mapping metadata and instances of all class factories for the schema. The declaration of the class is:
public class _DatabaseSchema : ISoodaSchema { public _DatabaseSchema() { } public virtual Sooda.Schema.SchemaInfo Schema { get { ... } } public static Sooda.Schema.SchemaInfo GetSchema() { } public virtual Sooda.ISoodaObjectFactory[] GetFactories() { } }
8.4.11.Typed Queries
There are two sets of classes that support typed queries: public classes and supporting classes. Public
classes are named OutputNamespace.TypedQueries.NNNField
where NNN
is the name
of the appropriate class. Supporting classes are located in the OutputNamespace.Stubs
namespace
and are named: NNNWrapperExpression
and NNNNullableWrapperExpression
:
namespace OutputNamespace.TypedQueries { public class NNNField { } } namespace OutputNamespace.Stubs { public class NNNWrapperExpression : Sooda.QL.SoqlPathExpression { } public class NNNNullableWrapperExpression : NNNWrapperExpression { } }
Each NNNField
8.5.Schema API
Sooda provides runtime access to the mapping schema (3) through a set of metadata classes. Each class represents a single mapping element. The following classes are available:
SchemaInfo
(8.5.1)
- represents the <schema /> root element and provides access to classes,
relations, data sources, precommit values and included filesClassInfo
(8.5.2)
- represents the <class /> schema elementRelationInfo
(8.5.3)
- represents the <relation /> schema elementTableInfo
(8.5.4)
- represents the <table /> schema elementFieldInfo
(8.5.5)
- represents the <field /> schema elementIncludeInfo
(8.5.6)
- represents the <include /> schema elementConstantInfo
(8.5.7)
- represents the <const /> schema elementDataSourceInfo
(8.5.8)
- represents the <datasource /> schema elementPrecommitValueInfo
(8.5.9)
- represents the <precommitValue /> schema elementCollectionOneToManyInfo
(8.5.10)
- represents the <collectionOneToMany /> schema elementCollectionManyToManyInfo
(8.5.11)
- represents the <collectionManyToMany /> schema element8.5.1.SchemaInfo class
Sooda.Schema.SchemaInfo
class represents the <schema /> element of the mapping schema (3)
.
You typically acquire a reference to the SchemaInfo
object that matches the compiled
stubs assembly by calling _DatabaseSchema.GetSchema()
method as in the following example.
The _DatabaseSchema
class is generated by the SoodaStubGen utility (8.1.1)
and
so the namespace depends on your code generation settings.
using Sooda; using SoodaSchema; class Program { static void Main() { SchemaInfo schema = MyBusinessObjects._DatabaseSchema.GetSchema(); foreach (ClassInfo cl in schema.Classes) { Console.WriteLine("class: {0}", cl.Name); } }
SchemaInfo
class has following properties:
Namespace
- schema namespaceAssemblyName
- schema assembly nameIncludes
- collection of IncludeInfo
(8.5.6)
objects representing included schema.DataSources
- collection of DataSourceInfo
(8.5.8)
objects representing defined data sources.Classes
- collection of ClassInfo
(8.5.2)
objects representing defined classes.Relations
- collection of RelationInfo
(8.5.3)
objects representing defined relations.DefaultPrecommitValues
- determines per-schema precommit strategy, explained
in section "3.10. Precommit Values"
.PrecommitValues
- collection of PrecommitValueInfo
(8.5.9)
objects representing defined precommit values.LocalClasses
- same as Classes
but is limited to classes defined in the schema (excluding those defined in included schema files)LocalRelations
- same as Relations
but is limited to relations defined in the schema (excluding those defined in included schema files)SchemaInfo
also exposes some public methods, but they are not meant to be used in user code. The methods must be public
so they can be called from generated code.
8.5.2.ClassInfo class
Sooda.Schema.ClassInfo
class represents the <class /> element of
the mapping schema (3)
. It can be used to retrieve class-level mapping metadata.
Objects of this class are stored in SchemaInfo.Classes
and SchemaInfo.LocalClasses
collections.
ClassInfo
has the following public properties:
Name
- class nameExtBaseClassName
- Specifies the name of the base class that this class should inherit from. DataSourceName
- name of the data sourceLocalTables
- collection of TableInfo
(8.5.4)
objects representing local tables of this class (excluding any tables defined in parent classes in the inheritance hierarchy)UnifiedTables
- collection of TableInfo
(8.5.4)
objects representing all tables that this class is based on (including tables defined in parent classes in the inheritance hierarchy)DatabaseTables
- collection of TableInfo
(8.5.4)
objects representing database tables of this class ignoring any vertical partitions (so two <table /> declarations
based on a single physical table are merged together. This structure is used to properly INSERT
objects
into the database.
LocalFields
- collection of FieldInfo
(8.5.5)
objects defined in all tables defined in this class (primary key fields are not duplicated)UnifiedFields
- collection of FieldInfo
(8.5.5)
objects defined in all tables defined in this class and all parent classes in the inheritance hierarchy (primary key fields are not duplicated)OrderedFieldNames
- array of ordered field names (as in UnifiedFields
)Collections1ToN
- one-to-many collections defined in this classCollectionsNToN
- many-to-many collections defined in this classLocalCollections
- all collections (one-to-many and many-to-many) defined in this classUnifiedCollections
- all collections (one-to-many and many-to-many) defined in this class and all parent classes in the inheritance hierarchyConstants
- collection of ConstantInfo
(8.5.7)
that define named constants for this classDefaultPrecommitValue
- default precommit value to be used for reference fields that refer to this classCached
- caching hint. Actual interpretation depends on the active caching policy (6.11)
.CacheCollections
- caching hint. Actual interpretation depends on the active caching policy (6.11)
.Cardinality
- caching hint. Actual interpretation depends on the active caching policy (6.11)
.Triggers
- determines whether per-field triggers are generated for this class.ReadOnly
- read-only class (no property setters are generated)LabelField
- name of the label field for this classSubclassSelectorFieldName
- name of the subclass selector field (propagated from parent classes if necessary)SubclassSelectorField
- a FieldInfo
(8.5.5)
object that represents the subclass selector fieldSubclassSelectorStringValue
- string value of the subclass selector fieldSubclassSelectorValue
- typed value of the subclass selector field (string or integer)InheritFrom
- name of the base Sooda class in the inheritance hierarchyInheritsFromClass
- a ClassInfo
(8.5.2)
object that represents the base class in the inheritance hierarchyKeyGenName
- name of the key generator that provides keys for this classOuterReferences
- collection of FieldInfo
(8.5.5)
objects that are references to this classDisableTypeCache
- whether type cache is disabled for this class
In addition to the properties, ClassInfo
exposes the following public methods:
FindCollectionOneToMany(name)
- finds the specified one-to-many collection in this class or any
ancestors in the inheritance hierarchyFindCollectionManyToMany(name)
- finds the specified many-to-many collection in this class or any
ancestors in the inheritance hierarchyContainsCollection(name)
- determines whether the specified collection is defined in this class
or any ancestor classGetPrimaryKeyFields()
- returns an array of FieldInfo
(8.5.5)
objects which are marked as primary keysGetFirstPrimaryKey()
- returns the FieldInfo
(8.5.5)
object for the first primary key fieldGetSubclassesForSchema(schema)
- returns the collection of ClassInfo
(8.5.2)
objects that are subclasses of this class in the specified schema.
Note that the list of subclasses may be different depending on the schema you use if you define a root class in one
schema file (3)
and classes inheriting from it in another one.Let's assume that you have defined the following classes in the mapping schema (3) :
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="A"> <table name="TA"> <field name="Id" type="Integer" primaryKey="true"/> <field name="F1" type="Integer"/> </table> <table name="TA"> <field name="Id" type="Integer" primaryKey="true"/> <field name="F2" type="Integer"/> </table> </class> <class name="B" inheritFrom="A"> <table name="TA"> <field name="Id" type="Integer" primaryKey="true"/> <field name="F3" type="Integer"/> </table> <table name="TB"> <field name="Id" type="Integer" primaryKey="true"/> <field name="F4" type="Integer"/> </table> <table name="TB"> <field name="Id" type="Integer" primaryKey="true"/> <field name="F5" type="Integer"/> </table> </class> </schema>
The LocalTables
collection of class A
contains two items, because we have defined two
<table /> elements inside the <class name="A" /> element. The DatabaseTables
of
this class contains one item, which is a table TA
containing the
Id
, F1
and F2
, because both <table />s that define them are
based on the same database table. The UnifiedTables
contents are the same as LocalTables
because the class does not inherit from any other class.
The LocalTables
collection of class B
contains three items, because this is the number of
<table /> elements defined within <class name="B" />. There are two items in the
DatabaseTables
collection, because there are two physical database tables:
TA
and TB
. The UnifiedTables
of class B
contains
five objects of class TableInfo
, one for each defined table in class B
and
its parent class A
.
8.5.3.RelationInfo class
Sooda.Schema.RelationInfo
class represents the <relation /> element of
the mapping schema (3)
. It can be used to retrieve relation-level mapping metadata.
Objects of this class are stored in SchemaInfo.Relations
and SchemaInfo.LocalRelations
collections.
RelationInfo
has the following public properties:
Name
- relation nameDataSourceName
- data source name for this relationTable
- the table that holds the fields referencing objects in relation8.5.4.TableInfo class
Sooda.Schema.TableInfo
class represents the <table /> element of
the mapping schema (3)
. It can be used to retrieve table-level mapping metadata.
Objects of this class are stored in ClassInfo.LocalTables
and ClassInfo.UnifiedTables
collections.
RelationInfo
maintains a reference to the underlying TableInfo
as well.
TableInfo
has the following public properties:
DBTableName
- name of the database tableFields
- collection of FieldInfo
(8.5.5)
objects that
represent <field /> elements defined for this tableOrdinalInClass
- the ordinal of this TableInfo
object in the UnifiedTables
collection of the parent classNameToken
- is a unique table name (auto-generated)OwnerClass
- reference to the ClassInfo
(8.5.2)
object that owns this table (null
for table in releation)8.5.5.FieldInfo class
Sooda.Schema.FieldInfo
class represents the <field /> element of
the mapping schema (3)
. It can be used to retrieve field-level mapping metadata.
FieldInfo
class has the following public properties:
Name
- field nameDBColumnName
- name of the database column (defaults to Name
it not specified).Type
- field typeSize
- field sizePrecision
- field precisionReferences
- name of the referenced classPrecommitValue
- precommit value stringPrimaryKey
- true
if the field is (part of) the primary keyPrecommitTypedValue
- typed precommit value (integer, string, datetime)Nullable
- true
if the field is nullableReadOnly
- true
if the field is read-onlyForceTrigger
- true
if the triggers for this field have been generated, regardless of
a value of the ClassInfo.Triggers
for the parent <class />.DeleteAction
- specifies the action that should be taken when referenced object is deleted. Equivalent of the onDelete
attribute.PrefetchLevel
- The value of prefetch
attribute for this field.FindMethod
- whether to generate FindByNNN()
method.FindListMethod
- whether to generate FindListByNNN()
method.OrdinalInTable
- ordinal of the field in enclosing <table />ClassLocalOrdinal
- ordinal of the field in the LocalFields
collection of the enclosing classClassUnifiedOrdinal
- ordinal of the field in the UnifiedTables
collection of the enclosing classTable
- enclosing <table />ParentClass
- enclosing <class /> - null if field defined in a relationParentRelation
- enclosing <relation /> - null if field defined in a class8.5.6.IncludeInfo class
Sooda.Schema.IncludeInfo
class represents the <include /> element of
the mapping schema (3)
. It can be used to retrieve information about included schemas.
IncludeInfo
class has the following public properties:
SchemaFile
- name of the schema fileAssemblyName
- name of the DAL assemblyNamespace
- namespace of the generated DAL8.5.7.ConstantInfo class
Sooda.Schema.ConstantInfo
class represents the <const /> element of
the mapping schema (3)
. It can be used to retrieve information about defined named constants.
ConstantInfo
class has the following public properties:
Name
- name of the constantKey
- key value of the object that should be associated with
the constant. Compount primary keys are not supported.8.5.8.DataSourceInfo class
Sooda.Schema.DataSourceInfo
class represents the <datasource /> element of
the mapping schema (3)
. It can be used to retrieve information about defined data sources.
DataSourceInfo
class has the following public properties:
Name
- name of the data sourceType
- type of the data source - name of the class that inherits from SoodaDataSource
8.5.9.PrecommitValueInfo class
Sooda.Schema.PrecommitValueInfo
class represents the <precommitValue /> element of
the mapping schema (3)
. It can be used to retrieve information about
defined precommit values for each data type.
PrecommitValueInfo
class has the following public properties:
DataName
- data type (Integer, String, DateTime and so on)ValueText
- textual precommit valueValue
- typed precommit value8.5.10.CollectionOneToManyInfo class
Sooda.Schema.CollectionOneToManyInfo
class represents the <collectionOneToMany /> element of
the mapping schema (3)
. It can be used to retrieve information about
defined one-to-many collections of a class:
CollectionOneToManyInfo
class has the following public properties:
Name
- collection nameClassName
- collection element type nameClass
- collection element ClassInfo
(8.5.2)
.ForeignFieldName
- name of the field in Class
that references at the object that defines the collectionCache
- caching hint8.5.11.CollectionManyToManyInfo class
Sooda.Schema.CollectionManyToManyInfo
class represents the <collectionManyToMany /> element of
the mapping schema (3)
. It can be used to retrieve information about
defined many-to-many collections of a class:
CollectionManyToManyInfo
class has the following public properties:
Name
- collection nameRelation
- name of the relation that resolves many-to-many relationshipForeignField
- name of the field in Relation
that references at the object that defines the collection8.6.Factory Cache
Factory cache is a mechanism that is meant to optimize the GetRef()
operation for classes
that are part of inheritance relationships. For all other cases GetRef()
can be very cheap,
constant-time operation, but if a class has subclasses, Sooda needs to issue a database query to
determine the concrete class that should be instantiated. Once the query has been executed and the concrete
class is known, we can cache it so that further GetRef()s
can be done without any database queries.
By default Sooda comes with eternal factory cache that stores the primary key to concrete class association in
a hashtable that is never emptied. This is sufficient for most databases with less than a few million records. For
larger databases you should implement your own factory cache by creating a class that implements the ISoodaObjectFactoryCache
interface.
Once you have defined your own factory cache, you should assign it to the static
property SoodaObjectFactoryCache
of the SoodaTransaction
class:
SoodaTransaction.SoodaObjectFactoryCache = new MyFactoryCache();
8.7.ActiveTransaction management
Implicit transaction (stored in SoodaTransaction.ActiveTransaction
) is kept in
thread's local storage (TLS). It means that as long as you access the objects from the thread
that created the transaction, you do not need to pass the transaction reference explicitly.
There are situations where storing the transaction on per-thread basis is not feasible. For example
events in the ASP.NET environment are not guaranteed to be thread-bound so you cannot store
implicit SoodaTransaction
in TLS.
Fortunately, Sooda supports multiple strategies for managing the implicit transaction reference. There are three predefined strategies:
SoodaThreadBoundTransactionStrategy
- default thread-bound implementationSoodaHttpContextBoundTransactionStrategy
- ASP.NET HttpContext-bound implementationSoodaProcessBoundTransactionStrategy
- process-bound implementation
To change the active strategy, create an instance of the appropriate strategy class and assign
it to the SoodaTransaction.DefaultTransactionStrategy
static property:
SoodaTransaction.DefaultTransactionStrategy = new SoodaHttpContextBoundTransactionStrategy();
To implement your own strategy, create a class that implements the
IDefaultSoodaTransactionStrategy
interface. Two methods need to be implemented: one that
gets current transaction and the other that sets it and returns a previous value.
8.8.Prefetching
Prefetching is a technique that allows object references to be pre-loaded from the database
at the same time the object is loaded. This is possible thanks to the SQL OUTER JOIN
operators. Normally Sooda issues a simple SQL statement to load an object from the database::
select column1, column2, ..., columnN from table1 where id=123
Assuming that columnR
is a foreign key reference to table2
, we can load two
objects in a single query by using an outer join:
select t1.column1, t1.column2, ..., t1.columnN t2.column1, t2.column2, ..., t2.columnM from table1 t1 left outer join table2 t2 on (t1.columnR = t2.id) where t1.id=123
Note that columns 1..N belong to objects stored in "table1
" and
columns N+1..N+M are from "table2
".
Sooda extends this concept with the possibility to include arbitrary number of objects being prefetched
on each load. This can be controlled by the prefetch
attribute of the <field /> element in the
mapping schema (3)
. By default it is zero, meaning that no prefetch should be performed,
but you can set it to a higher value to indicate level of objects that should be prefetched.
Consider this example of a simple movie management system:
<schema xmlns="http://www.sooda.org/schemas/SoodaSchema.xsd"> <class name="Movie"> <table name="Movie"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Title" type="String" size="40"/> <field name="Genre" type="Integer" references="MovieGenre" prefetch="1"/> <field name="Subgenre" type="Integer" references="MovieGenre" prefetch="2"/> <field name="AudioLanguage" type="Integer" references="AudioLanguage"/> <field name="VideoFormat" type="Integer" references="VideoFormat" prefetch="1"/> <field name="ParentalGuidance" type="Integer" references="ParentalRating"/> </table> </class> <class name="MovieGenre"> <table name="MovieGenre"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="ParentGenre" type="Integer" references="MovieGenre" prefetch="1"/> </table> </class> <class name="VideoFormat"> <table name="VideoFormat"> <field name="ID" type="Integer" primaryKey="true"/> <field name="FrameWidth" type="Integer"/> <field name="FrameHeight" type="Integer"/> <field name="ColorDepth" type="Integer"/> <field name="Compression" type="Integer"/> </table> </class> <class name="ParentalRating"> <table name="ParentalRating"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="MinimumAge" type="Integer"/> </table> </class> <class name="AudioLanguage"> <table name="AudioLanguage"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="ISOTwoLetterName" type="String" size="40"/> <field name="ISOThreeLetterName" type="String" size="40"/> </table> </class> </schema>
When you load a movie object m
Sooda automatically prefetches related objects, so that
the following properties can be accessed with no additional SQL queries:
m.ID m.Title m.Genre m.Subgenre m.AudioLanguage m.VideoFormat m.ParentalGuidance // these fields are prefetched because field Genre has prefetch="1" m.Genre.Name // these fields are prefetched because field Subgenre has prefetch="2" m.Subgenre.Name m.Subgenre.ParentGenre.ID m.Subgenre.ParentGenre.Name // these fields are prefetched because field VideoFormat has prefetch="1" m.VideoFormat.FrameWidth m.VideoFormat.FrameHeight m.VideoFormat.ColorDepth m.VideoFormat.Compression
Note that the following fields are NOT prefetched automatically and will involve a database access when first accessed:
m.AudioLanguage.Name m.AudioLanguage.ISOTwoLetterName m.AudioLanguage.ISOThreeLetterName m.ParentalGuidance.Name m.ParentalGuidance.MinimumAge
Because primary keys are always present in memory, accessing following fields will never
cause a database roundtrip, even if the prefetch
properties were not set:
m.Genre.ID m.Subgenre.ID m.VideoFormat.ID m.AudioLanguage.ID m.ParentalGuidance.ID
Prefetch can be used to optimize preloading of hierarchies, such as Employee-Manager relationship which is effectively
a tree. Let's assume you have a class named Employee
which includes a reference to its manager:
<class name="Employee"> <table name="Employee"> <field name="ID" type="Integer" primaryKey="true"/> <field name="Name" type="String" size="40"/> <field name="Salary" type="Decimal"/> <field name="HireDate" type="DateTime"/> <field name="Manager" type="Integer" reference="Employee"/> <field name="Active" type="BooleanAsInteger"/> </table> </class>
We can print all the managers of some particular employee with a simple for()
loop:
for (Employee e = this.Manager; e != null; e = e.Manager) { Console.WriteLine("manager: {0}", e.Name); }
Without the prefetch, Sooda would have to issue the SQL query to retrieve each Employee object separately.
Assuming you have 10 levels of management in your company, that would be 10 SQL queries. To minimize the number of queries, you can add prefetch="3"
to the "Manager
" field declaration:
<field name="Manager" type="Integer" reference="Employee" prefetch="3"/>
When you load an Employee
object e
, Sooda preloads the following referenced objects, so you can access
their properties without an SQL query:
e.Manager e.Manager.Manager e.Manager.Manager.Manager
Once you access e.Manager.Manager.Manager.Manager
, Sooda will preload:
e.Manager.Manager.Manager.Manager.Manager e.Manager.Manager.Manager.Manager.Manager.Manager e.Manager.Manager.Manager.Manager.Manager.Manager.Manager
If there are at most three managers in the hierarchy, setting the prefetch
to 3
ensures that
the entire structure will be fetched in a single SQL query.
The cost of prefetch is usually very small, but the number of columns returned in a query grows as you
prefetch, so you should limit the depth of the prefetch operation. Prefetch value of one is usually sufficient.