iBatisNet Tutorial

iBatisNet Tutorial


1. Welcome

This tutorial takes an "over-the-shoulder" Cookbook approach. We'll define a simple data access problem and use iBATIS to solve it for us.

2. Test first!

Let's say our client has a database and one of the tables in the database is a list of people. Client tells us:

We would like to use a web application to display the people in this table and to add, edit, and delete individual records.

Not a complicated story, but it will cover the CRUD most developers want to learn first. :) Let's start with the people table client mentioned. Since we're keeping it simple, we'll say it's a table in an Access database. The table definition is shown as Example 1.

Example 1. The Person Table

Name              Type            Size
PER_ID            Long Integer      4
PER_FIRST_NAME    Text             40
PER_LAST_NAME     Text             40
PER_BIRTH_DATE    Date/Time         8
PER_WEIGHT_KG     Double            8
PER_HEIGHT_M      Double            8

The first thing our story says is that client would like to display a list of people. Example 2 shows our test for that.

Example 2. PersonTest.cs

using System.Collections;
using IBatisNet.DataMapper;
using NUnit.Framework;complete
namespace iBatisTutorial.Model
{
[TestFixture]
public class PersonTest
{
[Test]
public void PersonList ()
{
// try it
IList people = Mapper.Instance().QueryForList("SelectAll",null);
// test it
Assert.IsNotNull(people,"Person list not returned");
Assert.IsTrue(people.Count>0,"Person list is empty");
Person person = (Person) people[0];
Assert.IsNotNull(person,"Person not returned");
}
}
}

Well, Example 2 sure looks easy enough! We ask a method to "select all", and it returns a list of person objects. But, what code do we need to write to pass this test?

Let's see. The test uses a list of person objects. We could start with a blank object, just to satisfy the test, and add the display properties later. But let's be naughty and skip a step. Our fully-formed person object is shown in Example 3.

Example 3. Person.cs

using System;
namespace iBatisTutorial.Model
{
public class Person
{
private int _Id;
private string _FirstName;
private string _LastName;
private DateTime _BirthDate;
private double _WeightInKilograms;
private double _HeightInMeters;
public int Id
{
get{ return _Id; }
set{ _Id = value; }
}
// Other public properties for the private fields ...
}
}

OK, that was fun! The Assert class is built into NUnit, so to compile Example 2, we just need the Mapper object and QueryForList method. The Mapper is built into the iBATIS framework, so we don't need to write that either. The iBATIS QueryForList method executes our SQL statement (or stored procedure) and returns the result as a list. Each row in the result becomes an entry in the list. Along with QueryForList, there is also Delete, Insert, Select, QueryForObject, and a couple of other methods in the iBATIS API. (See Section 5 in the Developers Guide for details.)

Looking at Example 2, we see that the QueryForList method takes the name of the statement we want to run and any runtime values the statement may need. Since a "SelectAll" statement wouldn't need any runtime values, we pass null in our test.

OK. Easy enough. But where does iBATIS get the "SelectAll" statement? Some systems try to generate SQL statements for you, but iBATIS specializes in data mapping, not code generation. It's our job (or the job of our database administrator) to craft the SQL or provide a stored procedure. We then describe the statement in an XML element, like the one shown in Example 4.

Example 4. We use XML elements to map a database statement to an application object

  <typeAlias alias="Person" assembly="iBatisTutorial.Model.dll"
type="iBatisTutorial.Model.Person" />
<resultMap id="SelectAllResult" class="Person">
<result property="Id" column="PER_ID" />
<result property="FirstName" column="PER_FIRST_NAME" />
<result property="LastName" column="PER_LAST_NAME" />
<result property="BirthDate" column="PER_BIRTH_DATE" />
<result property="WeightInKilograms" column="PER_WEIGHT_KG" />
<result property="HeightInMeters" column="PER_HEIGHT_M" />
</resultMap>
<select id="SelectAll" resultMap="SelectAllResult">
select
PER_ID,
PER_FIRST_NAME,
PER_LAST_NAME,
PER_BIRTH_DATE,
PER_WEIGHT_KG,
PER_HEIGHT_M
from PERSON
</select>

Note

Since this is a very simple case, iBATIS could in fact generate this SQL for us. In Example 4, we could have also written the select statement this way:

  <select id="SelectAll" resultMap="SelectAllResult">
<generate table="PERSON" />
</select>

Using the columns from the ResultMap, iBATIS would generate the SQL statement for us. But this feature only works with the simplest of SQL statements. Most often, you will write your own SQL statement or pass parameters to a stored procedure. (See Section 3 in the Developers Guide for more about the generate tag.)

The iBATIS mapping documents can hold several sets of related elements, like those shown in Example 4. We can also have as many mapping documents as we need. Having multiple mapping documents is handy when several developers are working on the project at once.

So, the framework gets the SQL code for the query from the mapping, and plugs it into a prepared statement. But, how does iBATIS know where to find the table's datasource?

Surprise! More XML! You can define a configuration file for each datasource your application uses. Example 5 shows a configuration file for our Access database.

Example 5. SqlMap.config - a configuration file for our Access database

<?xml version="1.0" encoding="UTF-8" ?>
<sqlMapConfig
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="SqlMapConfig.xsd">
<providers file="providers.config"/>
<database>
<dataSource name="iBatisTutorial"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=./Resources/iBatisTutorial.mdb"/>
</database>
<sqlMaps>
<sqlMap file="./Resources/PersonHelper.xml"/>
</sqlMaps>
</sqlMapConfig>

Of course, besides Access, other ADO.NET providers are supported, including SqlServer, Oracle, MySQL, and generic ODBC providers. (See Section 5 in the Developers Guide for details.)

The last part of the configuration file ("sqlMaps") is where we list our mapping documents, like the one shown back in Example 4. We can list as many documents as we need here, and they will all be read when the configuration is parsed.

OK, so how does the configuration get parsed?

Look back at Example 2. The heart of the code is the call to the "Mapper" object (under the remark "try it"). The Mapper object is a singleton. The first time it's called, it reads in the configuration documents to create the Mapper object. On subsequent calls, it reuses the Mapper object, so that the configuration is re-read only if the file changes.

The framework comes bundled with a default Mapper class. If you want to use a different name for the configuration file, or need to use more than one database, you can also use your own class, by copying and modifying the standard version.

Example 6 shows the code for the standard Mapper class that comes with the framework.

Example 6. The standard "Mapper" facade for providing access to the data maps

using IBatisNet.Common.Utilities;
using IBatisNet.DataMapper;
namespace IBatisNet.DataMapper
{
public class Mapper
{
private static volatile SqlMapper _mapper = null;
protected static void Configure (object obj)
{
_mapper = (SqlMapper) obj;
}
protected static void InitMapper()
{
ConfigureHandler handler = new ConfigureHandler (Configure);
_mapper = SqlMapper.ConfigureAndWatch (handler);
}
public static SqlMapper Instance()
{
if (_mapper == null)
{
lock (typeof (SqlMapper))
{
if (_mapper == null) // double-check
InitMapper();
}
}
return _mapper;
}
public static SqlMapper Get()
{
return Instance();
}
}
}

If you wanted to use a second database, you could load another configuration by changing the call to ConfigureHandler. There's another signature that takes a file name, which you can use to specify another configuration, as so:

ConfigureHandler handler = new ConfigureHandler (Configure,"sqlmap2.config");

You can access as many different database configurations as you need, just by setting up additional Mapper classes. Each Mapper configuration is a facade for a database. As far as our application knows, the "Mapper" *is* the database. Behind the Mapper facade, you can change the location of the database, or switch between SQL statements and stored procedures, with zero-changes to your application code.

If we put this all together into a solution, we can "green bar" our test, as shown by Figure 1. If you'd like to see that bar for yourself, open the iBatisNet Tutorial solution available from the website <http://sf.net/projects/ibatisnet>. If you do, you'll note that we set up the solution using two projects. A "Model" project for our business and database code and corresponding unit tests, and a "Web" project our user application. (After extracting the solution, set Web Sharing for the "Web" folder as "iBatisTutorial".)

Figure 1. Green Bar!

Green Bar!

3. Playtest second!

Now that we have a passing test, we setup a page to display our list of people. Example 7 shows the ASPX code for our display page. The key piece is the DataGrid.

Example 7. ASPX page for our Person list

<%@ Page language="c#" Codebehind="Person.aspx.cs" AutoEventWireup="false" Inherits="iBatisTutorial.Web.Forms.Person" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
<head>
<title>Person</title>
<meta name="GENERATOR" Content="Microsoft Visual Studio .NET 7.1">
<meta name="CODE_LANGUAGE" Content="C#">
<meta name=vs_defaultClientScript content="JavaScript">
<meta name=vs_targetSchema content="http://schemas.microsoft.com/intellisense/ie5">
</head>
<body>
<form id="Person" method="post" runat="server">
<asp:Panel ID="pnlList" Runat="server">
<h1>Person List</h1>
<asp:DataGrid id="dgList" Runat="server"></asp:DataGrid>
</asp:Panel>
</form>
</body>
</html>

Of course, we still need to populate that DataGrid. Example 8 shows the code-behind. The operative method is List_Load. The rest is supporting code.

Example 8. Code-behind class for our Person list page

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using IBatisNet.DataMapper;
namespace iBatisTutorial.Web.Forms
{
public class PersonPage : Page
{
#region panel: List
protected Panel pnlList;
protected DataGrid dgList;
private void List_Load ()
{
dgList.DataSource = Mapper.Instance().QueryForList("SelectAll",null);
dgList.DataBind();
}
#endregion
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack)
{
List_Load ();
}
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}

If we run this now, we'll get a list like the one shown in Figure 2.

Figure 2. A quick-and-dirty Person List

A quick-and-dirty Person List

Not pretty, but at least we have a starting point. The DataGrid is a flexible control, and we can always adjust the column order and headings by changing the ASPX file. The point of this exercise is to get the data to the grid so we can refine it later.

4. Test, test, again ...

Of course, tweaking the Person List display is not going to be the end of it. Clients always want more, and now ours wants to edit, add, or delete records. Let's write some tests for these new tasks, as shown in Example 9.

Example 9. New stories, new tests

  [Test]
public void PersonUpdate ()
{
const string EXPECT = "Clinton";
const string EDITED = "Notnilc";
// get it
person.Id = 1; person = (Person) Mapper.Instance().QueryForObject("Select",1);
// test it
Assert.IsNotNull(person,"Missing person");
Assert.IsTrue(EXPECT.Equals(person.FirstName),"Mistaken identity");
//change it
person.FirstName = EDITED;
Mapper.Instance().Update("Update",person);
// get it again some
person = (Person) Mapper.Instance().QueryForObject("Select",1);
// test it
Assert.IsTrue(EDITED.Equals(person.FirstName),"Same old, same old?");
// change it back
person.FirstName = EXPECT;
Mapper.Instance().Update("Update",person);
}
[Test]
public voidPersonInsertDelete ()
{
// insert it
Person person = new Person();
person.Id = -1;
Mapper.Instance().Insert("Insert",person);
// delete it
int count = Mapper.Instance().Delete("Delete",-1);
Assert.IsTrue(count>0,"Nothing to delete");
}    

Not the best tests ever written, but for now, they will do :)

To make the new tests work, we'll need some new mapping statements. Example 10 shows the complete PersonHelper.xml mapper document.

Example 10. The new and improved Mapper document

<xml version="1.0" encoding="utf-8" ?>
<sqlMap
namespace="Person"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="SqlMap.xsd">
<alias>
<typeAlias alias="Person" assembly="iBatisTutorial.Model.dll" type="iBatisTutorial.Model.Person" />
</alias>
<resultMaps>
<resultMap id="SelectResult" class="Person">
<result property="Id" column="PER_ID" />
<result property="FirstName" column="PER_FIRST_NAME" />
<result property="LastName" column="PER_LAST_NAME" />
<result property="BirthDate" column="PER_BIRTH_DATE" />
<result property="WeightInKilograms" column="PER_WEIGHT_KG" />
<result property="HeightInMeters" column="PER_HEIGHT_M" />
</resultMap>
</resultMaps>
<statements>
<select id="Select" parameterClass="int" resultMap="SelectResult">
select
PER_ID,
PER_FIRST_NAME,
PER_LAST_NAME,
PER_BIRTH_DATE,
PER_WEIGHT_KG,
PER_HEIGHT_M
from PERSON
<dynamic prepend="WHERE">
<isParameterPresent>
PER_ID = #value#
</isParameterPresent>
</dynamic>
</select>
<insert id="Insert" parameterClass="Person" resultClas"int">
insert into PERSON
(PER_ID, PER_FIRST_NAME, PER_LAST_NAME,
PER_BIRTH_DATE, PER_WEIGHT_KG, PER_HEIGHT_M)
values
(#Id#, #FirstName#, #LastName#,
#BirthDate#, #WeightInKilograms#, #HeightInMeters#)
</insert>
<update id="Update" parameterClass="Person" resultClass="int">
update PERSON set
PER_FIRST_NAME = #FirstName#,
PER_LAST_NAME = #LastName#,
PER_BIRTH_DATE = #BirthDate#,
PER_WEIGHT_KG = #WeightInKilograms#,
PER_HEIGHT_M = #HeightInMeters#
where PER_ID = #Id#
</update>
<delete id="Delete" parameterClass="int" resultClass="int">
delete from PERSON
where PER_ID = #value#
</delete>
</statements>
</sqlMap>

Note

Of course, the statements in Example 10 are so simple that iBATIS could generate the SQL for us. Since your own applications are liable to be more complex, we continue to use manual SQL in the examples.

Hmm. So, what's with this <isParameterPresent> tag in the Select element? The <isParameterPresent> tag is an example of Dynamic SQL. Rather than have nearly identical statements for SelectAll and SelectById, we can use Dynamic SQL to let the same statement serve both purposes. If Id is null, the WHERE clause is omitted. Otherwise, the WHERE clause is included. Dynamic SQL is even more useful for "Query By Example" stories. (See Section 3.7 in the Developers Guide for more.)

Why bother? Because with the dynamic clause, we can use a single Select statement for SelectAll or SelectById. Since both invocations are suppose to return the same result, using one statement instead of two reduces redundancy and saves maintenance.

Well, waddya know, if run our tests now, we are favored with a green bar!. It all works!

Note

Though, of course, everything did not work perfectly the first time! We had to fix this and that, and try, try, again. But NUnit made trying again quick and easy. For changes to the XML mappings document, we do not even have to recompile. We can just update the XML and rerun the test! No fuss, no muss.

Turning back to our ASP.NET page, we can revamp the DataGrid to allow in-place editing and deleting. To add records, we provide a button after the grid that inserts a blank person for client to edit. The ASPX code is shown as Example 11.

Example 11. ASPX code for our enhanced DataGrid

    <asp:DataGrid id="dgList" Runat="server"
AutoGenerateColumns=False
DataKeyField="Id"
OnEditCommand="List_Edit"
OnCancelCommand="List_Cancel"
OnUpdateCommand="List_Update"
OnDeleteCommand="List_Delete">
<Columns>
<asp:BoundColumn DataField="FirstName" HeaderText="First"></asp:BoundColumn>
<asp:BoundColumn DataField="LastName" HeaderText="Last"></asp:BoundColumn>
<asp:BoundColumn DataField="HeightInMeters" HeaderText="Height"></asp:BoundColumn>
<asp:BoundColumn DataField="WeightInKilograms" HeaderText="Weight"></asp:BoundColumn>
<asp:EditCommandColumn ButtonType="PushButton" EditText="Edit" CancelText="Cancel"
<asp:ButtonColumn ButtonType="PushButton" Text="Delete" CommandName="Delete"></asp:ButtonColumn>
UpdateText="Save"></asp:EditCommandColumn>
</Columns>
</asp:DataGrid>
<p><asp:Button ID="btnAdd" Runat="server"></asp:Button></p>

Example 12 shows the corresponding methods from the code-behind.

Example 12. The code-behind methods for our new DataGrid

 #region panel: List
protected Panel pnlList;
protected DataGrid dgList;
protected Button btnAdd;
private void List_Init()
{
btnAdd.Text ="Add New Person";
this.btnAdd.Click += new EventHandler(List_Add);
}
private void List_Load ()
{
dgList.DataSource = Mapper.Instance().QueryForList("Select",null);
dgList.DataBind();
}
protected void List_Delete(object source, DataGridCommandEventArgs e)
{
int id = GetKey(dgList,e);
Mapper.Instance().Delete("Delete",id);
List_Load();
}
protected void List_Edit(object source, DataGridCommandEventArgs e)
{
dgList.EditItemIndex = e.Item.ItemIndex;
List_Load();
}
protected void List_Update(object source, DataGridCommandEventArgs e)
{
Person person = new Person();
person.Id = GetKey(dgList,e);
person.FirstName = GetText(e,0);
person.LastName = GetText(e,1);
person.HeightInMeters = GetDouble(e,2);
person.WeightInKilograms = GetDouble(e,3);
Mapper.Instance().Update("Update",person);
List_Load();
}
protected void List_Cancel(object source, DataGridCommandEventArgs e)
{
dgList.EditItemIndex = -1;
List_Load();
}
private int GetKey(DataGrid dg, DataGridCommandEventArgs e)
{
return (Int32) dg.DataKeys[e.Item.DataSetIndex];
}
private string GetText(DataGridCommandEventArgs e, int v)
{
return ((TextBox) e.Item.Cells[v].Controls[0]).Text;
}
private double GetDouble(DataGridCommandEventArgs e, int v)
{
return Convernext to the t.ToDouble(GetText(e,v));
}
#endregion
private void Page_Load(object sender, System.EventArgs e)
{
List_Init();
if (!IsPostBack)
{
List_Load ();
}
}
// Web Form Designer generated code ...

OK, we are CRUD complete! There's more we could do here. In particular, we should add validation methods to prevent client from entering alphabetic characters where only numbers can live. But, that's ASP.NET grunt-work, and this is an iBATIS tutorial. So, for now, we can stand tall and proudly declare: Mission accomplished!

5. Then, refactor

Well, almost accomplished. The story is complete from client's perspective, but I really don't like spelling out the statement names in so many places. Mistyping the name is a bug waiting to happen. Let's encapsulate the statements we want to call in a helper class, so we only spell out the statement name in one place. Example 13 shows our PersonHelper class.

Example 13. PersonHelper.cs - A helper class for accessing the database

using System.Collections;
namespace iBatisTutorial.Model
{
public class PersonHelper: BaseHelper
{
public Person Select(int id)
{
return (Person) Mapper().QueryForObject("Select",id);
}
public IList SelectAll()
{
return Mapper().QueryForList("Select",null);
}
public int Insert(Person person)
{
Mapper().Insert("Insert",person);
// Insert is designed so that it can return the new key
// but we are not utilizing that feature here
return 1;
}
public int Update(Person person)
{
return Mapper().Update("Update",person);
}
public int Delete(int id)
{
return Mapper().Delete("Delete",id);
}
}

In a larger application, there would be several other "business" objects, like "Person", and several other helper objects. Knowing this, we setup a base Helper class. Our Helper base class, shown in Example 14, provides the Mapper method that hooks up our application with iBATIS.

Example 14. Helper.cs -- Our only link to iBATIS

using IBatisNet.DataMapper;
namespace iBatisTutorial.Model
{
public class Helper
{
public SqlMapper Mapper ()
{
return IBatisNet.DataMapper.Mapper.Instance ();
}
}
}

We could now go back and do a straight-forward refactoring. Everywhere we called, say:

Mapper.Get().Update("Update",person);

we could now call:

PersonHelper helper = new PersonHelper(); helper.Update(person);

Hmm. Better in terms of being strongly typed, worse in terms of object management. Now we have to create an instance of PersonHelper to make a data access call. Example 15 shows an alternative solution: Use a singleton to instantiate our helper class one time.

Example 15. Helpers.cs - A singleton "concierge" for our Helper classes.

namespace iBatisTutorial.Model {
public class Helpers
{
private static volatile PersonHelper _PersonHelper = null;
public static PersonHelper Person ()
{
if (_PersonHelper == null)
{
lock (typeof (PersonHelper))
{
if (_PersonHelper == null) // double-check
_PersonHelper = new PersonHelper();
}
}
return _PersonHelper;
}
}
}

Now if we do a refactoring, we can replace:

 Mapper.Instance().Update("Update",person);

with

 Helpers.Person().Update(person);

If we had another helper, say for pets, we could add that to the Helper class and also be able to call:

 Helpers.Pet().Update(pet);

and so forth. As shown in Figure 3, we can also make full use of Intellisense in selecting both a helper class and its methods.

Figure 3. Intellisense works great with our Helper singleton!

Intellisense works great with our Helper singleton!

Figure 4 shows the Subversion DIFF report after refactoring for the Helper classes. The lines that changed are emphasized.

Figure 4. Refactoring the Person test class

Index: C:/projects/SourceForge/ibatisnet/Tutorial2/Model/PersonTest.cs
===================================================================
--- C:/projects/SourceForge/ibatisnet/Tutorial2/Model/PersonTest.cs (revision 196)
+++ C:/projects/SourceForge/ibatisnet/Tutorial2/Model/PersonTest.cs (working copy)
@@ -1,5 +1,5 @@
using System.Collections;
-using IBatisNet.DataMapper;
+using iBatisTutorial.Model;
using NUnit.Framework;
namespace iBatisTutorial.Model
@@ -11,7 +11,7 @@
public void PersonList ()
{
// try it
-   IList people = Mapper.Instance().QueryForList("Select",null);
+   IList people = Helpers.Person().SelectAll();
// test it
Assert.IsNotNull(people,"Person list not returned");
@@ -28,7 +28,7 @@
// get it
Person person = new Person();
-   person = (Person) Mapper.Instance().QueryForObject("Select",1);
+   person = Helpers.Person().Select(1);

// test it
Assert.IsNotNull(person,"Missing person");
@@ -36,17 +36,17 @@
//change it
person.FirstName = EDITED;
-   Mapper.Instance().Update("Update",person);
+  Helpers.Person().Update(person);

// get it again
-   Mapper.Instance().Insert("Insert",person);
+   Helpers.Person().Insert(person);
-   person = (Person) Mapper.Instance().QueryForObject("Select",1);
+   person = Helpers.Person().Select(1);

// test it
Assert.IsTrue(EDITED.Equals(person.FirstName),"Same old, same old?");
// change it back
person.FirstName = EXPECT;
-   Mapper.Instance().Update("Update",person);
+   Helpers.Person().Update(person);
   }
[Test]
@@ -55,9 +55,9 @@
// insert it
Person person = new Person();
person.Id = -1;
-   Mapper.Instance().Insert("Insert",person);
+   Helpers.Person().Insert(person);
    // delete it
-   int count = Mapper.Instance().Delete("Delete",-1);
+   int count = Helpers.Person().Delete(person.Id);
    Assert.IsTrue(count>0,"Nothing to delete");
}
}

Now, all our references are to our own Helper singleton rather than the iBATIS Mapper class. Figure 5 shows the same type of DIFF report for our ASP.NET code-behind file. The ASP.NET page is unchanged. (Ahh,the beauty of code-behind!)

Figure 5. Refactoring the Person list code-behind class.

Index: C:/projects/SourceForge/ibatisnet/Tutorial2/WebView/Forms/Person.aspx.cs
===================================================================
--- C:/projects/SourceForge/ibatisnet/Tutorial2/WebView/Forms/Person.aspx.cs (revision 196)
+++ C:/projects/SourceForge/ibatisnet/Tutorial2/WebView/Forms/Person.aspx.cs (working copy)
@@ -1,13 +1,10 @@
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
-using IBatisNet.DataMapper;
using iBatisTutorial.Model;
namespace iBatisTutorial.Web.Forms
{
public class PersonPage : Page
{
@@ -25,14 +22,14 @@
private void List_Load ()
{
-   dgList.DataSource = Mapper.Instance().QueryForList("Select",null);
+   dgList.DataSource = Helpers.Person().SelectAll();
    dgList.DataBind();
}
protected void List_Delete(object source, DataGridCommandEventArgs e)
{
int id = GetKey(dgList,e);
-   Mapper.Instance().Delete("Delete",id);
+   Helpers.Person().Delete(id);
    List_Load();
}
@@ -50,7 +47,7 @@
person.LastName = GetText(e,1);
person.HeightInMeters = GetDouble(e,2);
person.WeightInKilograms = GetDouble(e,3);
-   Mapper.Instance().Update("Update",person);
+   Helpers.Person().Update(person);
    List_Load();
}
@@ -79,7 +76,7 @@
{
Person person = new Person();
person.FirstName = "--New Person--";
-   Mapper.Instance().Insert("Insert",person);
+   Helpers.Person().Insert(person);
    List_Load();
}

Note that adding the Helper singleton let us remove all iBATIS framework semantics from the Test and code-behind classes. Looking at the classes, you can't tell what data persistence system (if any) is being used. All you see is a call to some Helper. In my book, that's a good thing!

Now I'm happy, and ready to move on to the next story ... As for you, if you've liked what you've seen so far, please move on to the Developers Guide. There are many more goodies in store.

原文地址:https://www.cnblogs.com/guola/p/866167.html