提綱
一、什么是ORM。
二、反射以及Attribute在ORM中的應用。
三、創建一個數據庫表和表對應的實體model。
四、實體model如何映射出數據庫表。
五、組合ORM映射生成insert語句。
六、測試ORM的插入映射。
七、總結。
內容:
一 、什么是ORM?
概念: 對象關系映射(Object Relational Mapping,簡稱ORM,或O/RM,或O/R mapping),是一種程序技術,用於實現面向對象編程語言里不同類型系統的數據之間的轉換。
詳細介紹: 讓我們從O/R開始。字母O起源於"對象"(Object),而R則來自於"關系"(Relational)。幾乎所有的程序里面,都存在對象和關系數據庫。在業務邏輯層和用戶界面層中,我們是面向對象的。當對象信息發生變化的時候,我們需要把對象的信息保存在關系數據庫中。
當你開發一個應用程序的時候(不使用O/R Mapping),你可能會寫不少數據訪問層的代碼,用來從數據庫保存,刪除,讀取對象信息,等等。你在DAL中寫了很多的方法來讀取對象數據,改變狀態對象等等任務。而這些代碼寫起來總是重復的。
ORM解決的主要問題是對象關系的映射。域模型和關系模型分別是建立在概念模型的基礎上的。域模型是面向對象的,而關系模型是面向關系的。一般情況下,一個持久化類和一個表對應,類的每個實例對應表中的一條記錄,類的每個屬性對應表的每個字段。
ORM技術特點:
1.提高了開發效率。由於ORM可以自動對Entity對象與數據庫中的Table進行字段與屬性的映射,所以我們實際可能已經不需要一個專用的、龐大的數據訪問層。
2.ORM提供了對數據庫的映射,不用sql直接編碼,能夠像操作對象一樣從數據庫獲取數據。
二、反射以及Attribute在ORM中的應用。
什么是反射?
簡單點吧,反射就是在運行時動態獲取對象信息的方法,比如運行時知道對象有哪些屬性,方法,委托等等等等。
反射有什么用呢?
反射不但讓你在運行是獲取對象的信息,還提供運行時動態調用對象方法以及動態設置、獲取屬性等的能力。
反射在ORM中有什么用呢?
我 這里所討論的ORM實現是通過自定義Attribute的方式進行映射規則的描述的。但是我們並不知道具體哪個對象需要對應哪個表,並且這些對象是獨立於 我們的ORM框架的,所以我們只能通過自定義Attribute來定義映射規則,然后通過反射來動態獲取這些映射規則。
(這里只簡單說明下概念:具體如何實現過程請看第四項。)
三、創建一個數據庫表和表對應的實體model。
傳統的創建表和model實體的創建過程。
1.創建數據庫表
create table TB_People
(
Pl_ID Int identity(1,1) primary key ,
PL_Age Int,
Pl_Sex Nvarchar(4),
Pl_LoginName nvarchar(30),
Pl_TrueName nvarchar(30),
PL_Pwd nvarchar(60)
)
2.根據表結構一般我們會創建如下model實體
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FuzhuKeji
{
public class M_People
{
string _Pl_ID;
public string Pl_ID
{
get { return _Pl_ID; }
set { _Pl_ID = value; }
}
int _PL_Age;
public int PL_Age
{
get { return _PL_Age; }
set { _PL_Age = value; }
}
string _Pl_Sex;
public string Pl_Sex
{
get { return _Pl_Sex; }
set { _Pl_Sex = value; }
}
string _Pl_LoginName;
public string Pl_LoginName
{
get { return _Pl_LoginName; }
set { _Pl_LoginName = value; }
}
string _Pl_TrueName;
public string Pl_TrueName
{
get { return _Pl_TrueName; }
set { _Pl_TrueName = value; }
}
string _PL_Pwd;
public string PL_Pwd
{
get { return _PL_Pwd; }
set { _PL_Pwd = value; }
}
}
}
現在看到了表結構 和model實體,那如何根據model實體映射出表的插入語句及結構呢?下面我們就介紹有model映射到數據庫表。
四、實體model如何映射出數據庫表。
上面簡單介紹了反射以及Attribute在ORM中的應用,那如何通過這些進行映射出來的呢?
方法一:
看到 題綱三中的model實體了,下面我們就通過反射的方法來動態獲取此映射規則:
/// <summary> /// 測試映射 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button1_Click(object sender, EventArgs e) { M_People mp = new M_People(); mp.PL_Age = 26; mp.Pl_ID = "001"; mp.Pl_LoginName = "Test1"; mp.PL_Pwd = "123"; mp.Pl_Sex = "男"; mp.Pl_TrueName = "張三"; PropertyInfo[] infos = mp.GetType().GetProperties(); string Message_shuxing1 = ""; foreach (PropertyInfo info in infos) { //獲取屬性並打印 Message_shuxing1 = Message_shuxing1 + (info.Name + ":" + info.GetValue(mp, null)); } MessageBox.Show("這里看到可以獲得屬性名稱和屬性值(是不是對ORM有點慢慢明白了):"+Message_shuxing1); // 上面info.GetValue(mp, null)獲得屬性的值。 //info.SetValue(mp, "XX", null); 賦值 }
測試效果圖如下:
是不是有點思路了,知道如何搞了,呵呵。
看到紅色部分了嗎
有感覺了沒有:是不是和數據庫的名稱一樣,而且還獲得了值。為什么會出現這種情況呢?
屬性是來至那?--Model實體吧,屬性的名稱也是model實體屬性名稱吧。所以我們只要把屬性的名稱按某個規則定義就可以獲得其對應的數據庫字段名和類型。
方法二:
備注下:其實不只這種方法可以完成ORM的映射,而且還可以通過Attribute:
Attribute中文翻譯雖然也號稱“屬性”,但是她和對象的屬性(Property)其實是完全不同的兩概念。她是在運行時對對象或者對象屬性、方法、委托等等進行描述的類,用於在運行時描述你的代碼或者在運行時影響你的程序的行為。
其 實我們在c#的編程中經常看到Attribute,只不過我們沒有注意罷了。比如Main函數前的“[STAThread]”這個其實就是一個 Attribute。全程為[STAThreadAttribute]。另外指定類可序列化的[Serializable]等等。是不是都很熟悉啊?只不 過平時估計沒有用到,所以沒有注意罷了。
既然Attribute是類,那么她的定義方法和類就沒有兩樣了,唯一的不同就是自定義Attribute類必須繼承於System.Attribute。
那我們改下M_People實體的東西如下:
下面我們來簡單定義一個描述數據庫字段信息的Attribute,在此類中我們采用更省略的方式,僅僅提供“字段名”,“字段類型”:
public class DataFieldAttribute : Attribute
{
private string _FieldName;
private string _FieldType;
public DataFieldAttribute(string fieldname, string fieldtype)
{
this._FieldName = fieldname;
this._FieldType = fieldtype;
}
public string FieldName
{
get { return this._FieldName; }
set { this._FieldName = value; }
}
public string FieldType
{
get { return this._FieldType; }
set { this._FieldType = value; }
}
}
那我們把Mode更改下改為如下:
public class M_People
{
string _Pl_ID;
[DataFieldAttribute("Pl_ID", "Int")]
public string Pl_ID
{
get { return _Pl_ID; }
set { _Pl_ID = value; }
}
int _PL_Age;
[DataFieldAttribute("PL_Age", "Int")]
public int PL_Age
{
get { return _PL_Age; }
set { _PL_Age = value; }
}
string _Pl_Sex;
[DataFieldAttribute("Pl_Sex", "nvarchar")]
public string Pl_Sex
{
get { return _Pl_Sex; }
set { _Pl_Sex = value; }
}
string _Pl_LoginName;
[DataFieldAttribute("Pl_LoginName", "nvarchar")]
public string Pl_LoginName
{
get { return _Pl_LoginName; }
set { _Pl_LoginName = value; }
}
string _Pl_TrueName;
[DataFieldAttribute("Pl_TrueName", "nvarchar")]
public string Pl_TrueName
{
get { return _Pl_TrueName; }
set { _Pl_TrueName = value; }
}
string _PL_Pwd;
[DataFieldAttribute("PL_Pwd", "nvarchar")]
public string PL_Pwd
{
get { return _PL_Pwd; }
set { _PL_Pwd = value; }
}
}
通過自定義Attribute,我們定義了類屬性和數據庫字段的一一對應關系。
那我們通過事件測試下方法案例:
/// <summary> /// 反射+Attribute 映射出數據庫表 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button2_Click(object sender, EventArgs e) { M_People mp = new M_People(); mp.PL_Age = 26; mp.Pl_ID = "001"; mp.Pl_LoginName = "Test1"; mp.PL_Pwd = "123"; mp.Pl_Sex = "男"; mp.Pl_TrueName = "張三"; PropertyInfo[] infos = mp.GetType().GetProperties(); string Str_TestAtrrubute = ""; object[] objDataFieldAttribute = null; foreach (PropertyInfo info in infos) { objDataFieldAttribute = info.GetCustomAttributes(typeof(DataFieldAttribute), false); if (objDataFieldAttribute != null) { Str_TestAtrrubute=Str_TestAtrrubute+(info.Name + "->數據庫字段:" + ((DataFieldAttribute)objDataFieldAttribute[0]).FieldName)+" --------"; } } MessageBox.Show(Str_TestAtrrubute); }
測試的效果圖如下:
哈 哈,你是不是很想動手了啊?
加油!下面我們就介紹如何實現插入語句的映射!
五、組合ORM映射生成insert語句。
我們仔細思考下看到上面的我們會怎么想才可以生成一條sql語句並且執行。
首先我們是不是應該分開兩部分,
第一步負責生成插入語句。
第二步是負責執行插入語句得。
我們繼續思考?生成一條插入語句我們要考慮哪些問題?
a、是不是返回值
b、是不是要判斷表中是否有不需要組合為插入語句的字段(如自增字段)
c、而且這個插入語句是針對對象的插入語句而不是固定的某個或者已知的某個實體。所以我們會考慮到泛型的使用。
這樣我們基本確定了insert語句的參數和結構。
我們再回到第一步如何根據實體生成插入語句? 我們第四部也只是說了根據實體映射出和數據庫字段一樣的名字,這有什么用呢?
肯定根據這些字段名我們要想辦法組合個sql語句。繼續分析、、、
如何分工 :肯定要給個執行的sql語句
分工一:是不是獲得屬性名稱組合sql。
分工二:是不是做個參數的對應表。
分工三:組合這些東西,並把其屬性類型和數據庫字段類型對應起來。
上面說了那么多,只是幫大家打開思路,其實只要你理解了映射(第四項),用自己的思路去寫那些組合sql也可以得,
我這個地方寫的也不見得完美,只是給大家做個例子,嘿嘿,一起加油!
有幾個地方用到了枚舉首先我列出枚舉的方法:
第一個屬性標識是否為主鍵或者讀寫的標識
[Serializable]
[Flags]
public enum ColumnKeyType
{
/// <summary>
/// 默認狀態
/// </summary>
Default = 1,
/// <summary>
/// 標識為主鍵
/// </summary>
Identity = 2,
/// <summary>
/// Extend狀態下,不參與讀取、增加、修改
/// </summary>
Extend = 4,
/// <summary>
/// Read狀態下不參與增加、修改
/// </summary>
Read = 8
}
返回值做了枚舉:
public enum DBReturnType
{ /// <summary>
/// 返回受影響的行數
/// </summary>
EffectRow,
/// <summary>
/// 返回最后插入的主鍵值
/// </summary>
Identity
}
插入語句的代碼:
#region 把對象內容保存到數據庫中 Insert
/// <summary>
/// 把對象內容保存到數據庫中
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="model"></param>
/// <param name="isIncludeKeyColumn">插入語句中是否包含對主鍵的插入,當主鍵值為自動增加時為false</param>
/// <param name="returnType">返回的數據類型:DBReturnType.EffectRow 為返回受影響行數;DBReturnType.IdEntity 返回最新插入主鍵值(isIncludeKeyColumn == false時有效)</param>
public static int Insert<T>(T model, bool isIncludeKeyColumn, DBReturnType returnType) where T : class
{
int i = 0;
Type type = typeof(T);
//獲取表名
string tableName = EntityHelper.GetTableName(type);
PropertyInfo[] pis = type.GetProperties();
//獲取所有字段和主鍵名稱
List<string> columns = null;
//處理是否包含主鍵插入
if (isIncludeKeyColumn == false)
{
columns = EntityHelper.GetTableColumns(pis, ColumnKeyType.Identity | ColumnKeyType.Extend, null);
}
else
{
columns = EntityHelper.GetTableColumns(pis, ColumnKeyType.Extend, null);
}
//生成INSERT語句
StringBuilder sqlText = new StringBuilder();
sqlText.Append("INSERT INTO ");
sqlText.Append(tableName);
sqlText.Append(" (");
//第一個字段
sqlText.Append(columns[0]);
//第二個起所有字段
int loop = columns.Count;
for (i = 1; i < loop; i++)
{
sqlText.Append(",");
sqlText.Append(columns[i]);
}
sqlText.Append(") VALUES (");
//第一個字段
sqlText.Append("@");
sqlText.Append(columns[0]);
//第二個起所有字段
for (i = 1; i < loop; i++)
{
sqlText.Append(",@");
sqlText.Append(columns[i]);
}
sqlText.Append(");");
//生成MySqlParamter
PropertyInfo propertyInfo = null;
SqlParameter[] paras = new SqlParameter[loop];
for (i = 0; i < loop; i++)
{
propertyInfo = type.GetProperty(columns[i]);
paras[i] = new SqlParameter(columns[i], GetMySqlDbType(propertyInfo.PropertyType), -1);
paras[i].Value = propertyInfo.GetValue(model, null);
}
//根據兩種情況返回不同的值
if (isIncludeKeyColumn == false && returnType == DBReturnType.Identity)
{
sqlText.Append(" SELECT @@identity AS RetId");
SqlDataReader sdr = DataReader(sqlText.ToString(), CommandType.Text, paras);
int keyId = 0;
if (sdr.Read())
{
keyId = Convert.ToInt32(sdr["RetId"]);
}
sdr.Close();
return keyId;
}
else
{
return NonQuery(sqlText.ToString(), CommandType.Text, paras);
}
}
#endregion
#region 根據Type類型獲取SQL的數據類型
/// <summary>
/// 根據Type類型獲取MySQL的數據類型
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static SqlDbType GetMySqlDbType(Type type)
{
SqlDbType dbtype = SqlDbType.VarChar;
if (type.Equals(typeof(string)))
{
}
else if (type.Equals(typeof(int)))
{
dbtype = SqlDbType.Int;
}
else if (type.Equals(typeof(bool)))
{
dbtype = SqlDbType.Bit;
}
else if (type.Equals(typeof(DateTime)))
{
dbtype = SqlDbType.DateTime;
}
else if (type.Equals(typeof(decimal)))
{
dbtype = SqlDbType.Decimal;
}
else if (type.Equals(typeof(float)))
{
dbtype = SqlDbType.Float;
}
else if (type.Equals(typeof(double)))
{
dbtype = SqlDbType.Float;
}
return dbtype;
}
#endregion
下面我們簡單定義一個描述數據庫字段信息的Attribute 包括表名 屬性字段獲得
從Model模型中獲取數據表名、主鍵名、 獲取需要的讀取數據源的字段集
(忘了說明表的表名寫在那個地方,其實表名只需要定義在類的上面就可以了)為了簡單起見我還是把目前的model放到代碼里面
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FuzhuKeji
{
[Serializable]
[Property("TB_People")]
public class M_People
{
string _Pl_ID;
/// <summary>
/// 主鍵
/// </summary>
[Property(ColumnKeyType.Identity)]
public string Pl_ID
{
get { return _Pl_ID; }
set { _Pl_ID = value; }
}
int _PL_Age;
public int PL_Age
{
get { return _PL_Age; }
set { _PL_Age = value; }
}
string _Pl_Sex;
public string Pl_Sex
{
get { return _Pl_Sex; }
set { _Pl_Sex = value; }
}
string _Pl_LoginName;
public string Pl_LoginName
{
get { return _Pl_LoginName; }
set { _Pl_LoginName = value; }
}
string _Pl_TrueName;
public string Pl_TrueName
{
get { return _Pl_TrueName; }
set { _Pl_TrueName = value; }
}
string _PL_Pwd;
public string PL_Pwd
{
get { return _PL_Pwd; }
set { _PL_Pwd = value; }
}
}
}
好吧這樣就不用擔心了映射表名獲得字段的代碼在下面:
#region 下面我們簡單定義一個描述數據庫字段信息的Attribute 包括表名 屬性字段獲得
public class PropertyAttribute : Attribute
{
public string tableName;
public ColumnKeyType columnKeyType;
/// <summary>
/// 重構方法默認值
/// </summary>
public PropertyAttribute()
{
this.columnKeyType = ColumnKeyType.Default;
}
/// <summary>
///
/// </summary>
/// <param name="tableName"></param>
public PropertyAttribute(string tableName)
{
this.tableName = tableName;
}
public PropertyAttribute(ColumnKeyType columnKeyType)
{
this.columnKeyType = columnKeyType;
}
}
#endregion
#region 從Model模型中獲取數據表名、主鍵名、 獲取需要的讀取數據源的字段集
public class EntityHelper
{
/// <summary>
/// 從Model模型中獲取數據表名
/// </summary>
public static string GetTableName(Type type)
{
PropertyAttribute property = (PropertyAttribute)(type.GetCustomAttributes(false)[0]);
return property.tableName;
}
/// <summary>
/// 從Model模型中獲取數據主鍵名
/// </summary>
public static PropertyInfo GetTableIdentity(PropertyInfo[] pis)
{
object[] infos = null;
PropertyAttribute attribute = null;
foreach (PropertyInfo pi in pis)
{
infos = pi.GetCustomAttributes(false);
if (infos.Length > 0)
{
attribute = (PropertyAttribute)(infos[0]);
if (attribute.columnKeyType == ColumnKeyType.Identity)
{
return pi;
}
}
}
return null;
}
/// <summary>
/// 獲取需要的讀取數據源的字段集
/// </summary>
/// <param name="pis">Model模型所有屬性集合</param>
/// <param name="filter"></param>
/// <param name="customColumns">自定義查詢列名集合,使用逗號分隔。如不需要則為null</param>
/// <returns></returns>
public static List<string> GetTableColumns(PropertyInfo[] pis, ColumnKeyType filter, string customColumns)
{
string col = "";
return GetTableColumns(pis, filter, customColumns, ref col);
}
/// <summary>
/// 獲取需要的讀取數據源的字段集
/// </summary>
/// <param name="pis">Model模型所有屬性集合</param>
/// <param name="filter"></param>
/// <param name="customColumns">自定義查詢列名集合,使用逗號分隔。如不需要則為null</param>
/// <returns></returns>
public static List<string> GetTableColumns(PropertyInfo[] pis, ColumnKeyType filter, string customColumns, ref string outCol)
{
List<string> columns = new List<string>();
if (customColumns != null && customColumns.Length > 0)
{
/*
* 需要安全處理
* 限制字段不包含空格
*/
customColumns = customColumns.Trim();
string[] strs = customColumns.Split(',');
foreach (string str in strs)
{
if (IsRegexMatch(str, @"^(\w[^\s';]+)$"))
{
columns.Add(str);
}
}
outCol = customColumns;
}
else
{
object[] infos = null;
PropertyAttribute attribute = null;
foreach (PropertyInfo pi in pis)
{
//刪除外部擴展對象項
infos = pi.GetCustomAttributes(false);
if (infos.Length > 0)
{
attribute = (PropertyAttribute)(infos[0]);
if (attribute.columnKeyType == (filter & attribute.columnKeyType))
{
continue;
}
}
outCol += string.Concat(",", pi.Name);
columns.Add(pi.Name);
}
outCol = outCol.Remove(0, 1);
}
return columns;
}
/// <summary>
/// 檢查是否滿足某種正則表達式
/// </summary>
private static bool IsRegexMatch(string str, string Express)
{
if (string.IsNullOrEmpty(str))
{
return false;
}
return Regex.IsMatch(str, Express);
}
}
#endregion
上面就完成了sql語句的生成:下面我就定義一個sql語句的執行就可以了。其實你到insert方法里會發現 在生產完sql語句就調用執行方法了。
/// <summary>
/// 配置字符串參數
/// </summary>
private static void PrepareCommand(SqlConnection conn, SqlTransaction trans, SqlCommand sqlCommand, string sqlText, CommandType commandType, SqlParameter[] parms)
{
if (conn.State != ConnectionState.Open)
{
conn.Open();
}
sqlCommand.Connection = conn;
sqlCommand.CommandText = sqlText;
sqlCommand.CommandType = commandType;
if (trans != null)
{
sqlCommand.Transaction = trans;
}
if (parms != null)
{
foreach (SqlParameter parm in parms)
{
sqlCommand.Parameters.Add(parm);
}
}
}
/// <summary>
/// 執行SQL語句,返回數據集
/// </summary>
public static SqlDataReader DataReader(string sqlText, CommandType commandType, SqlParameter[] parms)
{
SqlConnection conn = new SqlConnection(@"Data Source=HAOFUQI\SQLEXPRESS;Initial Catalog=Fukusuke;Persist Security Info=True;User ID=sa;pwd=123");
SqlCommand sqlCommand = new SqlCommand();
PrepareCommand(conn, null, sqlCommand, sqlText, commandType, parms);
SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection);
sqlCommand.Dispose();
return reader;
}
/// <summary>
/// 執行SQL語句,並返回影響行數
/// </summary>
public static int NonQuery(string sqlText, CommandType commandType, SqlParameter[] parms)
{
int reVal = 0;
using (SqlConnection conn = new SqlConnection(@"Data Source=HAOFUQI\SQLEXPRESS;Initial Catalog=Fukusuke;Persist Security Info=True;User ID=sa;pwd=123"))
{
SqlCommand sqlCommand = new SqlCommand();
PrepareCommand(conn, null, sqlCommand, sqlText, commandType, parms);
reVal = sqlCommand.ExecuteNonQuery();
sqlCommand.Parameters.Clear();
sqlCommand.Dispose();
}
return reVal;
}
六、測試ORM的插入映射。
/// <summary> /// 執行插入語句 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button3_Click(object sender, EventArgs e) { M_People mp = new M_People(); mp.PL_Age = 26; mp.Pl_ID = "001"; mp.Pl_LoginName = "Test1"; mp.PL_Pwd = "123"; mp.Pl_Sex = "男"; mp.Pl_TrueName = "張三"; int Insert_Key= DBHelper.Insert<M_People>(mp,false ,DBReturnType.Identity); MessageBox.Show("添加成功! 插入數據的主鍵為:"+Insert_Key.ToString()); }
測試結果如下:

數據庫插入效果:

這個地方就插入語句成功了。
七、總結。
這篇文章寫得不是很好,我只是想描述,卻沒有一層層去剝開,也許這篇文章太長了吧。
這篇文章的核心應該是說明白如何映射如何到sql。沒有分層的概念。看起來混亂,但是如果分層我就要更多篇幅講解。
寫的錯誤或者不好的地方還請多多批評指導。
上一篇寫的是:為初學者寫三層.為初學者寫三層,三層的搭建和測試例子
下面准備寫ORM框架,然后把生成器集成到框架中。(偽三層+ORM)+生成器+常用類庫 用一個財務報銷系統作為案例。
好了就寫到這吧!
本文主要參考文獻:
1. 什么是ORM :http://www.cnblogs.com/double1030/archive/2009/02/01/1382062.html
2.ORM硬傷 :http://www.cnblogs.com/Barton131420/archive/2007/01/07/613955.html
3反射以及Attribute在ORM中的應用:.http://blog.csdn.net/ronotian/article/details/2900714
