AppBox 是基於 FineUI 的通用權限管理框架,包括用戶管理、職稱管理、部門管理、角色管理、角色權限管理等模塊。
從Subsonic到Entity Framework
Subsonic最早發布於2008年,當時他的無代碼生成模式吸引了很多人的眼球,ActiveRecord模式的支持也是Subsonic迅速流行的原因之一。Subsonic也曾經一度被認為是NHibernate的有力競爭對手。可惜在2009年左右Subsonic的作者Rob Conery被微軟挖去做Asp.net MVC之后,Subsonic實際上已經死去,雖然后來Subsonic 3.0的CodingHorror也試圖東山再起,但還是由於性能原因以及各個競爭對手的沖擊而逐漸沒落。
不過高手的確是高手,Rob Conery在2011年發表的一篇文章《Massive: 400 Lines of Data Access Happiness》出其不意地掀起了一陣Micro-ORM的熱潮,隨后出現了更多的微型ORM框架,比較著名的有PetaPoco,Dapper,ServiceStack.OrmLite,Simple.Data。我也曾經試用過ServiceStack.OrmLite,對他的易用性贊不絕口,特別是對其通過代碼完全控制數據庫的創建和操作的方式印象深刻,如下所示。
class Note
{
[AutoIncrement] // Creates Auto primary key
public int Id { get; set; }
public string NoteText { get; set; }
public DateTime? LastUpdated { get; set; }
}
static void Main(string[] args)
{
//Using Sqlite DB
var dbFactory = new OrmLiteConnectionFactory(
SqliteFileDb, false, SqliteDialect.Provider);
using (var db = dbFactory.Open()) {
db.CreateTableIfNotExists<Note>();
// Insert
db.Insert(
new Note {
SchemaUri = "tcm:0-0-0",
NoteText = "Hello world 5",
LastUpdated = new DateTime(2013, 1, 5)
});
// Read
var notes = db.Where<Note>(new { SchemaUri = "tcm:0-0-0" });
foreach (Note note in notes)
{
Console.WriteLine("note id=" + note.Id + "noteText=" + note.NoteText);
}
}
Console.ReadLine();
}
注:上面示例代碼來自博客。
但最終還是因為ServiceStack.OrmLite相關資料太少,對關聯表的支持不夠而放棄。
===================
題外話:我非常欣賞ServiceStack.OrmLite的地方還有他對類和表的處理方式,將復雜類型按照 JSV 的格式存儲在一個文本字段中。
JSV Format (i.e. JSON-like Separated Values) is a JSON inspired format that uses CSV-style escaping for the least overhead and optimal performance.
JSV:類似JSON,但是采用的是CSV風格。這樣做不僅可以減少存儲空間,而且加快了讀取和寫入速度(官方聲稱JSV的讀寫速度是JSON讀寫速度的 5.3 倍)。
===================
其實ServiceStack.OrmLite的代碼和Entity Framework的Code First代碼非常類似,AppBox之所以最終采用Entity Framework的Code First,除了官方支持、資料多(這一點非常重要,方便遇到問題時解決)外,最重要的是簡潔易懂,這也是FineUI所追求的目標。所以使用FineUI做前端展現,EntityFramework(CodeFirst)做后端數據操作,簡直就是絕配。
Entity Framework官方資料:http://msdn.microsoft.com/en-us/data/ee712907
Entity Framework遇到問題時搜索:http://stackoverflow.com/questions/tagged/entity-framework
使用Subsonic和Entity Framework的代碼對比
Entity Framework不僅減少了代碼量,而且結構更加清晰,下面對加載單個用戶數據的代碼進行簡單的對比。
Subsonic:
int id = GetQueryIntValue("id");
XUser current = XUser.FetchByID(id);
if (current == null)
{
// 參數錯誤,首先彈出Alert對話框然后關閉彈出窗口
Alert.Show("參數錯誤!", String.Empty, ActiveWindow.GetHideReference());
return;
}
labName.Text = current.Name;
labRealName.Text = current.ChineseName;
labEmail.Text = current.CompanyEmail;
labPersonalEmail.Text = current.PersonalEmail;
labCellPhone.Text = current.CellPhone;
labOfficePhone.Text = current.OfficePhone;
labOfficePhoneExt.Text = current.OfficePhoneExt;
labHomePhone.Text = current.HomePhone;
labRemark.Text = current.Remark;
labEnabled.Text = current.Enabled ? "啟用" : "禁用";
labGender.Text = current.Gender;
// 表關聯查詢用戶所屬的角色列表
XRoleCollection roles = new Select().From(XRole.Schema)
.InnerJoin(XRoleUser.RoleIdColumn, XRole.IdColumn)
.Where(XRoleUser.UserIdColumn).IsEqualTo(current.Id)
.ExecuteAsCollection<XRoleCollection>();
StringBuilder sb = new StringBuilder();
foreach (XRole role in roles)
{
sb.AppendFormat("{0},", role.Name);
}
labRole.Text = sb.ToString().TrimEnd(',');
// 初始化職稱列表的選擇項
XJobTitleCollection jobs = new Select().From(XJobTitle.Schema)
.InnerJoin(XJobTitleUser.JobTitleIdColumn, XJobTitle.IdColumn)
.Where(XJobTitleUser.UserIdColumn).IsEqualTo(current.Id)
.ExecuteAsCollection<XJobTitleCollection>();
sb = new StringBuilder();
foreach (XJobTitle job in jobs)
{
sb.AppendFormat("{0},", job.Name);
}
labJobTitle.Text = sb.ToString().TrimEnd(',');
// 所屬部門
// 初始化角色復選框列表的選擇項
XDeptCollection depts = new Select().From(XDept.Schema)
.InnerJoin(XDeptUser.DeptIdColumn, XDept.IdColumn)
.Where(XDeptUser.UserIdColumn).IsEqualTo(current.Id)
.ExecuteAsCollection<XDeptCollection>();
if (depts.Count > 0)
{
labDept.Text = depts[0].Name;
}
Entity Framework:
int id = GetQueryIntValue("id");
User current = DB.Users
.Include(u => u.Roles)
.Include(u => u.Dept)
.Include(u => u.Titles)
.Where(u => u.UserID == id).FirstOrDefault();
if (current == null)
{
// 參數錯誤,首先彈出Alert對話框然后關閉彈出窗口
Alert.Show("參數錯誤!", String.Empty, ActiveWindow.GetHideReference());
return;
}
labName.Text = current.Name;
labRealName.Text = current.ChineseName;
labCompanyEmail.Text = current.CompanyEmail;
labEmail.Text = current.Email;
labCellPhone.Text = current.CellPhone;
labOfficePhone.Text = current.OfficePhone;
labOfficePhoneExt.Text = current.OfficePhoneExt;
labHomePhone.Text = current.HomePhone;
labRemark.Text = current.Remark;
labEnabled.Text = current.Enabled ? "啟用" : "禁用";
labGender.Text = current.Gender;
// 用戶所屬角色
labRole.Text = String.Join(",", current.Roles.Select(r => r.Name).ToArray());
// 用戶的職稱列表
labTitle.Text = String.Join(",", current.Titles.Select(t => t.Name).ToArray());
// 用戶所屬的部門
if (current.Dept != null)
{
labDept.Text = current.Dept.Name;
}
對比:
使用Subsonic加載單個用戶的數據需要進行 4 次數據庫查詢,總代碼量達到 61 行。
使用Entity Framework加載單個用戶的數據需要進行 1 次數據庫查詢,總代碼量減少為 36 行,並且結構更加清晰易懂,是不是很心動。
使用Entity Framework的准備工作
1. 使用Visual Studio 2012
雖說Visual Studio 2012不是必須的,你完全可以在VS2010中完成全部編碼工作。但是VS2012包含LocalDB數據庫,並且所有的官方示例都是基於VS2012的,所以使用VS2012能夠幫助新手快速入門。
並且VS2012的界面真的很漂亮,灰白色的背景,藍底色的重點關注區域,可以引導我們的注意力到最需要關注的地方。
2. 使用NuGet安裝EntityFramework
在VS的工具 -> 庫程序包管理器 -> 管理解決方案的NuGet程序包,搜索Entity Framework並安裝,如下圖所示。
編寫Code First所需的模型類(Model)
這里就以用戶角色為例,首先定義角色的模型類。
public class Role
{
[Key]
public int ID { get; set; }
[Required, StringLength(50)]
public string Name { get; set; }
[StringLength(500)]
public string Remark { get; set; }
public virtual ICollection<User> Users { get; set; }
}
然后是用戶的模型類:
public class User
{
[Key]
public int ID { get; set; }
[Required, StringLength(50)]
public string Name { get; set; }
[Required, StringLength(100)]
public string Email { get; set; }
[Required, StringLength(50)]
public string Password { get; set; }
[Required]
public bool Enabled { get; set; }
[StringLength(10)]
public string Gender { get; set; }
[StringLength(100)]
public string ChineseName { get; set; }
[StringLength(100)]
public string EnglishName { get; set; }
[StringLength(200)]
public string Photo { get; set; }
[StringLength(50)]
public string QQ { get; set; }
[StringLength(100)]
public string CompanyEmail { get; set; }
[StringLength(50)]
public string OfficePhone { get; set; }
[StringLength(50)]
public string OfficePhoneExt { get; set; }
[StringLength(50)]
public string HomePhone { get; set; }
[StringLength(50)]
public string CellPhone { get; set; }
[StringLength(500)]
public string Address { get; set; }
[StringLength(500)]
public string Remark { get; set; }
[StringLength(50)]
public string IdentityCard { get; set; }
public DateTime? Birthday { get; set; }
public DateTime? TakeOfficeTime { get; set; }
public DateTime? LastLoginTime { get; set; }
public DateTime? CreateTime { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
注意,我們在此定義了兩個導航屬性(Navigation Property),分別是 Role.Users 和 User.Roles,並且聲明為 virtual ,其實這就啟用了Entity Framework的延遲加載特性。在后面的代碼中,你會看到我們都是使用 Include 來即時加載數據(內部SQL實現是表關聯),從而避免了延遲加載造成的多次數據庫連接。
在上面定義中,我們使用了一些Data Annotations來聲明屬性,比如Key用來跟蹤每一個模型類的實例(也就是實體 - Entity,這也許就是Entity Framework名字的由來),對應到數據庫表中的主鍵。StringLength則用來定義屬性的長度,對應到數據庫表中字段的長度。更多的Data Annotations請參考:http://msdn.microsoft.com/en-us/data/jj591583
使用Fluent API來配置模型類的關系
雖然使用Data Annotation也能設定模型類的關系,但是不夠靈活。Entity Framework還提供了另一種方式Fluent API來設置關系,詳細的介紹可以參考博客園 dudu 老大的這篇文章:http://www.cnblogs.com/dudu/archive/2011/07/11/ef_one-to-one_one-to-many_many-to-many.html
定義用戶和角色之間多對多的關系:
public class AppBoxContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Role>()
.HasMany(r => r.Users)
.WithMany(u => u.Roles)
.Map(x => x.ToTable("RoleUsers")
.MapLeftKey("RoleID")
.MapRightKey("UserID"));
}
}
用更加通俗的話來解釋上面的代碼:
1. 一個角色(Role)有很多(HasMany)用戶(Users);
2. 每個用戶(Users)又有很多(WithMany)角色(Roles);
3. 把這種多對多的關系映射到一張表(RoleUsers),外鍵分別是RoleID和UserID。
需要注意的是,在Entity Framework不能直接對關聯表進行操作,需要通過Role或者User實體來修改添加刪除關系。
編寫數據庫初始化代碼
1. 首先在Global.asax中設置數據庫初始化類:
protected void Application_Start(object sender, EventArgs e)
{
Database.SetInitializer(new AppBoxDatabaseInitializer());
}
2. 定義數據庫初始化類:
public class AppBoxDatabaseInitializer : DropCreateDatabaseIfModelChanges<AppBoxContext> // DropCreateDatabaseAlways<AppBoxContext>
{
protected override void Seed(AppBoxContext context)
{
GetUsers().ForEach(u => context.Users.Add(u));
GetRoles().ForEach(r => context.Roles.Add(r));
}
private static List<Role> GetRoles()
{
var roles = new List<Role>()
{
new Role()
{
Name = "系統管理員",
Remark = ""
},
new Role()
{
Name = "部門管理員",
Remark = ""
},
new Role()
{
Name = "項目經理",
Remark = ""
},
new Role()
{
Name = "開發經理",
Remark = ""
},
new Role()
{
Name = "開發人員",
Remark = ""
},
new Role()
{
Name = "后勤人員",
Remark = ""
},
new Role()
{
Name = "外包人員",
Remark = ""
}
};
return roles;
}
private static List<User> GetUsers()
{
string[] USER_NAMES = { "男", "童光喜", "男", "方原柏", "女", "祝春亞", "男", "塗輝", "男", "舒兆國" };
string[] EMAIL_NAMES = { "qq.com", "gmail.com", "163.com", "126.com", "outlook.com", "foxmail.com" };
var users = new List<User>();
var rdm = new Random();
for (int i = 0, count = USER_NAMES.Length; i < count; i += 2)
{
string gender = USER_NAMES[i];
string chineseName = USER_NAMES[i + 1];
string userName = "user" + i.ToString();
users.Add(new User
{
Name = userName,
Gender = gender,
Password = PasswordUtil.CreateDbPassword(userName),
ChineseName = chineseName,
Email = userName + "@" + EMAIL_NAMES[rdm.Next(0, EMAIL_NAMES.Length)],
Enabled = true,
CreateTime = DateTime.Now
});
}
// 添加超級管理員
users.Add(new User
{
Name = "admin",
Gender = "男",
Password = PasswordUtil.CreateDbPassword("admin"),
ChineseName = "超級管理員",
Email = "admin@examples.com",
Enabled = true,
CreateTime = DateTime.Now
});
return users;
}
}
開始查詢數據庫
使用如下代碼查詢單個用戶:
using(var db = new AppBoxContext())
{
int id = Convert.ToInt32(Request.QueryString["id"]);
User current = db.Users
.Include(u => u.Roles)
.Where(u => u.UserID == id).FirstOrDefault();
if (current != null)
{
labName.Text = current.Name;
labRealName.Text = current.ChineseName;
labGender.Text = current.Gender;
// 用戶所屬角色
labRole.Text = String.Join(",", current.Roles.Select(r => r.Name).ToArray());
}
}
但是每次都寫using 會覺得很煩,能不能就將AppBoxContext實例存儲在一個變量中呢,下面這篇文章給出了最佳實踐:
One DbContext per Request
我們的實現,在Global.asax的后台代碼中:
protected void Application_BeginRequest(object sender, EventArgs e)
{
}
protected virtual void Application_EndRequest()
{
var context = HttpContext.Current.Items["__AppBoxContext"] as AppBoxContext;
if (context != null)
{
context.Dispose();
}
}
然后在PageBase基類中:
public static AppBoxContext DB
{
get
{
// http://stackoverflow.com/questions/6334592/one-dbcontext-per-request-in-asp-net-mvc-without-ioc-container
if (!HttpContext.Current.Items.Contains("__AppBoxContext"))
{
HttpContext.Current.Items["__AppBoxContext"] = new AppBoxContext();
}
return HttpContext.Current.Items["__AppBoxContext"] as AppBoxContext;
}
}
下載或捐贈AppBox
1. AppBox v2.0 是免費軟件,免費提供下載:http://fineui.com/bbs/forum.php?mod=viewthread&tid=3788
2. AppBox v3.0 是捐贈軟件,你可以通過捐贈作者來獲取AppBox v3.0的全部源代碼(http://fineui.com/donate/)。
喜歡這篇文章,請幫忙點擊頁面右下角的【推薦】按鈕。


