Mercurial > pub > bltoolkit
diff Tools/DocGen/Content/Doc/DataAccess/Introduction.htm @ 0:f990fcb411a9
Копия текущей версии из github
author | cin |
---|---|
date | Thu, 27 Mar 2014 21:46:09 +0400 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Tools/DocGen/Content/Doc/DataAccess/Introduction.htm Thu Mar 27 21:46:09 2014 +0400 @@ -0,0 +1,629 @@ +<% title # Introduction to abstract data accessors %> +<% order # 1 %> +<h4>Introduction</h4> + +<p class='j'> +Before we start talking about the data accessors, let us create a few examples of +typical data accessor methods. +</p> +<p class='j'> +The following table contains three stored procedures and +three data access methods implementing the stored procedure calls. +</p> + +<table width='100%'> +<tr><th>Stored procedure</th><th>Data access method</th></tr> +<tr><td colspan=2 style='padding-top:7px'> +The first stored procedure takes filter and page parameters and returns recordset from the Person table. +</td></tr> +<tr> +<td width=50% height=100%><% sql # +CREATE Procedure GetPersonListByName( + @firstName varchar(50), + @lastName varchar(50), + @pageNumber int, + @pageSize int) +AS + -- stored procedure implementation + -- +%></td> +<td width=50%><% cs # +public List<Person> GetPersonListByName( + string firstName, + string lastName, + int pageNumber, + int pageSize) +{ + // method implementation. +} +%></td> +</tr> +<tr><td colspan=2> +Second example will return single <b>Person</b> record by <b>id</b>. +</td></tr> +<tr> +<td width=50% height=100%><% sql # +CREATE Procedure GetPersonByID(@id int) +AS + -- stored procedure implementation + -- +%> +</td> +<td width=50%><% cs # +public Person GetPersonByID(int id) +{ + // method implementation. +} +%></td> +</tr> +<tr><td colspan=2> +The last example will delete a record from the database by <b>id</b>. +</td></tr> +<tr> +<td width=50% height=100%><% sql # +CREATE Procedure DeletePersonByID(@id int) +AS + -- stored procedure implementation + -- +%></td> +<td width=50%><% cs # +public void DeletePersonByID(int id) +{ + // method implementation. +} +%></td> +</tr> +</table> + +<p class='j'>Now lets see what we can say if we compare the stored procedure and C# method signatures. +<ol> +<li>Stored procedure and method names match up.</li> +<li>Sequential order, method parameter types and names correspond to stored procedure parameters.</li> +<li>Methods' return values can give us an idea what <b>Execute</b> method we +should utilize and what object type has to be used to map data from recordset if needed.</li> +</ol> +</p> + +<p class='j'> +As demonstrated above method definition contains all the information we need +to implement the method body. Actually, by defining the method signatures, we +completed the most intelligent part of data accessor development. The rest of +work is definitely a monkey's job. Honestly, I got bored of being just a coding +machine writing the same data access code over and over again, especially +understanding that this process can be automated. +</p> +<p class='j'> +This introduction shows how to avoid the implementation step of data access +development and how to reduce this routine process to the method declaration. +</p> + +<h4>Abstract classes</h4> + +<p class='j'> +Unfortunately, mainstream .NET languages still do not have a compile-time +transformation system like some functional or <a href="http://nemerle.org/Macros">hybrid</a> languages do. +All we have today is pre-compile- and run-time code generation. +</p> +<p class='j'> +This introduction concentrates on run-time code generation and its support by +<a href="http://www.bltoolkit.net/">Business Logic Toolkit for .NET</a>. +</p> +<p class='j'> +Let us step back and bring the methods from the previous examples together in one class. +Ideally, this data accessor class could look like the following: +</p> +<% cs # +using System; +using System.Collections.Generic; + +public class PersonAccessor +{ + public List<Person> GetPersonListByName( + string firstName, string lastName, int pageNumber, int pageSize); + + public Person GetPersonByID (int id); + public void DeletePersonByID(int id); +} +%> + +<p class='j'> +The bad news about this sample is that we cannot use such syntax as the +compiler expects the method's body implementation. +</p> +<p class='j'> +The good news is we can use abstract classes and methods that give us quite +similar, compilable source code. +</p> +<% cs # +using System; +using System.Collections.Generic; + +public /*[a]*/abstract/*[/a]*/ class PersonAccessor +{ + public /*[a]*/abstract/*[/a]*/ List<Person> GetPersonListByName( + string firstName, string lastName, int pageNumber, int pageSize); + + public /*[a]*/abstract/*[/a]*/ Person GetPersonByID (int id); + public /*[a]*/abstract/*[/a]*/ void DeletePersonByID(int id); +} +%> + +<p class='j'>This code is 100% valid and our next step is to make it workable.</p> + +<H4>Abstract DataAccessor</H4> + +<p class='j'> +Business Logic Toolkit provides the <b>DataAccessor</b> class, which is +used as a base class to develop data accessor classes. If we add +<b>DataAccessor</b> to our previous example, it will look like the following: +</p> +<% cs # +using System; +using System.Collections.Generic; + +public abstract class PersonAccessor : /*[a]*/DataAccessor/*[/a]*/ +{ + public abstract List<Person> GetPersonListByName( + string firstName, string lastName, int pageNumber, int pageSize); + + public abstract Person GetPersonByID (int id); + public abstract void DeletePersonByID(int id); +} +%> + +<p class='j'>That's it! Now this class is complete and fully functional. The code below shows how to use it:</p> + +<% cs # +using System; +using System.Collections.Generic; + +using BLToolkit.Reflection; + +namespace DataAccess +{ + class Program + { + static void Main(string[] args) + { + PersonAccessor pa = /*[a]*/TypeAccessor/*[/a]*/<PersonAccessor>./*[a]*/CreateInstance/*[/a]*/(); + + List<Person> list = pa.GetPersonListByName("Crazy", "Frog", 0, 20); + + foreach (Person p in list) + Console.Write("{0} {1}", p.FirstName, p.LastName); + } + } +} +%> + +<p class='j'> +The only magic here is the <b>TypeAccessor.CreateInstance</b> method. First of all this +method creates a new class inherited from the <b>PersonAccessor</b> class and +then generates abstract method bodies depending on each method declaration. +If we wrote those methods manually, we could get something like the following: +</p> +<% cs # +using System; +using System.Collections.Generic; + +using BLToolkit.Data; + +namespace Example.BLToolkitExtension +{ + public sealed class PersonAccessor : Example.PersonAccessor + { + public override List<Person> GetPersonListByName( + string firstName, + string lastName, + int pageNumber, + int pageSize) + { + using (DbManager db = GetDbManager()) + { + return db + .SetSpCommand("GetPersonListByName", + db.Parameter("@firstName", firstName), + db.Parameter("@lastName", lastName), + db.Parameter("@pageNumber", pageNumber), + db.Parameter("@pageSize", pageSize)) + .ExecuteList<Person>(); + } + } + + public override Person GetPersonByID(int id) + { + using (DbManager db = GetDbManager()) + { + return db + .SetSpCommand("GetPersonByID", db.Parameter("@id", id)) + .ExecuteObject<Person>(); + } + } + + public override void DeletePersonByID(int id) + { + using (DbManager db = GetDbManager()) + { + db + .SetSpCommand("DeletePersonByID", db.Parameter("@id", id)) + .ExecuteNonQuery(); + } + } + } +} +%> + +<p class='j'> +(The <a href='..\Data\index.htm'>DbManager</a> class is another BLToolkit class used for 'low-level' database access). +</p> + +<p class="j"> +Every part of the method declaration is important. +Method's return value specifies one of the Execute methods in the following way: +<table class='data'> +<tr><th>Return Type</th><th>Execute Method</th></tr> +<tr><td><i>IDataReader</i> interface</td><td>ExecuteReader</td></tr> +<tr><td>Subclass of <i>DataSet</i></td><td>ExecuteDataSet</td></tr> +<tr><td>Subclass of <i>DataTable</i></td><td>ExecuteDataTable</td></tr> +<tr><td>Class implementing the <i>IList</i> interface</td><td><a href="ExecuteList.htm">ExecuteList</a> or <a href="ExecuteList.htm">ExecuteScalarList</a></td></tr> +<tr><td>Class implementing the <i>IDictionary</i> interface</td><td><a href="ExecuteDictionary.htm">ExecuteDictionary</a> or <a href="ExecuteDictionary.htm">ExecuteScalarDictionary</a></td></tr> +<tr><td><i>void</i></td><td>ExecuteNonQuery</td></tr> +<tr><td><i>string</i>, <i>byte[]</i> or value type</td><td><a href="ExecuteScalar.htm">ExecuteScalar</a></td></tr> +<tr><td>In any other case</td><td><a href="ExecuteObject.htm">ExecuteObject</a></td></tr> +</table> +The method name explicitly defines the action name, which is converted to the stored procedure name. +Type, sequential order, and name of the method parameters are mapped to the command parameters. +Exceptions from this rule are: +<br> +<div style='margin:-10px 0px -0px -10px'><ul compact="compact"> +<li>a parameter of <a href="../Data/index.htm"><i>DbManager</i></a> type. In this case generator uses provided <a href="../Data/index.htm"><i>DbManager</i></a> to call the command.</li> +<li>parameters decorated with attribute <a href='#FormatAttribute'>FormatAttribute</a>, <a href='#DestinationAttribute'>DestinationAttribute</a>.</li> +</ul></div> +</p> + +<h4>Generating process control</h4> + +<p class='j'> +The <b>PersonAccessor</b> class above is a very simple example and, of +course, it seems too ideal to be real. In real life, we need more flexibility +and more control over the generated code. BLToolkit contains a bunch of +attributes to control DataAccessor generation in addition to <b>DataAccessor</b> virtual members. +</p> + +<h5>Method CreateDbManager</h5> + +<% cs # +protected virtual DbManager CreateDbManager() +{ + return new DbManager(); +} +%> + +<p class='j'> +By default, this method creates a new instance of <b>DbManager</b> that uses default database configuration. +You can change this behavior by overriding this method. For example: +</p> +<% cs # +public abstract class OracleDataAccessor : DataAccessor +{ + protected override DbManager CreateDbManager() + { + return new DbManager("Oracle", "Production"); + } +} +%> + +<p class='j'> +This code will use the <i>Oracle</i> data provider and <i>Production</i> configuration. +</p> + +<a name='GetDefaultSpName'></a><h5>Method GetDefaultSpName</h5> + +<% cs # +protected virtual string GetDefaultSpName(string typeName, string actionName) +{ + return typeName == null? + actionName: + string.Format("{0}_{1}", typeName, actionName); +} +%> + +<p class='j'> +As I mentioned, the method name explicitly defines the so-called action name. +The final stored procedure name is created by the <b>GetDefaultSpName</b> +method. The default implementation uses the following naming convention: +</p> + +<ul> +<li> +If type name is provided, the method constructs the stored proc name by +concatenating the type and action names. Thus, if the type name is "Person" and +the action name is "GetAll", the resulting sproc name will be "Person_GetAll". +</li> +<li> +If the type name is NOT provided, the stored procedure name will equal the +action name.</li> +</ul> + +<p class='j'> +You can easily change this behavior. For example, for the naming convention "p_Person_GetAll", +the method implementation can be the following: +</p> +<% cs # +public abstract class MyBaseDataAccessor<T,A> : DataAccessor<T,A> + where A : DataAccessor<T,A> +{ + protected override string GetDefaultSpName(string typeName, string actionName) + { + return string.Format("p_{0}_{1}", typeName, actionName); + } +} +%> + +<h5>Method GetTableName</h5> + +<% cs # +protected virtual string GetTableName(Type type) +{ + // ... + return type.Name; +} +%> + +<p class='j'> +By default, the table name is the associated object type name (<i>Person</i> in our examples). +There are two ways to associate an object type with an accessor. By providing generic parameter: +</p> + <% cs # +public abstract class PersonAccessor : DataAccessor<Person> +{ +} +%> + +<p class='j'> +And by the <b>ObjectType</b> attribute: +</p> +<% cs # +[ObjectType(typeof(Person))] +public abstract class PersonAccessor : DataAccessor +{ +} +%> + +<p class='j'> +If you want to have different table and type names in your application, you may override the <b>GetTableName</b> method: +</p> +<% cs # +public abstract class OracleDataAccessor<T,A> : DataAccessor<T,A> + where A : DataAccessor<T,A> +{ + protected override string GetTableName(Type type) + { + return base.GetTableName(type).ToUpper(); + } +} +%> + +<h5>TableNameAttribute</h5> + +<p class='j'> +Also, you can change the table name for a particular object type by decorating this object +with the <b>TableNameAttribute</b> attribute: +</p> +<% cs # +[TableName("PERSON")] +public class Person +{ + public int ID; + public string FirstName; + public string LastName; +} +%> + +<h5>ActionNameAttribute</h5> + +<p class='j'> +This attribute allows changing the action name. +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + [ActionName("GetByID")] + protected abstract IDataReader GetByIDInternal(DbManager db, int id); + + public Person GetByID(int id) + { + using (DbManager db = GetDbManager()) + using (IDataReader rd = GetByIDInternal(db, id)) + { + Person p = new Person(); + + // do something complicated. + + return p; + } + } +} +%> + +<h5>ActionSprocNameAttribute</h5> + +<p class='j'> +This attribute associates the action name with a stored procedure name: +</p> +<% cs# +[ActionSprocName("Insert", sp_Person_Insert")] +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + public abstract void Insert(Person p); +} +%> + +<p class='j'> +This attribute can be useful when you need to reassign a stored procedure name for a method defined in your base class. +</p> + +<h5>SprocNameAttribute</h5> + +<p class='j'> +The regular way to assign deferent from default sproc name for a method is the <b>SprocName</b> attribute. +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + [SprocName("sp_Person_Insert")] + public abstract void Insert(Person p); +} +%> + +<a name='DestinationAttribute'></a><h5>DestinationAttribute</h5> + +<p class='j'> +By default, the DataAccessor generator uses method's return value to determine which <i>Execute</i> method +should be used to perform the current operation. +The <b>DestinationAttribute</b> indicates that target object is a parameter decorated with this attribute: +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + public abstract void GetAll([Destination] List<Person> list); +} +%> + +<h5>DirectionAttributes</h5> + +<p class='j'> +<i>DataAccessor</i> generator can map provided business object to stored +procedure parameters. <b>Direction</b> attributes allow controlling this process more precisely. +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + public abstract void Insert( + [Direction.Output("ID"), Direction.Ignore("LastName")] Person person); +} +%> + +<p class='j'> +In addition, BLToolkit provides two more direction attributes: +<b>Direction.InputOutputAttribute</b> and <b>Direction.ReturnValueAttribute</b>. +</p> + +<h5>DiscoverParametersAttribute</h5> +<p class='j'> +By default, BLToolkit expects method parameter names to match stored procedure +parameter names. The sequential order of parameters is not important in this +case. This attribute enforces BLToolkit to retrieve parameter information from +the sproc and to assign method parameters in the order they go. Parameter names +are ignored. +</p> + +<a name='FormatAttribute'></a><h5>FormatAttribute</h5> + +<p class='j'> +This attribute indicates that the specified parameter is used to construct the stored procedure name or SQL statement: +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + [SqlQuery("SELECT {0} FROM {1} WHERE {2}")] + public abstract List<string> GetStrings( + [Format(0)] string fieldName, + [Format(1)] string tableName, + [Format(2)] string whereClause); +} +%> + +<h5>IndexAttribute</h5> + +<p class='j'> +If you want your method to return a dictionary, you will have to specify fields to build the dictionary key. +The Index attribute allows you to do that: +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + [SqlQuery("SELECT * FROM Person")] + [Index("ID")] + public abstract Dictionary<int, Person> SelectAll1(); + + [SqlQuery("SELECT * FROM Person")] + [Index("@PersonID", "LastName")] + public abstract Dictionary<CompoundValue, Person> SelectAll2(); +} +%> +<p class='j'>Note: if your key has more than one field, the type of this key should be <b>CompoundValue</b>.</p> +<p class='j'>If the field name starts from '@' symbol, BLToolkit reads the field value from data source, +otherwise from an object property/field.</p> + +<h5>ParamNameAttribute</h5> + +<p class='j'> +By default, the method parameter name should match the stored procedure parameter name. +This attribute specifies the sproc parameter name explicitly. +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + public abstract Person SelectByName( + [ParamName("FirstName")] string name1, + [ParamName("@LastName")] string name2); +} +%> + +<h5>ScalarFieldNameAttribute</h5> + +<p class='j'> +If your method returns a dictionary of scalar values, you will have to specify the name or index of the field +used to populate the scalar list. The <b>ScalarFieldName</b> attribute allows you to do that: +</p> +<% cs # +public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + [SqlQuery("SELECT * FROM Person")] + [Index("@PersonID")] + [ScalarFieldName("FirstName")] + public abstract Dictionary<int, string> SelectAll1(); + + [SqlQuery("SELECT * FROM Person")] + [Index("PersonID", "LastName")] + [ScalarFieldName("FirstName")] + public abstract Dictionary<CompoundValue, string> SelectAll2(); +} +%> + +<h5>ScalarSourceAttribute</h5> + +<p class='j'> +If a method returns a scalar value, this attribute can be used to specify how database returns this value. +The <b>ScalarSource</b> attribute take a parameter of the <b>ScalarSourceType</b> type: +</p> + +<table class='data'> +<tr><th>ScalarSourceType</th><th>Description</th></tr> +<tr><td>DataReader</td><td>Calls the <b>DbManager.ExecuteReader</b> method, and then calls <b>IDataReader.GetValue</b> method to read the value.</td></tr> +<tr><td>OutputParameter</td><td>Calls the <b>DbManager.ExecuteNonQuery</b> method, and then reads value from the <b>IDbDataParameter.Value</b> property.</td></tr> +<tr><td>ReturnValue</td><td>Calls the <b>DbManager.ExecuteNonQuery</b> method, and then reads return value from command parameter collection.</td></tr> +<tr><td>AffectedRows</td><td>Calls the <b>DbManager.ExecuteNonQuery</b> method, and then returns its return value.</td></tr> +</table> + +<h5>SqlQueryAttribute</h5> + +<p class='j'> +This attribute allows specifying SQL statement. +</p> +<% cs # public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor> +{ + [SqlQuery("SELECT * FROM Person WHERE PersonID = @id")] + public abstract Person GetByID(int @id); +} +%> + +<h4>Conclusion</h4> + +<p class='j'> +I hope this brief tutorial demonstrates one of the simplest, quickest and +most low-maintenance ways to develop your data access layer. In addition, you +will get one more benefit, which is incredible object mapping performance. But +that is a topic we will discuss later. +</p>