1. 程式人生 > 其它 >ORM查詢語言(OQL)簡介--高階篇(續):廬山真貌

ORM查詢語言(OQL)簡介--高階篇(續):廬山真貌

相關文章內容索引:

ORM查詢語言(OQL)簡介--概念篇

ORM查詢語言(OQL)簡介--例項篇

ORM查詢語言(OQL)簡介--高階篇:脫胎換骨

ORM查詢語言(OQL)簡介--高階篇(續):廬山真貌

   PDF.NET框架的OQL經過“脫胎換骨”般的重構之後,引來了它華麗麗的新篇章,將“物件化的SQL”特徵發揮到極致,與至於我在Q群裡面說這應該算是OQL的“收山之作”了。然而,我這麼說有什麼依據?它的設計哲學是什麼?它究竟是何樣?由於本文篇幅較長,請聽本篇慢慢道來,敘說它的廬山真面目!

[有圖有真相]

     User user=new User();

注意:圖上的表示式中的形參 parent 其實是OQL物件,這裡表示父級OQL物件,它參與構造它的子OQL物件。

(圖4:高階子查詢)

(圖5:SQL鎖)

三、精簡之道

    PDF.NET誕生的背景就是在2006年,我參與一個使用iBatis.Net的專案,當時沒有找到合適的程式碼生成工具,手工寫各種配置,寫SQL對映寫的吐血,心想這麼複雜的東西為何還得到那麼多人的喜歡?也許功能的確很強大,而複雜正是體現了它的強大,而我,天生就是一個只喜歡“簡單”的程式猿,因此選擇了.NET而不是Java平臺,既然如此何必要用iBatis這種複雜的東西?

    於是,我參考iBatis的特點,將它大力精簡,僅保留了SQL簡單的對映功能,將SQL語句寫到一個配置檔案裡面,然後提供一套生成工具來自動生成DAL程式碼,這便是PDF.NET的

SQL-MAP功能。儘管從使用上已經比iBatis.Net簡單很多了,但是對於大多數簡單的CRUD,還是需要寫SQL對映,實在不爽,於是給框架加入了點ORM功能,但覺得當時的ORM動不動就將實體類的全部屬性欄位的資料返回來,也覺得不爽,於是設計了OQL,來操作ORM。這便是PDF.NET Ver 1.0 誕生的故事。

    儘管已經過去7年多時間,PDF.NET不斷髮展,但主線還是它的特色功能SQL-MAPOQL資料控制元件這三大部分。最近一段時間,我對OQL進行了完全的重構,仍然堅守最初的設計理念,做最簡單最易用的資料框架。下面,就從OQL的查詢API設計,來講下這個理念。

3.1,Select血案—委託之殤

    ORM選取實體欄位的Select方法該怎樣設計?象EF這樣子:

var user = from c in db.User
    select new
    {
      c.UserID,
      c.Accounts,
      c.Point,
      c.SavePoint,
      c.LoginServerID
    };

    EF使用Linq作為ORM查詢語言,Linq是語法糖,它本質上會轉化成下面的Lambda方式: 

Var user=db.User.Select(c=>new {
c.UserID,
      c.Accounts,
      c.Point,
      c.SavePoint,
      c.LoginServerID
});

    而Lambda,常常用來簡化匿名委託的寫法,也就是說,Lambda也是委託的一種語法糖。因此,EF實現這個效果,靠的還是委託這個功能。

    上面是單個實體類的實體屬性欄位選取,如果是多個呢?

    我們知道,Linq可以處理多個實體類的連線(左、右、內)查詢,但百度了半天,實在不知道怎麼用Lambda來實現多個實體類的屬性選取,有知道的還請大俠告之,謝謝。

    假設有一個集合C,它內部包含了2個集合A,B連線處理後的資料,我們這樣來使用Select方法:

Var data=C.Select((a,b)=>new {
    F1=a.F1,
    F2=b.F2
});

    大家注意到Select方法需要傳遞2個引數進去,此時對引數的型別推導可能會成為問題,因此,實際上的Select擴充套件方法的定義應該帶有2個型別的泛型方法的,呼叫的其實是下面的方法:

Var data=C.Select<A,B>((a,b)=>new {
    F1=a.F1,
    F2=b.F2
});

    假如有3個型別的引數呢?自然得像下面這個樣子使用:

Var data=C.Select<X,Y,Z>((x,y,z)=>new {
    F1=x.F1,
    F2=y.F2,
    F3=z.F3
});

    假如還有4個、5個。。。。N個型別的引數呢?豈不是要定義含有N個引數的Select擴充套件方法?

    如果還有其它方法,假設Where方法也有這種問題呢?

My God!

    委託方法儘管保證了我們寫的程式碼是強型別的,一旦遇到方法需要的型別過多那麼麻煩也就越多,還是回過頭來說ORM查詢的select問題,假設使用委託的解決方案怎麼看都不是一個最佳的方案,特別是多實體類查詢的時候。

    PDF.NET的ORM查詢語言OQL很早就注意到了這個問題,所以它的Select方法採用了非泛型化的設計,例如單個實體類屬性欄位選取:

OQL q = OQL.From(user)
           .Select(user.ID, user.UserName, user.RoleID)
        .END;

    多個實體類的屬性欄位選取:

OQL q2 = OQL.From(user)
            .InnerJoin(roles).On(user.RoleID, roles.ID)
            .Select(user.RoleID, roles.RoleName)
         .END;

    從上面的例子看出來了,不管OQL查詢幾個實體,它的Select使用方式始終是一致的,要想使用哪個屬性欄位,在Select方法裡面通過“實體類例項.屬性”的方式,一直寫下去即可,而支援這個功能僅使用了C#的可選引數功能

public OQL1 Select(params object[] fields)
{
//具體實現略
}

    方法沒有使用委託引數,也沒有定義N多泛型過載,就輕易地實現了我們的目標,從這個意義上來說,在這裡使用委託,真是委託之殤啊!

3.2,Where迷途—委託神器

3.2.1,最簡單的Where條件比較方法

    OQL的Where 條件構造支援最簡單的使用方式,如果查詢條件都是“相等比較”方式,那麼可以使用下面的方式:

            Users user = new Users() { NickName = "pdf.net", RoleID=5 };
            OQL q0 = OQL.From(user)
               .Select()
               .Where(user.NickName,user.RoleID)
               .OrderBy(user.ID)
            .END;
            q0.SelectStar = true;
            Console.WriteLine("q0:one table and select all fields rn{0}", q0);
            Console.WriteLine(q0.PrintParameterInfo());

    程式輸出的結果是:

q0:one table and select all fields
SELECT  *
FROM [LT_Users]
     WHERE  [NickName]=@P0 AND  [RoleID]=@P1
                 ORDER BY  [ID]
--------OQL Parameters information----------
 have 2 parameter,detail:
  @P0=pdf.net    Type:String
  @P1=5          Type:Int32
------------------End------------------------

    可見Where 這樣使用非常簡單,實際專案中很多也是想等條件查詢的,採用這種方式不僅僅構造了查詢引數,而且將引數值也順利的設定好了,這就是使用ORM的實體類例項呼叫 方式最大的好處。

    Where方法支援多個這樣的實體類引數,該方法在PDF.NET Ver4.X之前就一直支援。下面是它的定義:

public OQL2 Where(params object[] fields)
{
//其它程式碼略
}

3.2.1,升級到V5版OQLCompare的問題

    OQL的Where方法支援使用OQLCompare物件。在文章前面的2.6 OQLCompare –比較物件的組合模式 一節中說道我們通過它我們處理了長達5000行業務程式碼構造的查詢條件,在PDF.NET Ver 4.X 版本中,OQLCompare物件得這樣使用:

User user=New User();
OQLCompare cmp=new OQLCompare(user);
OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”) & cmp.Compare(user.Password,”=”,”123456”);
If(xxx條件)
{
  cmpResult=cmpResult & …… //其它條件
}
//條件物件構造完成
OQL q=OQL.From(user).Select().Where(cmpResult).END;

    如果前面的程式碼不修改,那麼使用新版PDF.NET編譯時候不會出錯,但執行時會出錯:

OQLCompare 關聯的OQL物件為空!

3.2.2,OQLCompare新的建構函式

    PDF.NET Ver 5.0版本之後,OQLCompare不通過實體類來初始化此物件,而是用對應的OQL物件來構造它,所以前面的程式碼需要改造成下面這個樣子:

User user=New User();
OQL q=new OQL(user);
OQLCompare cmp=new OQLCompare(q);
OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”) & cmp.Compare(user.Password,”=”,”123456”);
If(xxx條件)
{
  cmpResult=cmpResult & …… //其它條件
}
//條件物件構造完成
q.Select().Where(cmpResult);

3.2.3,OQLCompare 條件比較委託

    除了上面的修改方式,我們也可以不調整程式碼的順序,僅作小小的改變:

User user=new User();
//OQLCompare cmp=new OQLCompare(user);
OQLCompareFun cmpFun = cmp=> {
    OQLCompare cmpResult=cmp.Compare(user.UserName,”=”,”zhagnsan”) 
                        & cmp.Compare(user.Password,”=”,”123456”);
    if(xxx條件)
    {
         cmpResult=cmpResult & cmp.Compare(....)…… //AND其它條件
    }
    return cmpResult;
}
//條件物件構造完成
//OQL q=OQL.From(user).Select().Where(cmpResult).END;
OQL q=OQL.From(user).Select().Where(cmpFun ).END;

    這裡,我們的Where方法接受了一個OQLCompareFun 委託引數,我們來看這個新版的Where方法是怎麼實現的:

        public OQL2 Where(OQLCompareFunc cmpFun)
        {
            OQLCompare compare = new OQLCompare(this.CurrentOQL);
            OQLCompare cmpResult = cmpFun(compare);

            return GetOQL2ByOQLCompare(cmpResult);
        }

    原來沒啥神器的程式碼,僅僅在方法內部聲明瞭一個新的OQLCompare物件,它使用了傳入OQL物件的建構函式,然後將這個OQLCompare物件例項給OQLCompare委託方法使用。但是,我們不能小看這個小小的改進,它將具體OQLCompare物件的處理延遲到了頂層OQL物件的例項化之後,這個“延遲執行”的特點,大大簡化了我們原來的程式碼。

3.2.4,委託進階--泛型委託

    前面的程式碼還是稍微複雜了點,我們來看一個簡單的例子:

User user=new User();
Var list=OQL.From (user)
            .Select()
            .Where(cmp=> cmp.Property( user.RoleId)==10)
         .END
         .ToList<User>();

    我們僅使用了2行程式碼查詢到了使用者表中所有使用者的角色ID等於10的記錄,我們在一行程式碼之內完成了條件程式碼的編寫。

    對於前面的程式碼,我們還能不能繼續簡化呢?如果我們的Where用的委託引數能夠接受一個實體類引數,那麼User物件的例項不必在這裡聲明瞭。

    下面是OQLCompare的委託方法定義:

   public delegate OQLCompare OQLCompareFunc<T>(OQLCompare cmp, T p);

    然後再定義一個對應的Where方法:

    public OQL2 Where<T>(OQLCompareFunc<T> cmpFun)
            where T : EntityBase
        {
            OQLCompare compare = new OQLCompare(this.CurrentOQL);
            T p1 = GetInstance<T>();

            OQLCompare cmpResult = cmpFun(compare, p1);
            return GetOQL2ByOQLCompare(cmpResult);
        }

    OK,有了這個委託神器,前面的程式碼終於可以一行搞定了:

Var list=OQL.From<User>()
            .Select()
            .Where<User>((cmp,user)=> cmp.Property( user.RoleId)==10)
         .END
         .ToList<User>();

    不錯,委託真是厲害!

    按照上面的思路如法炮製,我們定義最多有3個泛型型別的OQLCompare泛型委託,下面是全部的委託定義:

     public delegate OQLCompare OQLCompareFunc(OQLCompare cmp);
    public delegate OQLCompare OQLCompareFunc<T>(OQLCompare cmp, T p);
    public delegate OQLCompare OQLCompareFunc<T1,T2>(OQLCompare cmp,T1 p1,T2 p2);
    public delegate OQLCompare OQLCompareFunc<T1, T2,T3>(OQLCompare cmp, T1 p1, T2 p2,T3 p3);

    有了它,我們再來看一個複雜點的例子:

        void Test4()
        {
            OQLCompareFunc<Users, UserRoles> cmpResult = (cmp, U, R) =>
                   (
                     cmp.Property(U.UserName) == "ABC" &
                     cmp.Comparer(U.Password, "=", "111") &
                     cmp.Comparer(R.RoleName, "=", "Role1")
                   )
                      |
                   (
                     (cmp.Comparer(U.UserName, "=", "CDE") &
                       cmp.Property(U.Password) == "222" &
                       cmp.Comparer(R.RoleName, "like", "%Role2")
                     )
                     |
                     (cmp.Property(U.LastLoginTime) > DateTime.Now.AddDays(-1))
                   )
                   ;
            Users user = new Users();
            UserRoles roles = new UserRoles() { RoleName = "role1" };

            OQL q4 = OQL.From(user)
                        .InnerJoin(roles) .On(user.RoleID, roles.ID)
                        .Select()
                        .Where(cmpResult)
                     .END;
            Console.WriteLine("OQL by OQLCompareFunc<T1,T2>  Test:rn{0}", q4);
            Console.WriteLine(q4.PrintParameterInfo());
            q4.Dispose();
        }

下面是對應的SQL語句和引數資訊:

OQL by OQLCompareFunc<T1,T2>  Test:
SELECT  M.*,T0.*
FROM [LT_Users]  M
INNER JOIN [LT_UserRoles] T0  ON  M.[RoleID] = T0.[ID]
     WHERE
        (   M.[UserName] = @P0 AND  M.[Password] = @P1  AND  T0.[RoleName] = @P2
 )
 OR
        (
          (
             M.[UserName] = @P3 AND  M.[Password] = @P4  AND  T0.[RoleName]  LIKE  @P5
          )
        OR
         M.[LastLoginTime] > @P6 )

--------OQL Parameters information----------
 have 7 parameter,detail:
  @P0=ABC        Type:String
  @P1=111        Type:String
  @P2=Role1      Type:String
  @P3=CDE        Type:String
  @P4=222        Type:String
  @P5=%Role2     Type:String
  @P6=2013/7/28 17:31:35         Type:DateTime
------------------End------------------------

3.2.5,委託與閉包

    前面我們說道只定義到了3個泛型引數的OQLCompareFun委託,為啥不再繼續定義更多引數的泛型委託?

    我覺得,這個問題從3方面考慮:

  • A,如果你需要連線3個以上的表進行查詢,那麼你的查詢設計過於複雜,可以從資料庫或者系統設計上去避免;
  • B,泛型具有閉包功能,可以將需要的引數傳遞進去;
  • C,如果定義更多的OQLCompare泛型委託,有可能重蹈“委託之殤”。

    如果你不贊成A的說法,查詢一定得有3個以上的情況,那麼你可以應用B的方式。實際上,該方式前面已經舉例過了,再來看一個實際的例子:

        void Test3()
        {
            Users user = new Users();
            UserRoles roles = new UserRoles() { RoleName = "role1" };

            OQLCompareFunc cmpResult = cmp =>
                   (
                     cmp.Property(user.UserName) == "ABC" &
                     cmp.Comparer(user.Password, "=", "111") &
                     cmp.EqualValue(roles.RoleName)
                   )
                      |
                   (
                     (cmp.Comparer(user.UserName, OQLCompare.CompareType.Equal, "BCD") &
                       cmp.Property(user.Password) == 222 &
                       cmp.Comparer(roles.ID, "in", new int[] { 1,2,3 })
                     )
                     |
                     (cmp.Property(user.LastLoginTime) > DateTime.Now.AddDays(-1))
                   )
                   ;
            OQL q3 = OQL.From(user).InnerJoin(roles)
               .On(user.RoleID, roles.ID)
               .Select()
               .Where(cmpResult)
               .END;
            Console.WriteLine("OQL by OQLCompareFunc Test:rn{0}", q3);
            Console.WriteLine(q3.PrintParameterInfo());
        }

     我們在cmpResult 委託的結果中,使用了委託變數之外的引數物件user和roles 。如果有更多的引數委託方法也是可以使用的,這些引數就是委託中的“閉包”,使用該特性,那麼再複雜的問題都能夠處理了。

    再次感嘆,委託,真乃神器也!

3.3,Having 之旅—重用之歡

    我們再重溫一下1.2.4的Having問題,看看那個SQL語句:

SELECT SalesOrderID, SUM(LineTotal) AS SubTotal
FROM Sales.SalesOrderDetail
GROUP BY SalesOrderID
HAVING SUM(LineTotal) > 100000.00
ORDER BY SalesOrderID ;

    Having是對分組Group之後的再次篩選,而Where是在Group之前的,所以本質上Having子句也是一個條件表示式,但由於相對Where要簡單,我們先用個方法來實現:

public OQL4 Having(object field,object Value,string sqlFunctionFormat)
{
//具體程式碼略
}

     使用的時候這樣用即可:

OQL q5 = OQL.From(user)
            .Select(user.RoleID).Count(user.RoleID, "count_rolid")
            .GroupBy(user.RoleID)
            .Having(user.RoleID, 2,”COUNT{0}  >  {1} ”))
         .END;

    上面這種使用方式,首先需要手寫Having的聚合函式條件,不是很方便,OQL的Where方法可以使用OQLCompare物件作為比較條件,那麼Having也是可以使用的,將Having方法改寫下:

        public OQL4 Having(OQLCompareFunc cmpFun)
        {
            OQLCompare compare = new OQLCompare(this.CurrentOQL);
            OQLCompare cmpResult = cmpFun(compare);

            if (!object.Equals(cmpResult, null))
            {
                CurrentOQL.oqlString += "rn             HAVING " + cmpResult.CompareString;
            }
            return new OQL4(CurrentOQL);
        }

    然後OQL中就可以下面這樣使用:

OQL q5 = OQL.From(user)
             .Select(user.RoleID).Count(user.RoleID, "count_rolid")
             .GroupBy(user.RoleID)
             .Having(p => p.Count(user.RoleID, OQLCompare.CompareType.GreaterThanOrEqual, 2))
         .END;

Console.WriteLine("q5:having Test: rn{0}", q5);
Console.WriteLine(q5.PrintParameterInfo());

    程式輸出:

q5:having Test:
SELECT
     [RoleID] ,COUNT( [RoleID]) AS count_rolid
FROM [LT_Users]
          GROUP BY  [RoleID]
             HAVING COUNT( [RoleID]) >= @P0
--------OQL Parameters information----------
 have 1 parameter,detail:
  @P0=2          Type:Int32
------------------End------------------------

    OQLCompare 成功應用於Having方法,找到問題的類似之處,然後重用問題的解決方案,這不是令人非常歡樂的事情嗎:)

四、OQL高階例項

    (測試例子說明)

    本篇簡要介紹了PDF.NET V5 版本的功能增強部分,其它例項請看《OQL例項篇》。

4.1,使用星號查詢全部欄位

   OQL的Select方法如果不傳入任何引數,預設將使用關聯的實體類的全部欄位,使用SelectStar 屬性設定“*”進行所有欄位的查詢,此特性用於某些情況下不想修改實體類但又想將資料庫表新增的欄位查詢到實體類中的情況,比如某些CMS系統,可以讓使用者自由增加文章表的欄位。這些增加或者修改的欄位,可以通過entity.PropertyList(“fieldName”) 獲取欄位的值。

Users user = new Users() { NickName = "pdf.net", RoleID=5 };
OQL q0 = OQL.From(user)
             .Select()
             .Where(user.NickName,user.RoleID)
             .OrderBy(user.ID)
         .END;
 q0.SelectStar = true;
 Console.WriteLine("q0:one table and select all fields rn{0}", q0);
 Console.WriteLine(q0.PrintParameterInfo());

程式輸出:

q0:one table and select all fields
SELECT  *
FROM [LT_Users]
     WHERE  [NickName]=@P0 AND  [RoleID]=@P1
                 ORDER BY  [ID]
--------OQL Parameters information----------
 have 2 parameter,detail:
  @P0=pdf.net    Type:String
  @P1=5          Type:Int32
------------------End------------------------

4.2,延遲選取屬性欄位

    有時候可能會根據情況來決定要Select哪些欄位,只需要在OQL例項上多次呼叫Select方法並傳入實體類屬性引數即可,在最終得到SQL語句的時候才會進行合併處理,實現了延遲選取屬性欄位的功能,如下面的例子:

OQL q = OQL.From(user)
           .Select(user.ID, user.UserName, user.RoleID)
        .END;
q.Select(user.LastLoginIP).Where(user.NickName);

Console.WriteLine("q1:one table and select some fieldsrn{0}", q);
Console.WriteLine(q.PrintParameterInfo());

程式輸出:

q1:one table and select some fields
SELECT
     [LastLoginIP],
     [RoleID],
     [UserName],
     [ID]
FROM [LT_Users]
     WHERE  [NickName]=@P0
--------OQL Parameters information----------
 have 1 parameter,detail:
  @P0=pdf.net    Type:String
------------------End------------------------

可以看出,最後輸出的是兩次Select的結果。

4.3,GroupBy約束

    OQL會嚴格按照SQL的標準,檢查在查詢使用了GroupBy子句的時候,Select中的欄位是否包含在GroupBy子句中,如果不包含,那麼會丟擲錯誤結果。某些資料庫可能不會有這樣嚴格的約束,從而使得查詢結果跟SQL標準的預期不一樣,比如SQLite,而在OQL進行這樣的約束檢查,保證了OQL對於SQL標準的支援,使得系統有更好的移植性。

    下面是一個倆聯合查詢並分組的例子:

OQL q2 = OQL.From(user)
             .InnerJoin(roles).On(user.RoleID, roles.ID)
             .Select(user.RoleID, roles.RoleName)
             .Where(user.NickName, roles.RoleName)
             .GroupBy(user.RoleID, roles.RoleName)
             .OrderBy(user.ID)
         .END;
 Console.WriteLine("q2:two table query use joinrn{0}", q2);
 Console.WriteLine(q2.PrintParameterInfo());

程式輸出:

q2:two table query use join
SELECT  
     M.[RoleID],
     T0.[RoleName]  
FROM [LT_Users]  M   
INNER JOIN [LT_UserRoles] T0  ON  M.[RoleID] = T0.[ID] 
     WHERE  M.[NickName]=@P0 AND  T0.[RoleName]=@P1
         GROUP BY  M.[RoleID], T0.[RoleName]
                 ORDER BY  M.[ID] 
--------OQL Parameters information----------
 have 2 parameter,detail:
  @P0=pdf.net      Type:String 
  @P1=role1      Type:String 
------------------End------------------------

4.4,多實體Where條件連線查詢

    SQL中除了多個表之間的左連線、右連線、內連線等Join連線外,還支援一種通過Where條件進行的多表連線的查詢,這種查詢跟內連線等效。

OQL q3 = OQL.From(user, roles)
            .Select(user.ID, user.UserName, roles.ID, roles.RoleName)
            .Where(cmp => cmp.Comparer(user.RoleID, "=", roles.ID)
                    & cmp.EqualValue(roles.RoleName))
            .OrderBy(user.ID)
        .END;
Console.WriteLine("q3:two table query not use joinrn{0}", q3);
Console.WriteLine(q3.PrintParameterInfo());

程式輸出:

q3:two table query not use join
SELECT  
     M.[ID],
     M.[UserName],
     T0.[ID],
     T0.[RoleName]  
FROM [LT_Users]  M   ,[LT_UserRoles] T0
     WHERE   M.[RoleID] =  T0.[ID] AND  T0.[RoleName] = @P0 
                 ORDER BY  M.[ID] 
--------OQL Parameters information----------
 have 1 parameter,detail:
  @P0=role1      Type:String 
------------------End------------------------

4.5,OQLCompare建構函式和操作符過載

    下面的例子演示了新版OQL支援的建構函式,需要使用傳遞OQL引數的過載,同時本例還演示了比較條件的操作符過載,是的程式碼有更好的可讀性。

         void Test2()
        {
            Users user = new Users();
            UserRoles roles = new UserRoles() { RoleName = "role1" };

            OQL q2 = new OQL(user);
            q2.InnerJoin(roles).On(user.RoleID, roles.ID);

            OQLCompare cmp = new OQLCompare(q2);
            OQLCompare cmpResult = 
                   (
                     cmp.Property(user.UserName) == "ABC" &
                     cmp.Comparer(user.Password, "=", "111") &
                     cmp.EqualValue(roles.RoleName) 
                   )
                      |
                   (
                     (cmp.Comparer(user.UserName, "=", "CDE") &
                       cmp.Property(user.Password) == "222" &
                       cmp.Comparer(roles.RoleName, "like", "%Role2")
                     )
                     |
                     (cmp.Property(user.LastLoginTime) > DateTime.Now.AddDays(-1))
                   )
                   ;

            q2.Select().Where(cmpResult);
            Console.WriteLine("OQL by OQLCompare Test:rn{0}", q2);
            Console.WriteLine(q2.PrintParameterInfo());
        }

程式輸出:

OQL by OQLCompare Test:
SELECT  M.*,T0.*  
FROM [LT_Users]  M   
INNER JOIN [LT_UserRoles] T0  ON  M.[RoleID] = T0.[ID] 
     WHERE 
    (   M.[UserName] = @P0 AND  M.[Password] = @P1  AND  T0.[RoleName] = @P2 )
 OR 
    (
      (
         M.[UserName] = @P3 AND  M.[Password] = @P4  AND  T0.[RoleName]  LIKE  @P5 
      ) 
    OR 
     M.[LastLoginTime] > @P6 )
  
--------OQL Parameters information----------
 have 7 parameter,detail:
  @P0=ABC      Type:String 
  @P1=111      Type:String 
  @P2=role1      Type:String 
  @P3=CDE      Type:String 
  @P4=222      Type:String 
  @P5=%Role2      Type:String 
  @P6=2013/7/28 22:15:38      Type:DateTime 
------------------End------------------------

4.6,OQLCompare委託與泛型委託

    參見測試程式的Test3()、Test4()、Test5()方法,原理和部分程式碼例項已經在3.2 Where迷霧—委託神器 一節中做了詳細說明。

4.7,動態構造查詢條件

    下面的例子演示瞭如何在OQLCompare委託方法中,動態的根據其它附加條件,構造OQLCompare查詢條件,同時也演示了通過Lambda表示式與通過委託方法分別實現動態條件構造的過程,而後者的方式適合在.net2.0 下面編寫委託程式碼。

         void TestIfCondition()
        {
            Users user = new Users() {  ID=1, NickName="abc",UserName="zhagnsan",Password="pwd."};
            OQLCompareFunc cmpFun = cmp => {
                OQLCompare cmpResult = null;
                if (user.NickName != "")
                    cmpResult = cmp.Property(user.AddTime) > new DateTime(2013, 2, 1);
                if (user.ID > 0)
                    cmpResult = cmpResult & cmp.Property(user.UserName) == "ABC" & cmp.Comparer(user.Password, "=", "111");
                return cmpResult;
            };

            OQL q6 = OQL.From(user).Select().Where(cmpFun).END;
            Console.WriteLine("OQL by 動態構建 OQLCompare Test(Lambda方式):rn{0}", q6);
            Console.WriteLine(q6.PrintParameterInfo());
        }

        void TestIfCondition2()
        {
            Users user = new Users() { ID = 1, NickName = "abc"};
            OQL q7 = OQL.From(user)
                .Select()
                .Where<Users>(CreateCondition)
                .END;
            Console.WriteLine("OQL by 動態構建 OQLCompare Test(委託函式方式):rn{0}", q7);
            Console.WriteLine(q7.PrintParameterInfo());
        }

        OQLCompare CreateCondition(OQLCompare cmp,Users user)
        {
            OQLCompare cmpResult = null;
            if (user.NickName != "")
                cmpResult = cmp.Property(user.AddTime) > new DateTime(2013, 2, 1);
            if (user.ID > 0)
                cmpResult = cmpResult & cmp.Property(user.UserName) == "ABC" 
                    & cmp.Comparer(user.Password, "=", "111");
            return cmpResult;
        }

 程式輸出:

OQL by 動態構建 OQLCompare Test(Lambda方式):
SELECT  [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],[LastLoginTime],[LastLoginIP],[Remarks],[AddTime]  
FROM [LT_Users]   
     WHERE    [AddTime] > @P0 AND  [UserName] = @P1  AND  [Password] = @P2  
--------OQL Parameters information----------
 have 3 parameter,detail:
  @P0=2013/2/1 0:00:00      Type:DateTime 
  @P1=ABC      Type:String 
  @P2=111      Type:String 
------------------End------------------------
OQL by 動態構建 OQLCompare Test(委託函式方式):
SELECT  [ID],[UserName],[Password],[NickName],[RoleID],[Authority],[IsEnable],[LastLoginTime],[LastLoginIP],[Remarks],[AddTime]  
FROM [LT_Users]   
     WHERE    [AddTime] > @P0 AND  [UserName] = @P1  AND  [Password] = @P2  
--------OQL Parameters information----------
 have 3 parameter,detail:
  @P0=2013/2/1 0:00:00      Type:DateTime 
  @P1=ABC      Type:String 
  @P2=111      Type:String 
------------------End------------------------

 注意:

在 TestIfCondition 方法中,程式中使用了實體類來做if 語句的條件,但是這個實體類是OQL關聯的實體類,在使用實體類屬性的時候會觸發OQL欄位堆疊操作。早期版本的PDF.NET SOD框架對此問題支援不是很完善,有可能生成不是預期的SQL語句。該現象在VS的單步除錯執行中出現的可能性比較大,這就是以前說的“除錯陷阱”。有可能請使用動態查詢條件使用者,請升級到版本 Ver5.2.3.0429 之後的新版本。

下面再給一個例子:

            SalesOrder model = new SalesOrder();
            model.iOrderTypeID = "123";

            //string orderTypeID = model.iOrderTypeID;
            BCustomer bCustomer = new BCustomer();

            OQLCompareFunc<BCustomer,SalesOrder> cmpFun = (cmp,C,S) =>
            {
                OQLCompare cmpResult = null;
                cmpResult = cmp.Comparer(S.iBillID, OQLCompare.CompareType.Equal, 1);
                
                if (!string.IsNullOrEmpty(S.iOrderTypeID))
                    cmpResult = cmpResult & cmp.Comparer(S.iOrderTypeID, OQLCompare.CompareType.Equal, S.iOrderTypeID);

                int iCityID = 39;
                //由於呼叫了關聯實體類的 S.iOrderTypeID 用於條件比較,所以下面需要呼叫 cmp.NewCompare()
                //cmpResult = cmpResult & cmp.NewCompare().Comparer<int>(C.iCityID, OQLCompare.CompareType.Equal, iCityID);
                //感謝網友 紅楓星空 發現此問題

                //或者繼續採用下面的寫法,但是必須確保 Comparer 方法第一個引數呼叫為實體類屬性,而不是待比較的值
                cmpResult = cmpResult & cmp.Comparer(C.iCityID, OQLCompare.CompareType.Equal, iCityID);
                return cmpResult;
            };
           

            OQL oQL = OQL.From(model)
                    .LeftJoin(bCustomer).On(model.iCustomerID, bCustomer.ISID)
                    .Select()
                    .Where(cmpFun)
                    .OrderBy(model.iBillID, "desc")
                .END;

            Console.WriteLine(oQL);
            Console.WriteLine(oQL.PrintParameterInfo());
            Console.ReadLine();

注意:上面的變數 iCityID 不能等於屬性 C.iCityID 的當前值,比如0,這種情況框架無法判斷方法使用的實體類屬性是在本方法的引數上,還是方法呼叫前曾經使用過但還沒有清理過的實體類屬性呼叫。每當執行了Comparer 方法後,OQL的欄位堆疊會清空的,但是這個例子中,它可能沒有被清空,從而有可能出錯。當然,這裡還可以採用 呼叫 NewCompare 方法的方式,見註釋。

正確的輸出結果,應該是:

SELECT  M.*,T0.*
FROM [tb_SalesOrder]  M
LEFT JOIN [tb_BCustomer] T0  ON  M.[iCustomerID] = T0.[ISID]
     WHERE    M.[iBillID] = @P0 AND  M.[iOrderTypeID] = @P1  AND  T0.[iCityID] = @P2
                 ORDER BY  M.[iBillID] desc
--------OQL Parameters information----------
 have 3 parameter,detail:
  @P0=1          Type:Int32
  @P1=123        Type:String
  @P2=39         Type:Int32
------------------End------------------------

備註:如果需要了解更多的OQL動態條件查詢的資訊,請參考這篇文章《左求值表示式,堆疊,除錯陷阱與ORM查詢語言的設計

4.8,IN 條件子查詢

    下面的例子使用一個child 的OQL例項作為q的OQL例項的子物件,構造了一 個IN 條件子查詢。當前例項演示的是簡單子查詢,它沒有在子查詢中引用父查詢的欄位。

         void TestChild()
        {
            Users user = new Users();
            UserRoles roles = new UserRoles();
            OQL child = OQL.From(roles)
                .Select(roles.ID)
                .Where(p => p.Comparer(roles.NickName, "like", "%ABC"))
                .END;

            OQL q = OQL.From(user)
                .Select(user.ID,user.UserName)
                .Where(cmp => cmp.Comparer(user.RoleID, "in", child))
                .END;

            Console.WriteLine("OQL by 子查詢Test:rn{0}", q);
            Console.WriteLine(q.PrintParameterInfo());
        }

    程式輸出:

OQL by 子查詢Test:
SELECT  
     [ID],
     [UserName]  
FROM [LT_Users]   
     WHERE  [RoleID]  IN  
(SELECT  
     [ID]  
FROM [LT_UserRoles]   
     WHERE  [RoleNickName]  LIKE  @P0 )
 
--------OQL Parameters information----------
 have 1 parameter,detail:
  @P0=%ABC      Type:String 
------------------End------------------------

4.9,高階子查詢

     高階子查詢必須使用OQLChildFunc  委託,並且使用OQL.From(OQL parent,EntityBase entity) 的過載,通過該方式即可在子查詢中使用父查詢的實體類,而子查詢最後作為OQLCompare物件的條件比較方法的引數傳入,即下面程式碼中的

          cmp.Comparer(user.RoleID, "=", childFunc)

    下面是詳細程式碼:

 void TestChild2()
        {
            /*
             SELECT * FROM [LT_Users]  WHERE RoleID =
  (SELECT ID FROM dbo.LT_UserRoles r WHERE  [LT_Users].NickName=r.NickName)
             */
            Users user = new Users() {  NickName="_nickName"};
            UserRoles roles = new UserRoles() {  NickName="_roleNickName"};

            OQLChildFunc childFunc = parent => OQL.From(parent,roles)
                .Select(roles.ID)
                .Where(cmp => cmp.Comparer(user.NickName, "=", roles.NickName) //比較的欄位順序無所謂
                            & cmp.Property(roles.AddTime) > DateTime.Now.AddDays(-3)) 
                .END;

            OQL q = OQL.From(user)
                .Select()
                .Where(cmp => cmp.Comparer(user.RoleID, "=", childFunc))
                .END;

            q.SelectStar = true;
            Console.WriteLine("OQL by 高階子查詢Test:rn{0}", q);
            Console.WriteLine(q.PrintParameterInfo());
        }

    程式輸出:

OQL by 高階子查詢Test:
SELECT  *  
FROM [LT_Users]  M   
     WHERE  [RoleID] = 
(SELECT  
     [ID]  
FROM [LT_UserRoles]   
     WHERE  M.[NickName] =  [RoleNickName] AND  [AddTime] > @P0  )
 
--------OQL Parameters information----------
 have 1 parameter,detail:
  @P0=2013/7/26 22:15:38      Type:DateTime 
------------------End------------------------

4.10,批量更新操作

    下面的例子使用了Lambda 條件方式的Where作為更新的條件,在被註釋的程式碼中,還演示了舊版本的條件更新方式。如果更新條件對應的資料不是單條的,那麼即可實現“批量更新”的效果。

        void TestUpdate()
        {
            Users user = new Users() {
                AddTime=DateTime.Now.AddDays(-1), 
                Authority="Read", 
                NickName = "菜鳥" 
            };
            OQL q = OQL.From(user)
                .Update(user.AddTime, user.Authority, user.NickName)
                .Where(cmp => cmp.Property(user.RoleID) == 100)
                .END;

            //OQL q = OQL.From(user)
            //    .Update(user.AddTime)
            //    .Where(user.Authority, user.NickName)
            //    .END;
            Console.WriteLine("OQL update:rn{0}rn",q);

            Console.WriteLine(q.PrintParameterInfo());
        }

    程式輸出:

OQL update:
UPDATE [LT_Users]  SET 
     [AddTime] = @P0,
     [Authority] = @P1,
     [NickName] = @P2
     WHERE  [RoleID] = @P3

--------OQL Parameters information----------
 have 4 parameter,detail:
  @P0=2013/7/28 22:15:38      Type:DateTime 
  @P1=Read      Type:String 
  @P2=菜鳥      Type:String 
  @P3=100      Type:Int32 
------------------End------------------------

4.11,動態排序

    有時候我們需要根據使用者的選擇來決定派系的方式和排序的欄位,這個時候就需要查詢具有動態排序功能了,只需要在OQL的OrderBy方法內呼叫一個排序委託方法即可。下面的例子中被註釋的部分,總共演示了OQL支援的3種排序方式。

        void TestOQLOrder()
        {
            Users user = new Users();
            //OQLOrderAction<Users> action = this.OQLOrder;
            OQL q = OQL.From(user)
                .Select(user.UserName,user.ID)
                //.OrderBy(p => p.Desc(user.UserName).Asc(user.ID))
                //.OrderBy(action,user)
                .OrderBy<Users>(OQLOrder,user) //3種OQLOrder 物件的使用方法
                .END;

            Console.WriteLine("OQL test OQLOrder object:rn{0}rn", q);
        }

        void OQLOrder(OQLOrder p, Users user)
        {
             p.Desc(user.UserName).Asc(user.ID);
        }

    程式輸出: 

OQL test OQLOrder object:
SELECT  
     [UserName],
     [ID]  
FROM [LT_Users]   
                 ORDER BY  [UserName] DESC, [ID] ASC

4.12,批量資料插入

    新版本支援通過OQL進行實體類的資料插入,同時還支援高效的直接從資料庫的查詢結果插入目標表的操作。前者直接使用OQL的Insert方法,後者使用InsertFrom方法。示例只插入了一列資料,如果需要插入多列,在確保子查詢返回多列的情況下,用下面的方式:

OQL q = OQL.From(user)
            .InsertFrom(child,user.RoleID,user.ID,user.Name,user.NickName);

 下面是實際的例子:

        void TestInsert()
        {
            Users user = new Users()
            {
                AddTime = DateTime.Now.AddDays(-1),
                Authority = "Read",
                NickName = "菜鳥"
            };

            OQL q = OQL.From(user)
                .Insert(user.AddTime, user.Authority, user.NickName);

            Console.WriteLine("OQL insert:rn{0}rn", q);
            Console.WriteLine(q.PrintParameterInfo());
        }

        void TestInsertFrom()
        {
            Users user = new Users();
            UserRoles roles = new UserRoles();

            OQL child = OQL.From(roles)
                .Select(roles.ID)
                .Where(cmp => cmp.Comparer(roles.ID, ">", 100))
                .END;

            OQL q = OQL.From(user)
                .InsertFrom(child,user.RoleID);

            Console.WriteLine("OQL insert from:rn{0}rn", q);
            Console.WriteLine(q.PrintParameterInfo());
        }

程式輸出:

OQL insert:
INSERT INTO [LT_Users] (
     [AddTime],
     [Authority],
     [NickName]) 
VALUES
    (@P0,@P1,@P2) 

--------OQL Parameters information----------
 have 3 parameter,detail:
  @P0=2013/7/28 22:15:38      Type:DateTime 
  @P1=Read      Type:String 
  @P2=菜鳥      Type:String 
------------------End------------------------

OQL insert from:
INSERT INTO [LT_Users] (
     [RoleID]
    ) 
SELECT  
     [ID]  
FROM [LT_UserRoles]   
     WHERE @P0 >  [ID]  

--------OQL Parameters information----------
 have 1 parameter,detail:
  @P0=0      Type:Int32 
------------------End------------------------

4.13,指定查詢的鎖定方式

    SqlServer可以在SQL單條查詢語句中指定查詢的鎖定方式,比如行鎖、頁鎖或者不鎖定資料等,詳細內容可以參考OQL.SqlServerLock 列舉型別定義,或者參考SqlServer聯機幫助。

    請注意:如果使用了OQL的With方法指定了查詢的鎖定方式,那麼該條OQL將只能在SqlServer中使用,不利於OQL的跨資料庫平臺的特性,但由於PDF.NET使用者的強烈要求,最終加入了該特性。實際上,對查詢的鎖定方式,可以通過指定事務的隔離級別實現。

     下面的例子實現了對User表查詢的 NOLOCK :

         void TestSqlLock()
        {
            Users user = new Users();
            OQL q = OQL.From(user)
                //.With(OQL.SqlServerLock.NOLOCK)
                .With("nolock")
                .Select(user.ID,user.UserName,user.NickName)
                .END;
            Console.WriteLine("OQL Test SQL NoLock:rn{0}rn", q);
        }

    程式輸出:

OQL Test SQL NoLock:
SELECT  
     [ID],
     [UserName],
     [NickName]  
FROM [LT_Users]  WITH(NOLOCK)  
 

4.14,欄位的計算條件

 不同於2個欄位之間的簡單比較,有時候可能需要1個欄位進行計算後,再跟第2個欄位比較,這種條件我們稱呼它為“計算條件”。實際上,對1個欄位的計算有點類似於對欄位使用函式的操作,只是這個“函式”是個匿名函式而已。比如有下面的查詢條件:

user.LastLoginTime-user.AddTime>'23:00:00'

比較最後登入時間與使用者記錄增加的時間要大於23小時(當然這個條件可以通過DateDiff函式來實現,這裡只是用它來做一個例子說明計算條件),我們使用“不等式替換”原理,上面的條件可以改寫為:

user.LastLoginTime -'23:00:00'>user.AddTime

於是,這個計算條件,就可以使用OQLCompare的“函式條件表示式”了,下面之間給出OQL的例子:

 Users user = new Users();
 //    user.LastLoginTime-user.AddTime>'23:00:00' 
 // => user.LastLoginTime -'23:00:00'>user.AddTime
 OQL q = OQL.From(user)
     .Select()
     .Where(cmp => cmp.Comparer(user.LastLoginTime, ">", user.AddTime, "{0}-'23:00:00'"))
     .END;
 q.SelectStar = true;
 Console.WriteLine("OQL Test SQL Field compute:rn{0}rn", q);
 Console.WriteLine(q.PrintParameterInfo());

 程式輸出:

OQL Test SQL Field compute:
SELECT  *
FROM [LT_Users]
     WHERE  [LastLoginTime]-'23:00:00' >  [AddTime]

-------No paramter.--------

程式成功達到我們的預期,這說明,只要我們肯思考,問題還是容易解決的。

(注:該小結內容於2013.8.7日增加)

4.15,NOT 邏輯比較條件

 SQL的NOT操作用於對條件表示式取反,儘管對於“相等”可以取反得到“不等”操作,但對於比較複雜的組合條件,整體取反從邏輯語義上來說,更容易理解。所以OQL也支援NOT邏輯比較條件。只需要使用OQLCompare.Not() 方法即可,比如有下面的查詢語句:

            Users user = new Users();
            OQL q = OQL.From(user)
                .Select(user.ID, user.UserName,user.Password)
                .Where<Users>((cmp, u) => OQLCompare.Not( 
                    cmp.Property(u.UserName) == "ABC" & cmp.Property(u.Password) == "123")
                    )
                .END;

            Console.WriteLine("OQL Test NOT Condition:rn{0}rn", q);
            Console.WriteLine(q.PrintParameterInfo());

程式輸出:

OQL Test NOT Condition:
SELECT
     [ID],
     [UserName],
     [Password]
FROM [LT_Users]
     WHERE  NOT (  [UserName] = @P0 AND  [Password] = @P1 )

--------OQL Parameters information----------
 have 2 parameter,detail:
  @P0=ABC        Type:String
  @P1=123        Type:String
------------------End------------------------

4.16,BETWEEN操作

 Between 用於指定條件比較的範圍,是包含關係,相當於 min <= x <=max 。用Between可以簡化這樣的條件比較。  OQLCompare的Between方法是這樣定義:

/// <summary>
 /// 指定條件的包含範圍
 /// </summary>
 /// <typeparam name="T">屬性欄位的型別</typeparam>
 /// <param name="field">屬性欄位</param>
 /// <param name="beginValue">起始值</param>
 /// <param name="endValue">結束值</param>
 /// <returns>比較物件</returns>
 public OQLCompare Between<T>(T field, T beginValue, T endValue)
 {
   //略
 }

只需要這樣使用:

 OQL q7 = OQL.From(user).Select()
    .Where(cmp =>cmp.Between(user.ID,5,10))
    .END;
 q7.SelectStar = true;
 Console.WriteLine("q7:having Test: rn{0}", q7);
 Console.WriteLine(q7.PrintParameterInfo());

程式輸出:

q7:having Test:
SELECT  *
FROM [LT_Users]
     WHERE  [ID]  BETWEEN   @P0 AND @P1
--------OQL Parameters information----------
 have 2 parameter,detail:
  @P0=5          Type:Int32
  @P1=10         Type:Int32
------------------End------------------------

4.17 使用SQL函式進行比較

 有時候,我們可能需要對一個欄位進行一個SQL函式計算,然後再讓這個結果跟某一個值進行比較,當然這些函式可能在不同的資料庫中是不同的,比如SqlServer與Oracle在很多欄位處理函式上都不同,下面以SqlServer 求取某個欄位的小時數是否大於15點:

 Users user = new Users();

 OQL q = OQL.From(user)
     .Select()
     .Where(cmp => cmp.ComparerSqlFunction(user.LastLoginTime, ">", 15, "DATEPART(hh, {0})"))
     .END;
 q.SelectStar = true;
 Console.WriteLine("OQL Test SQL Fuction:rn{0}rn", q);
 Console.WriteLine(q.PrintParameterInfo());

程式輸出:

OQL Test SQL Fuction:
SELECT  *
FROM [LT_Users]
     WHERE  DATEPART(hh, [LastLoginTime]) > @P0

--------OQL Parameters information----------
 have 1 parameter,detail:
  @P0=15      Type:Int32 
------------------End------------------------

通過這種方式,我們能夠在比較條件上應用任何SQL函式,相比EF,這種方式要簡單。注意這裡使用的是 OQLCompare的 ComparerSqlFunction 函式。

附錄

下面是PDF.NET Ver5.0版本OQL測試完整的原始碼和SQL輸出,其中大部分內容已經在上面的章節中做過說明,但有少部分未在正文中做說明,供大家集中參考。

附錄相關的程式將在PDF.NET的開源專案 http://pwmis.codeplex.com 下載頁面提供下載, PDF.NET_V5.0_Beta_20130807 (已經更新,之前下載過的請重新下載)

有關框架更多的資訊,請參考框架官網 http://www.pwmis.com/sqlmap

附錄1:OQL測試完整原始碼

附錄2:OQL測試程式輸出的SQL資訊