給程序員準備的“蜜糍”--SOD框架簡介
注:本文是SOD框架源碼倉庫的首頁介紹,因為國內訪問Github比較困難,所以將此文發到博客園。原文地址
一、框架介紹
1,SOD框架是什么?
以前有一個著名的國產化妝品“大寶SOD密”,SOD框架雖然跟它沒有什么關系,但是名字的確受到它的啟發,因為SOD框架就是給程序員準備的“蜜糍”(一種含有蜂蜜的糍粑),簡單靈活且非常容易“上手”。

SOD框架是一個全功能數據開發框架,框架的三大核心功能(SQL-MAP、ORM、Data Controls)代表三種數據開發模式(SQL開發模式/ORM開發模式/窗體控件開發模式),這三大功能名稱的英文首字母縮寫也是SOD框架名稱的由來。SOD框架包含很多有用的功能組件,還包括多種企業級解決方案,以及相關的集成開發工具、圖書和社區支持。
當你用它來開發復雜的企業級項目的時候,你會感覺“愛不釋手”,因為不論你的團隊是什么樣子的,SOD框架總能給您提供以最簡潔的方式實現最強大的功能,保證菜鳥級的程序員可以輕松看懂使用SOD框架編寫的每一行代碼,也能讓資深程序員有一種“越野駕駛”的體驗--對數據訪問細節全方位的掌控能力!
SOD框架脫胎于PDF.NET框架,框架追求的目標就是簡單與效率的平衡,體現在代碼的精簡、開發維護的簡單與追求極致的運行效率。SOD框架目前運行在.NET平臺,但它沒有依賴于.NET框架很多獨有的特性,這使得SOD框架在理論上有可能實現跨語言平臺支持。
2,為什么需要SOD框架?
EF框架或大部分ORM框架的缺點就是SOD框架的優點, 因為SOD框架并不只是一個ORM框架,它提供了多種數據開發解決方案。
ORM框架并不能解決所有的數據開發問題,如果試圖這么做將大大增加ORM框架的復雜性和使用難度(比如用EF框架來做數據批量更新),所以這也是為什么很多開發人員更加喜歡Dapper這類“微型ORM”(或半ORM)的原因。
在企業級數據開發的時候,有時候使用其它一些手段能夠起到更好的效果,這就要求框架要支持多種開發模式,支持更精準的操縱SQL查詢,更靈活的對象關系映射(ORM),更直接的面向底層數據訪問,或者更高層面的數據抽象,需要全方位的數據開發解決方案。SOD框架擁有超過15年的項目應用歷史,相信它為你而生!
SOD框架包含SQL-MAP,ORM,DataControls三大子框架,但它卻是一個非常輕量級的框架,也是一個企業級數據應用開發的解決方案。
了解更多,請看這里。
SOD框架特別適合于以下類型的企業項目:
- 對數據操作安全有嚴格要求的金融行業;
- 對數據訪問速度、內存和CPU資源有苛刻要求的互聯網行業;
- 對需求常常變化,項目經常迭代,要求快速開發上線的項目;
- 對穩定性要求高,需要長期維護的企業級應用如MIS、ERP、MES等行業;
- 預算有限的初創企業或中小規模的開發團隊。
- 國產化項目,需要支持國產數據庫人大金倉和達夢。(國產化支持)
因為足夠簡單,所以SOD框架是少數仍然支持 .NET 2.0的框架,當然,它也支持 .NET 3.x,.NET 4.x,.Net core 以及.NET 5/6等以上框架版本 。
3,功能特性
SOD框架包括以下功能:
3.1 核心三大功能 -(S,O,D):
- SQL-MAP :
- XML SQL config and Map DAL --基于XML配置的SQL查詢和數據訪問層映射
- SQL Map Entity --SQL語句映射為實體類
- ORM :
- OQL(ORM Query Language) --ORM查詢語言:OQL
- Data Container --數據容器
- Entity Indexer --實體類索引器訪問
- Table Map route Query --分表查詢支持
- Micro ORM --微型ORM
- Data Controls :
- Consistent Data Froms --一致的數據窗體訪問技術
- WebForm Data Controls --Web窗體數據控件
- WinForm Data Controls --Windows窗體數據控件
3.2 有用的功能組件:
Hot Use Cache --熱緩存(緩存最常用的數據)
Binary Serialization --二進制序列化
Query Log --查詢日志
Command Pipeline --命令管道
Distributed Identification --分布式ID
3.3 企業級解決方案:
MVVM (Web/WinForm) --MVVM數據窗體
Memory Database --內存數據庫
Transaction Log Data Replication --事務日志數據復制
Data Synchronization --數據同步
Distributed transaction --分布式事務
OData Client --OData 客戶端
3.4 工具:
Integrated Development Tool --集成開發工具,包括實體類生成、SQL-MAP代碼自動生成和多種數據庫訪問工具。
Nuget support --Nuget 支持
4,源碼和社區:
- Code: https://github.com/znlgis/sod or https://gitee.com/znlgis/sod
- Home: http://www.pwmis.com/sqlmap
- Blog: http://www.rzrgm.cn/bluedoctor
- QQ Group:18215717,154224970
要了解更多,請看這篇文章:.NET ORM 的 “SOD蜜”--零基礎入門篇

或者參考框架作者編著的圖書:《SOD框架企業級應用數據架構實戰》,該書對SOD框架的企業級解決方案進行了詳細的介紹。。
二、快速入門
1,準備工作
在開始工作之前,先建立一個控制臺項目,然后在程序包管理控制臺,添加SOD框架的Nuget 包引用:
Install-Package PWMIS.SOD
這樣即可獲取到最新的SOD框架包并且添加引用,然后,就可以開始下面的工作了。
注意,上面是創建的是SOD框架應用程序.NET6及以上目標版本的項目,如果創建的項目.NET運行時目標是.NET 5或者.NET 4x及以下版本,添加的包名字是 PDF.NET.SOD,即:
Install-Package PDF.NET.SOD
已經建立好的當前Demo程序請看框架源碼解決方案項目中的“SODTest”項目。
1.1,配置數據連接:
SOD框架的數據訪問對象AdoHelper提供了基礎的數據訪問功能,可以直接設置連接字符串,支持設置讀寫不同的連接字符串,如下所示:
AdoHelper sqlHelper = new SqlServer();
sqlHelper.ConnectionString="Data Source=.;Initial Catalog=MyDB;Integrated Security=True";
通常我們不會把連接字符串直接寫在代碼中,而是配置文件中,SOD框架支持通過配置文件來自動切換應用程序使用的數據庫類型。
SOD框架默認使用應用程序配置文件中配置的最后一個數據連接配置。為了方便,我們在源碼項目的app.config文件中,做如下數據訪問連接配置:
<connectionStrings>
<add name="local"
connectionString="Data Source=.;Initial Catalog=MyDB;Integrated Security=True"
providerName="SqlServer"/>
<add name="local2"
connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=~/Database2.mdf;Integrated Security=True"
providerName="SqlServer" />
</connectionStrings>
providerName 是SOD框架的數據訪問提供程序,PWMIS.Core.dll內置的可選簡略名稱有:
Access | SqlServer | Oracle | SqlCe | OleDb | Odbc
如果是其它的擴展程序集,那么providerName應該寫成下面的形式:
providerName="<提供程序類全名稱>,<提供程序類所在程序集>"
比如使用SOD封裝過的Oracle官方的ADO.NET提供程序類:
providerName="PWMIS.DataProvider.Data.OracleDataAccess.Oracle,PWMIS.OracleClient"
在其它提供程序中,SOD框架提供了對【MySQL、Oracle、PostgreSQL、SQLite、達夢、人大金倉】等常見數據庫的支持(擴展程序集),只要數據庫提供了ADO.Net驅動程序,那么SOD框架經過簡單包裝即可保證支持。
在當前示例中使用的是名字為“local2”的數據連接,你可以修改成你實際的連接字符串,示例中使用的是SQLServer本地數據庫文件,源碼中已經包含了此文件,如果不能使用請重新創建一個。
1.2,實體類定義
SOD框架使用ORM功能并不需要先定義實體類,直接使用與數據表對應的接口類型即可開始查詢。
下面定義一個用于數據訪問的“用戶表”接口和對應的實體類。
public interface ITbUser
{
int ID { get; set; }
string Name { get; set; }
string LoginName { get; set; }
string Password { get; set; }
bool Sex { get; set; }
DateTime BirthDate { get; set; }
}
public class UserEntity : EntityBase, ITbUser
{
public UserEntity()
{
TableName = "TbUser";
IdentityName = "ID";
PrimaryKeys.Add("ID");
}
public int ID
{
get { return getProperty<int>("ID"); }
set { setProperty("ID", value); }
}
public string Name
{
get { return getProperty<string>("Name"); }
set { setProperty("Name", value,100); } //長度 100
}
public string LoginName
{
get { return getProperty<string>("LoginName"); }
set { setProperty("LoginName", value,50); }
}
public string Password
{
get { return getProperty<string>("Password"); }
set { setProperty("Password", value,50); }
}
public bool Sex
{
get { return getProperty<bool>("Sex"); }
set { setProperty("Sex", value); }
}
public DateTime BirthDate
{
get { return getProperty<DateTime>("BirthDate"); }
set { setProperty("BirthDate", value); }
}
}
用同樣的方式定義“訂單”接口ISimpleOrder、ISimpleOrderItem 和相應的實體類SimpleOrderEntity、SimpleOrderItemEntity。有關這幾個接口和類型的詳細定義請參見項目源碼。
元數據映射:
實體類的元數據包括映射的表名稱、主外鍵、標識字段、屬性映射的字段名稱和字段類型、長度等。
在上面定義的實體類UserEntity中,它繼承了SOD框架的實體類基類EntityBase,然后就可以在實體類的屬性定義中使用
getProperty方法和setProperty方法,這兩個方法都提供了“屬性字段名”參數,它表示當前屬性名和數據表字段名的映射關系。
在實體類構造函數中,TableName表示實體類映射的數據表名稱,IdentityName表示映射的數據表標識字段名稱,一般用于自增字段,PrimaryKeys表示實體類映射的主鍵字段,可以有多個字段來表示主鍵,例如聯合主鍵。
動態映射:
SOD框架的實體類采用“動態元數據映射”,這些元數據都是可以在程序運行時進行修改,因此它與Entity Framework等其它ORM框架的實體類映射方式有很大不同。這個特點使得實體類的定義和元數據映射可以在一個類代碼中完成,并且不依賴于.NET特性聲明。這種動態性使得SOD框架可以脫離繁瑣的數據庫表元數據映射過程,簡化數據訪問配置,并且能夠輕松的支持“分表、分庫”訪問。
邏輯映射(虛擬映射):
元數據的映射可以是“邏輯映射”,即映射一個數據表不存在的內容,例如指定要映射外鍵字段,但數據庫可以沒有物理的外鍵字段,或者指定一個虛擬的主鍵。也可以不做任何元數據映射,這樣實體類可以作為一個類似的“字典”對象來使用,或者用于UI層數據對象。
動態實體類:
約定勝于配置。如果元數據全部采用默認映射,可以大大簡化實體類的定義過程,直接采用接口類型來動態創建實體類和進行查詢訪問。
在默認映射的時候,通常將類型名稱映射為表名稱,屬性名稱映射為字段名稱。如果是接口類型,映射表名稱的時候會去掉接口名中第一個“I”字符。
例如接口類型 "IDbUser" 映射的表名稱為 "DbUser"
下面是根據接口動態創建實體類的示例:
IDbUser user = EntityBuilder.CreateEntity<IDbUser>();
1.3,定義數據上下文
注意這是一個可選步驟,如果你不使用Code First開發模式的話。
在很多ORM框架中,數據上下文對象DbContext使用都很常見,但對于SOD框架并不是必須的,SOD框架崇尚簡單直接的使用方式,有多種方式可以直接開始數據查詢。如果你需要使用Code First開發模式,由程序自動創建數據表而不是先設計數據庫表,可以使用SOD框架的數據上下文功能。
下面定義一個用于數據訪問的簡單數據上下文對象SimpleDbContext。
public class SimpleDbContext : DbContext
{
public SimpleDbContext():base("local2")
{
}
protected override bool CheckAllTableExists()
{
CheckTableExists<UserEntity>();
CheckTableExists<SimpleOrderEntity>();
//創建表以后立即創建索引
InitializeTable<SimpleOrderItemEntity>("CREATE INDEX [Idx_OrderID] On [{0}] ([OrderID])");
return true;
}
}
在上面的代碼中,SimpleDbContext使用名為“local2”的數據連接配置,它會在首次執行的時候調用CheckAllTableExists方法,檢查表是否存在,如果不存在則創建實體類對應的數據表。
可以使用InitializeTable方法在表第一次創建完成以后執行表的初始化工作,比如創建索引。
2,數據訪問輔助對象
AdoHelper對象是SOD框架的數據訪問輔助對象,它是一個抽象數據訪問對象,需要根據具體的數據庫類型提供相應的數據訪問提供程序。任何數據庫的.NET驅動程序經過簡單包裝即可成為SOD框架的數據訪問提供程序。
SOD框架內置的數據訪問提供程序類型請參考本文【1.1 配置數據連接】的內容。
AdoHelper對象提供了各種數據訪問方法,包括獲取DataSet、DataReader以及執行數據的增刪改、調用存儲過程和多級事務查詢等。
實例化一個AdoHelper對象可以通過MyDB類的多種方式來實現,也可以根據連接配置來動態創建,或者直接實例化一個SOD數據訪問提供程序。下面的例子提供了三種方式來實例化AdoHelper對象:
//根據連接配置中的“連接名字”來創建,例如"local2"
AdoHelper db1 = MyDB.GetDBHelperByConnectionName("local2");
//根據連接配置中最后一個配置來創建
AdoHelper db2 = MyDB.GetDBHelper();
//直接實例化數據訪問提供程序,例如SqlServer數據庫
AdoHelper db3= SqlServer();
下面是使用AdoHelper的簡單示例。
2.1,執行不返回值的查詢
通過調用ExecuteNonQuery方法來實現,該方法返回本次執行受影響的行數的結果值。
下面是一個創建用戶表的例子,創建的用戶表在下面的示例中會使用到:
AdoHelper db2 = AdoHelper.CreateHelper("local2");
//異常處理示例
string sql_createUser = @"
Create table [TbUser](
[ID] int identity primary key,
[Name] nvarchar(100),
[LoginName] nvarchar(50),
[Password] varchar(50),
[Sex] bit,
[BirthDate] datetime
)";
try
{
db2.ExecuteNonQuery(sql_createUser);
Console.WriteLine("表[TbUser] 創建成功!");
}
catch (PWMIS.DataProvider.Data.QueryException qe)
{
Console.WriteLine("SOD查詢錯誤,錯誤原因:{0}", qe.InnerException.Message);
}
catch (Exception ex)
{
Console.WriteLine("錯誤:{0}", ex.Message);
}
使用AdoHelper對象進行查詢的時候,如果執行查詢發生異常,使用異常處理代碼可以捕獲QueryException查詢異常對象,
在該異常對象中可以查看詳細的錯誤原因,以及執行查詢相關的一些信息,如果SQL語句、執行參數等。
2.2,參數化查詢
SOD框架認為保證數據訪問的安全是框架最重要的目標,參數化查詢是避免“SQL注入”最有效的手段。SOD框架對所有數據庫都支持參數化查詢,包括Access數據庫等。
除了最基礎的AdoHelper對象可以支持參數化查詢,SQL-MAP功能也是支持參數化查詢的。下面是使用參數化查詢插入用戶數據的例子:
string sql_insert = "INSERT INTO [TbUser] ([Name],[LoginName],[Password],[Sex],[BirthDate]) VALUES(@Name,@LoginName,@Password,@Sex,@BirthDate)";
IDataParameter[] paras = new IDataParameter[] {
db2.GetParameter("Name","張三"),
db2.GetParameter("LoginName","zhangsan"),
db2.GetParameter("Password","888888"),
db2.GetParameter("Sex",true),
db2.GetParameter("BirthDate",new DateTime(1990,2,1))
};
int rc = db2.ExecuteNonQuery(sql_insert, CommandType.Text, paras);
if (rc > 0)
Console.WriteLine("插入數據成功!用戶名:{0}", paras[0].Value);
AdoHelper對象的GetParameter方法有多個重載方法,可以滿足參數化查詢的各種需求,它返回的是當前數據庫類型的查詢參數對象,可以實現更多的參數化查詢的細節控制,例如調用存儲過程所需的查詢參數。
2.3,微型ORM
微型ORM的特點是允許直接執行SQL語句查詢,但是查詢結果可以直接映射成為POCO類型或者實體類型,
相比較以前執行查詢返回一個數據集(DataSet)而言,微型ORM即享受了直接編寫SQL執行查詢的靈活性,又得到了強類型對象使用的便利性,因此微型ORM很受開發人員歡迎。
SOD框架支持微型ORM功能,它允許你直接控制查詢返回的DataReader對象,也可以將查詢直接映射到一個強類型對象。下面的示例演示了通過微型ORM功能查詢得到用戶列表對象。
string sql_query = "SELECT [ID],[Name],[Sex],[BirthDate] FROM [TbUser] WHERE [LoginName]={0}";
var mapUsers = db2.ExecuteMapper(sql_query, "zhangsan")
.MapToList(reader => new
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
Sex = reader.GetBoolean(2),
BirthDate = reader.GetDateTime(3)
});
// 方式二
var userList = db2.QueryList<UserInfo>(sql_query, "zhangsan");
上面的示例演示了兩者結果映射方式,使用AdoHelper對象的MapToList方法可以直接操作返回的DataReader對象,根據SQL語句中的字段順序定制讀取查詢結果字段值,這種方式由于是面向底層Ado.NET的操作,因此查詢具有很高的性能。
另外一種方式就是使用AdoHelper對象的QueryList方法,它直接將SQL查詢結果映射為一個POCO對象。
class User
{
public int ID{get;set;}
public string Name{get;set;}
public bool Sex{get;set;}
public DateTime Birthday{get;set;}
}
//
string sql_query = "SELECT [ID],[Name],[Sex],[BirthDate] FROM [TbUser] WHERE [LoginName]={0}";
var userList= db2.QueryList<User>(sql_query,"zhangsan");
上面的示例還演示了SOD的微型ORM查詢使用的“參數化查詢”方式,相比較于直接的參數化查詢,這里只需要使用參數的占位符來表示參數,就像Console.WriteLine()方法使用的參數一樣。例如上面的示例中查詢的參數是字段LoginName對于的查詢參數,參數值是“zhangsan”。
3,SQL-MAP簡介
3.1,參數化查詢的難題
參數化查詢的SQL語句可以被數據庫編譯執行從而提高查詢效率;參數化查詢是在數據庫完成 SQL 指令的編譯后,才套用參數運行,因此就算參數中含有危害數據庫的指令,也不會被數據庫所運行,因此參數化查詢提高了SQL執行的安全性。
雖然參數化查詢有很多優點,但缺點是在開發上會增加代碼編寫量,另外由于參數化查詢的SQL語句缺乏統一的標準,也會使得采用參數化查詢的代碼難以在不同數據庫平臺之間移植。請看下面的示例:
--Microsoft SQL Server
INSERT INTO myTable (c1, c2, c3, c4) VALUES (@c1, @c2, @c3, @c4)
--Microsoft Access
UPDATE myTable SET c1 = ?, c2 = ?, c3 = ? WHERE c4 = ?
--MySQL
UPDATE myTable SET c1 = ?c1, c2 = ?c2, c3 = ?c3 WHERE c4 = ?c4
--Oracle
UPDATE myTable SET c1 = :c1, c2 = :c2, c3 = :c3 WHERE c4 = :c4
--PostgreSQL
UPDATE myTable SET c1 = $1, c2 = $2, c3 = $3 WHERE c4 = $4
從上面不同數據庫的SQL參數化查詢的示例可以看到,不同數據庫支持的參數查詢的參數名寫法是不同的,要求在參數名前面加不同的前綴符號(@/???/$)。
在ADO.NET中,采用各數據庫的OleDB或者ODBC驅動程序,都要求使用 ?符號表示參數。
3.2,抽象SQL參數化查詢
對于參數化查詢SQL語句寫法不統一的問題,只需要再抽象出一種參數化查詢的方式即可解決這個問題。在SOD框架中,對參數的定義統一采用##來處理,具體格式如下:
#參數名字[:參數類型],[數據類型],[參數長度],[參數輸出輸入類型]#
上面定義當中,中括號里面的內容都是可選的。參數定義的詳細內容,請參看PDF.NET(PWMIS數據開發框架)之SQL-MAP目標和規范
采用抽象SQL參數查詢,根據參數定義規范,上面對于myTable的更新語句可以寫成下面的樣子:
UPDATE myTable SET
c1 = #c1#,
c2 = #c2:String#,
c3 = #c3:String,Sring,50#
WHERE c4 = #c4:Int32#
如果不指定參數的類型,默認為String類型,例如c1參數。
程序在運行時,會根據當前具體的數據庫訪問程序實例,將##內部的參數替換成合適的參數內容。
3.3,抽象SQL查詢:SQL-MAP技術
我們將SQL中的參數“抽象化”了,我們還可以進一步抽象整個SQL,看下面的抽象過程:
1, 編寫任意形式的合法SQL查詢語句;
2, 抽象SQL中的參數;
3, 將整個SQL語句抽象成一個唯一名字為CommandName;
4, 將一組CommandName映射到一個DAL類文件;
5, 將這個CommandName映射到一個DAL類的方法名稱;
6, 將SQL語句中的參數名稱映射到該DAL類的當前方法中的參數名稱;
7, 將整個SQL腳本文件映射到一個DAL程序集。
這個思想,就是SQL-MAP,將SQL語句映射為程序!
下面,我們用一個學生成績管理的例子作為SQL-MAP的SQL語句寫法的示例:
<CommandClass Name ="ScoreManagement" Description ="成績管理" Class ="" >
<Select CommandName="GetStudent" CommandType="Text" Description="查詢所屬系的學生信息" Method="" ResultClass="DataSet">
<![CDATA[
select * from Student where deptID=#DID:Int32#
]]>
</Select>
<Insert CommandName="InsertStudent" CommandType="Text" Description="增加學生" Method="AddStudent" >
insert into [Student](stuName,deptID) values(#Name:String#,#DeptId:Int32#)
</Insert>
</CommandClass>
將上面的XML保存為一個名字叫做SqlMap.config的配置文件,該配置文件的詳細內容請參考SOD框架源碼解決方案里面SqlMapDemo項目中的代碼。
在上面的SQL-MAP配置文件中,CommandClass節點對應于程序的DAL層查詢類,一個查詢類下面有多種查詢類型,包括Select/Update/Inset/Delete這四種SQL查詢類型,分別對應于
CommandClass節點下面的Select/Update/Insert/Delete類型,如上所述,Select節點有一個命令名為GetStudent,Insert節點有一個命令名為InsertStudent,
這些命令名字分別對應于ScoreManagement類下面的方法。
3.4,SQL查詢和代碼映射
下面看一下這個SQL-MAP配置映射的DAL(數據訪問層)類的具體代碼:
using System;
using System.Data;
using System.Collections.Generic;
using PWMIS.DataMap.SqlMap;
using PWMIS.DataMap.Entity;
using PWMIS.Common;
namespace SqlMapDemo.SqlMapDAL
{
public partial class ScoreManagement
: DBMapper
{
public ScoreManagement()
{
Mapper.CommandClassName = "ScoreManagement";
Mapper.EmbedAssemblySource="SqlMapDemo,SqlMapDemo.SqlMap.config";
}
/// <summary>
/// 查詢所屬系的學生信息
/// </summary>
/// <param name="DID"></param>
/// <returns></returns>
public DataSet GetStudent(Int32 DID )
{
//獲取命令信息
CommandInfo cmdInfo=Mapper.GetCommandInfo("GetStudent");
//參數賦值,推薦使用該種方式;
cmdInfo.DataParameters[0].Value = DID;
//參數賦值,使用命名方式;
//cmdInfo.SetParameterValue("@DID", DID);
//執行查詢
return CurrentDataBase.ExecuteDataSet(CurrentDataBase.ConnectionString, cmdInfo.CommandType, cmdInfo.CommandText , cmdInfo.DataParameters);
//
}//End Function
/// <summary>
/// 增加學生
/// </summary>
/// <param name="Name"></param>
/// <param name="DeptId"></param>
/// <returns></returns>
public Int32 AddStudent(String Name , Int32 DeptId )
{
//獲取命令信息
CommandInfo cmdInfo=Mapper.GetCommandInfo("InsertStudent");
//參數賦值,推薦使用該種方式;
cmdInfo.DataParameters[0].Value = Name;
cmdInfo.DataParameters[1].Value = DeptId;
//參數賦值,使用命名方式;
//cmdInfo.SetParameterValue("@Name", Name);
//cmdInfo.SetParameterValue("@DeptId", DeptId);
//執行查詢
return CurrentDataBase.ExecuteNonQuery(CurrentDataBase.ConnectionString, cmdInfo.CommandType, cmdInfo.CommandText , cmdInfo.DataParameters);
//
}//End Function
}//End Class
}//End NameSpace
上面的SqlMapDal代碼完整的映射了SQL-MAP配置文件的內容,實現了SQL語句抽象到程序抽象的過程,因此理論上只要會寫SQL語句即可寫DAL查詢程序,
這個SqlMapDal代碼文件可以通過SOD框架的集成開發工具自動生成,每次修改了SQL-MAP配置文件,可以通過工具“一鍵生成DAL代碼”,極大的增加了DAL層開發的靈活性和開發效率。
4,ORM查詢簡介
SOD框架的ORM功能支持普通的基于實體類的CRUD功能,還有框架獨特的ORM查詢語言--OQL,其中OQL又分為面向單表操作的泛型OQL(GOQL)和支持多表復雜查詢的OQL。
4.1,ORM查詢語言--OQL
使用ORM必然會提到實體類的話題,實體類實現了程序對象和數據表結構的映射。在很多ORM框架的實現中,實體類又分為“充血實體類”和“貧血實體類”兩種,后者一般就是簡單的POCO類型,而前者需要復雜的定義以支持實體類保持數據的狀態,以便生成正確的SQL語句;對于前者必須使用另外的方式在代碼編譯期間生成SQL語句。
ORM本來是完成“對象-關系映射”的,但這里大多數的ORM都包含了“生成SQL”的功能,而要實現SQL那樣的靈活性,那么我們必須分離出ORM這個關注點,將“生成SQL”的功能從ORM中抽取出來,這樣我們就能夠有更多的精力致力于發明一個面向對象的,用于ORM查詢的語言,(ORM Query Language) ,這就是OQL。
下面是使用OQL的最簡形式:
//查詢所有用戶數據
var userList= OQL.From<UserEntity>().ToList();
ORM查詢語言,其實早就有了,從早期的Hibernate的HQL,到MS的Linq(Linq2SQL,EF其實內部都是使用Linq生成的SQL),它們都可以生成復雜的SQL語句,它們都是直接作用于ORM框架的。幾乎在與Linq同一時期,SOD框架也發明了自己的ORM查詢語言,稱為OQL。下面提到的OQL,都是指的SOD框架的OQL。下面是使用OQL的一個經典示例:
UserEntity u = new UserEntity();
u.FirstName = "zhang";
//OQL表達式示例:
var q = OQL.From(u)
.Select(u.ID,u.FirstName,u.LastName)
.Where(u.FirstName)
.OrderBy(u.ID)
.END;
var userList = EntityQuery<UserEntity>.QueryList(q);
上面的查詢構建了一個OQL表達式,用來查詢用戶表中所有姓”zhang“的用戶,并且僅查詢用戶表的三個字段的內容。可以看到編寫這個OQL表達式跟編寫SQL查詢語句非常相似,幾乎沒有使用門檻,體現了SOD框架簡單易用的特點。EntityQuery泛型對象的QueryList方法執行OQL對象進行查詢,默認情況下使用App.config的最后一個連接配置作為AdoHelper查詢對象,例如本篇文章的db2對象。也可以通過該方法的重載方法傳入指定的AdoHelper對象。
有關OQL的由來以及OQL的詳細語法和使用示例,請參考以下幾篇文章:
4.2,GOQL示例
GOQL適合單表查詢,可以僅定義一個對應數據表的接口類型來進行GOQL查詢,下面的例子使用ITbUser接口來查詢用戶數據:
//GOQL簡單示例
//GOQL使用接口類型進行查詢
var goql = OQL.FromObject<ITbUser>()
.Select()
.Where((cmp, obj) => cmp.Comparer(obj.LoginName, "=", "zhangsan"))
.END;
var list1 = goql.ToList();
//GOQL使用實體類類型進行查詢
var list11 = OQL.FromObject<UserEntity2>()
.Select()
.Where((cmp, obj) => cmp.Comparer(obj.LoginName, "=", "zhangsan"))
.END
.ToList();
//GOQL復雜示例
var list2 = OQL.FromObject<ITbUser>()
.Select(s => new object[] { s.ID, s.Name, s.Sex, s.BirthDate }) //選取指定字段屬性查詢
.Where((cmp, obj) => cmp.Property(obj.LoginName) == "zhangsan") //使用操作符重載的條件比較
.OrderBy((order, obj) => order.Desc(obj.ID))
.ToList();
OQL表達式總是以From方法開始,以END屬性結束,通過OQL的鏈式語法,Select、Where、OrderBy方法總是以固定的順序出現,因此無任何SQL編寫經驗的人也可以通過OQL寫出正確的查詢。
GOQL對象通過調用OQL的FormObject泛型方法得到,之后的語法跟OQL一樣,最后通過GOQL的ToList方法執行查詢得到結果。
ToList方法有重載,可以用一個AdoHelper對象做參數來對指定的數據庫進行查詢。
GOQL的復雜查詢支持通過Select方法指定要查詢的實體類屬性字段,也可以在Where、OrderBy方法上使用Lambda表達式購置查詢和排序條件。
Where方法上使用了OQLCompare對象來生成查詢條件對象,它有多種條件比較方法和方法重載,也支持操作符重載。
4.3,OQL示例
GOQL實際上式OQL的簡化形式,所以OQL可以實現更強大的查詢方式,除了支持單表/單實體類查詢,也支持多表/多實體類查詢。
OQL對象的Form方法需要一個或者多個實體類對象實例作為參數,因此在構造OQL表達式的時候可以訪問當前實體類對象的屬性從而簡化條件表達式的構造。
下面是用戶登錄查詢的示例,在數據庫中查詢是否有匹配的登錄名和密碼的記錄:
//OQL查詢示例
UserEntity ue = new UserEntity();
ue.LoginName = "zhangsan";
ue.Password = "888888";
//OQL簡單查詢示例
var oql = OQL.From(ue)
.Select()
.Where(ue.LoginName, ue.Password)
.END;
var userObj = EntityQuery<UserEntity>.QueryObject(oql, db2);
上面的查詢中直接使用了UserEntity對象的LoginName、Password屬性的值作為查詢條件值,如果使用GOQL,上面的查詢等價于下面的查詢方式:
var userObj2 = OQL.FromObject<UserEntity>()
.Select()
.Where((cmp, obj) => cmp.Comparer(obj.LoginName, "=", "zhangsan") &
cmp.Comparer(obj.Password, "=", "888888"))
.END
.ToObject(db2);
var userObj3 = OQL.FromObject<UserEntity>()
.Select()
.Where((cmp, obj) => cmp.Property(obj.LoginName)== "zhangsan" &
cmp.Property(obj.Password) == "888888")
.END
.ToObject(db2);
在上面的查詢中,Where方法里面使用了兩個比較條件,OQL使用&符號表示SQL的AND條件,使用|符號表示SQL的OR條件。
這個功能使用了C#語言的操作符重載功能來實現的,OQL還支持>,<,==,!=,>=,<= 這些操作符重載,操作符重載功能的支持使得OQL表達式編寫更簡單直觀。
除了上面介紹的OQL簡單查詢,OQL也支持復雜查詢,請看下面的示例:
//OQL復雜查詢示例
var oql2 = OQL.From(ue)
.Select( ue.ID, ue.Name, ue.Sex, ue.BirthDate )
.Where(cmp => cmp.Property(ue.LoginName) == "zhangsan" & cmp.EqualValue(ue.Password))
.OrderBy(order => order.Desc(ue.ID))
.END;
oql2.Limit(5, 1);
var list4 = EntityQuery<UserEntity>.QueryList(oql2, db2);
實際上這個查詢并不算復雜,OQL還支持主子表查詢、多表查詢,由于篇幅原因不在這里做更多介紹。
OQL除了從數據庫查詢數據到實體類對象,還支持批量更新操作,例如更新所有用戶的密碼為“8888”:
UserEntity ue= new UserEntity();
ue.Password = "8888";
var oql_update = OQL.From(ue).Update(ue.Password).END;
var ru = EntityQuery<UserEntity>.ExecuteOql(oql_update, db2);
4.4,增刪改數據
由于SOD框架采用的是充血實體類,實體類可以記錄前后操作的數據狀態,使得編寫增刪改功能的代碼很簡單,
如下面的對用戶數據的增刪改過程:
UserEntity ue2 = new UserEntity();
ue2.LoginName = "lisi";
ue2.Password = "8888";
ue2.Name = "李四";
int ic= EntityQuery<UserEntity>.Instance.Insert(ue2, db2);
if(ic>0)
Console.WriteLine("添加數據成功,用戶ID={0}", ue2.ID);
ue2.BirthDate = new DateTime(1990, 1, 2);
ue2.Sex = false ;
int uc=EntityQuery<UserEntity>.Instance.Update(ue2, db2);
if(uc>0)
Console.WriteLine("修改數據成功,用戶ID={0}", ue2.ID);
ue2.PrimaryKeys.Clear();
ue2.PrimaryKeys.Add("LoginName");
int dc= EntityQuery<UserEntity>.Instance.Delete(ue2, db2);
if (dc > 0)
Console.WriteLine("刪除用戶[{0}]成功!",ue2.LoginName);
在用戶表中ID字段是標識字段,因此插入數據的時候框架會忽略標識字段的值,但是插入數據成功后實體類中標識字段對應的屬性會自動獲取剛才的自增字段值。
如上面的例子,當ue2對象插入成功后,ue2.ID就獲得了剛剛插入的自增字段值。同時,ue2對象的ID屬性也是實體類的主鍵字段屬性,
這樣當實體類對象修改后,可以調用EntityQuery實例對象的Update方法更新數據。
SOD框架實體類的元數據是動態元數據,實體類的主鍵字段是虛擬的,并不需要跟數據表的主鍵一一對應,所以程序可以在運行時修改實體類的主鍵字段。
如上述示例程序,ue2對象清除了自己的主鍵字段,添加了用戶登錄名字段,之后成功刪除了前面添加的數據。
除了采用實體類的方式類增刪改數據,SOD框架還支持批量增刪改數據,使用OQL語句即可實現這個功能,可以參考前面OQL的示例。
5,示例:保存和查詢訂單數據
前面介紹了SOD框架ORM操作的一些基礎知識,實際上,框架提供了至少8種查詢方式,詳細內容,請看.NET ORM 的 “SOD蜜”--零基礎入門篇
為了更好的展示SOD框架在項目上的實際應用,下面使用常見的訂單操作來說明實體類定義和ORM查詢更多的細節。
5.1,訂單業務類設計
訂單類一般需要包含用戶信息、訂單名、訂單價格、商品清單、收貨地址等信息,為了簡化示例,我們設計一個簡單訂單類SimpleOrder,它僅僅包含訂單的基礎信息和商品清單,
所以訂單業務類的設計包含一個訂單類和商品清單類,為統一訂單實體類的設計,這里先設計訂單接口類型。
public interface ISimpleOrder
{
long OrderID { get; set; }
string OrderName { get; set; }
int UserID { get; set; }
DateTime OrderDate { get; set; }
double OrderPrice { get; set; }
ISimpleOrderItem[] OrderItems { get; }
}
public interface ISimpleOrderItem
{
string GoodsID { get; set; }
string GoodsName { get; set; }
int Number { get; set; }
double UnitPrice { get; set; }
}
然后設計訂單業務類:
public class SimpleOrder : ISimpleOrder
{
public long OrderID { get; set; }
public string OrderName { get; set; }
public int UserID { get; set; }
public DateTime OrderDate { get; set; }
public double OrderPrice { get; set; }
public ISimpleOrderItem[] OrderItems { get; set; }
}
public class SimpleOrderItem: ISimpleOrderItem
{
public string GoodsID { get; set; }
public string GoodsName { get; set; }
public int Number { get; set; }
public double UnitPrice { get; set; }
}
SimpleOrder包含一個商品清單OrderItems,它是ISimpleOrderItem[]類型,SimpleOrderItem繼承了ISimpleOrderItem接口。SimpleOrder的子類型使用接口類型來定義,方便訂單實體類的子類型能繼承同樣的接口。
5.2,訂單實體類設計
訂單實體類SimpleOrderEntity同SimpleOrder業務類一樣,都繼承了ISimpleOrder接口:
public class SimpleOrderEntity :EntityBase, ISimpleOrder
{
public SimpleOrderEntity()
{
TableName = "TbOrders";
PrimaryKeys.Add(nameof(OrderID));
}
public long OrderID
{
get { return getProperty<long>(nameof(OrderID)); }
set { setProperty(nameof(OrderID), value); }
}
public string OrderName
{
get { return getProperty<string>(nameof(OrderName)); }
set { setProperty(nameof(OrderName), value, 100); } //長度 100
}
public int UserID
{
get { return getProperty<int>(nameof(UserID)); }
set { setProperty(nameof(UserID), value); }
}
public DateTime OrderDate
{
get { return getProperty<DateTime>(nameof(OrderDate)); }
set { setProperty(nameof(OrderDate), value); }
}
public double OrderPrice
{
get { return getProperty<double>(nameof(OrderPrice)); }
set { setProperty(nameof(OrderPrice), value); }
}
public ISimpleOrderItem[] OrderItems
{
get {
return this.OrderItemEntities.ToArray();
}
}
public List<SimpleOrderItemEntity> OrderItemEntities { get; set; }
}
public class SimpleOrderItemEntity : EntityBase, ISimpleOrderItem
{
public SimpleOrderItemEntity()
{
TableName = "TbOrderItems";
IdentityName = nameof(ID);
PrimaryKeys.Add(nameof(ID));
SetForeignKey<SimpleOrderEntity>(nameof(OrderID));
}
public long ID
{
get { return getProperty<long>(nameof(ID)); }
set { setProperty(nameof(ID), value); }
}
public long OrderID
{
get { return getProperty<long>(nameof(OrderID)); }
set { setProperty(nameof(OrderID), value); }
}
public string GoodsID
{
get { return getProperty<string>(nameof(GoodsID)); }
set { setProperty(nameof(GoodsID), value, 100); } //長度 100
}
public string GoodsName
{
get { return getProperty<string>(nameof(GoodsName)); }
set { setProperty(nameof(GoodsName), value, 100); } //長度 100
}
public int Number
{
get { return getProperty<int>(nameof(Number)); }
set { setProperty(nameof(Number), value); }
}
public double UnitPrice
{
get { return getProperty<double>(nameof(UnitPrice)); }
set { setProperty(nameof(UnitPrice), value); }
}
}
在訂單實體類的定義中,使用了nameof語句,這樣更改屬性名維持屬性名和數據表字段名的一致性就很方便了。
另外,在訂單實體類SimpleOrderEntity 多定義了一個屬性OrderItemEntities:
public List
OrderItemEntities
它并不是訂單接口ISimpleOrder要求定義的屬性,設計OrderItemEntities的目的是為了讓框架識別這是一個“子實體類”屬性,在需要到時候可以將子實體類一并查詢出來。
5.3,創建和保存訂單
在實際的項目開發中,為了區分業務層和持久層代碼,需要定義創建訂單和保存訂單的接口,并且需要在業務對象和實體類對象之間進行數據映射。為簡化示例,這里創建和保存訂單都直接操作訂單實體類。
創建一個訂單,它的商品清單包含兩個商品對象:
SimpleOrderEntity order1 = new SimpleOrderEntity();
order1.OrderID = CommonUtil.NewSequenceGUID();
order1.OrderName = "筆記本訂單_某想XL型號_" + DateTime.Now.ToString("yyyyMMdd");
order1.UserID = 1;
order1.OrderDate = DateTime.Now;
order1.OrderPrice = 5000;
var orderItems = new SimpleOrderItemEntity[] {
new SimpleOrderItemEntity()
{
OrderID = order1.OrderID,
GoodsID = "123456_7890_abc",
GoodsName = "某想XL型號",
UnitPrice = 4500,
Number = 1
},
new SimpleOrderItemEntity()
{
OrderID = order1.OrderID,
GoodsID = "1526656_7670_bcd",
GoodsName = "藍牙鍵盤",
UnitPrice = 500,
Number = 1
}
};
然后保存訂單,由于保存訂單實際上需要同時保存訂單清單實體類對象,所以需要使用事務來保存數據:
//自動創建表
SimpleDbContext db2_ctx = new SimpleDbContext();
//使用事務添加實體對象
//插入訂單
bool addResult = db2_ctx.Transaction(ctx =>
{
ctx.Add(order1);
ctx.AddList(orderItems);
}, out string errorMessage);
if (addResult)
Console.WriteLine("保存訂單信息成功!");
else
Console.WriteLine("保存訂單信息失敗,原因:{0}", errorMessage);
上面的代碼使用了DbContext對象的事務方法來保存訂單數據,也可以直接使用AdoHelper對象的事務方法來操作,但需要自己處理數據訪問可能發生的異常。
5.4,更新訂單
更新操作可以使用DbContext方式,也可以使用EntityQuery方式,下面以更新訂單的價格作為示例:
//方式一:使用DbContext方式
order1.OrderPrice = 4999;
int ur1= db2_ctx.Update(order1);
if (ur1 > 0)
Console.WriteLine("訂單價格更新成功,點單號:{0},價格:{1}",order1.OrderID,order1.OrderPrice);
//方式二:使用EntityQuery方式
order1.OrderPrice = 4998;
int ur2 = EntityQuery<SimpleOrderEntity>.Instance.Update(order1, db2);
if (ur2 > 0)
Console.WriteLine("訂單價格更新成功,點單號:{0},價格:{1}", order1.OrderID, order1.OrderPrice);
5.4,查詢指定用戶的訂單
查詢指定用戶的訂單,包括訂單中的商品明細:
SimpleOrderEntity order2 = new SimpleOrderEntity() { UserID = 1 };
var oql_order= OQL.From(order2)
.Select()
.Where(order2.UserID)
.END;
var list_order = EntityQuery<SimpleOrderEntity>.QueryListWithChild(oql_order, db2);
上面示例代碼使用了EntityQuery類的QueryListWithChild方法來查詢記錄。注意使用該方法查詢“主-子實體類”的時候,子實體類需要指定外鍵屬性。
例如在SimpleOrderItemEntity的定義中,使用SetForeignKey 方法定義了關聯的主實體類的屬性OrderID:
SetForeignKey
(nameof(OrderID));
其中SimpleOrderEntity類型是SimpleOrderItemEntity的父類型,nameof(OrderID)對應的是SimpleOrderItemEntity類型的屬性字段OrderID.
5.5,查詢訂單用戶信息
查詢最近10條購買了筆記本的訂單用戶記錄,包括用戶年齡、性別數據。該查詢需要關聯訂單表和用戶表數據進行聯合查詢:
SimpleOrderEntity soe = new SimpleOrderEntity();
UserEntity ue = new UserEntity();
var oql_OrderUser = OQL.From(soe)
.InnerJoin(ue).On(soe.UserID, ue.ID)
.Select()
.Where(cmp => cmp.Comparer(soe.OrderName, "like", "筆記本訂單%"))
.OrderBy(soe.OrderID,"desc")
.END;
oql_OrderUser.Limit(10);
EntityContainer ec = new EntityContainer(oql_OrderUser, db2);
var listView = ec.MapToList(() => new {
soe.OrderID,
soe.OrderName,
OrderPrice =soe.OrderPrice.ToString("#0.00")+"¥",
UserID =ue.ID,
UserName =ue.Name,
Sex =ue.Sex?"男":"女",
UserAge =DateTime.Now.Year- ue.BirthDate.Year,
soe.OrderDate
});
上面的關聯查詢使用了OQL的InnerJoin查詢,由于關聯查詢一般都需要將結果映射到一個新的結果類型,所以OQL的Select方法對于查詢字段的選擇延遲到EntityContainer對象的MapToList方法里面進行,
MapToList方法可以將結果映射到一個已知的類型,包括實體類類型,也可以直接映射到一個匿名類型,如上面的示例。
5.6,列表動態排序
當頁面數據以表格形式呈現給用戶后,為方便用戶查看數據,一般都允許用戶對某些列進行自定義的排序,后端程序接收用戶在前端的排序操作,根據用戶選擇的排序字段重新組織查詢。
假設用戶正在查看訂單用戶信息列表頁面,用戶希望該列表以訂單下單日期排序或者訂單價格排序。在本例中,下單日期排序基本上就是訂單表的自增字段排序了,所以本例的問題是按照OrderID字段排序還是OrderDate排序。下面是修改后的OQL語句寫法示例:
string orderField="OrderPrice"; //"OrderDate","OrderID"
SimpleOrderEntity soe = new SimpleOrderEntity();
UserEntity ue = new UserEntity();
var oql_OrderUser = OQL.From(soe)
.InnerJoin(ue).On(soe.UserID, ue.ID)
.Select()
.Where(cmp => cmp.Comparer(soe.OrderName, "like", "筆記本訂單%"))
.OrderBy(soe[orderField],"desc")
//等價于 .OrderBy(soe.OrderPrice,"desc")
.END;
這里實現動態排序的關鍵是利用了實體類的索引器屬性,調用soe["OrderPrice"] 與調用 soe.OrderPrice 結果基本是一樣的。同樣的道理,在Where方法的條件比較中,也可以利用實體類索引器調用的方式,實現動態條件字段查詢。如果是其它ORM框架要實現動態查詢和動態排序代碼編寫是比較復雜的,沒法做到SOD框架這么簡單。
6,數據窗體
SOD框架的數據窗體開發技術基于一套數據控件接口,實現了WebForm/WinForm/WPF的窗體數據綁定、數據收集與自動更新技術,實現了窗體表單數據的填充、收集、清除,和直接到數據庫的CRUD功能。
框架對于WebForm/WinForm常用的表單控件CheckBox、DropDownList、Label、ListBox、RadioButton、TextBox都實現了 IDataControl接口,通過這個接口可以非常方便的進行表單數據開發,在WebForm/WinForm下有一致的開發體驗。
有關SOD框架的數據窗體開發技術,請看作者下面兩篇博客文章:
更多詳細的內容介紹,請看框架作者的圖書《SOD框架“企業級”應用數據架構實戰 》書中 【第五章 數據窗體開發】 的內容。
三、其它
Thank you for your donation
歡迎您捐助本項目,捐贈地址:框架官網
頁面編輯時間:2023.7.9
浙公網安備 33010602011771號