Linq בזמן ריצה (Runtime)

נוסף ב-25/12/2007 02:47 על ידי דניאל כץ

במאמר זה נסקור את המתודולוגיה החדשה הנקראת Linq to Sql ונענה על השאלות: איך Linq ממומש ברמת הCLR? מהם המרכיבים המרכזיים בטכנולוגיה? ואיך ניתן לממש תשאול דינאמי בזמן ריצה (Runtime)? כל זה מלווה בדוגמאות קוד עם הסברים.

מה זה Linq?

מתודולוגיית Linq נוספה לNet. בגרסתה ה3.5, ומהווה מקבץ ביטויים לתשאול טיפוסים דמויי מערכים, או בעברית פשוטה – Linq יכול לתשאל כל טיפוס שמממש את הממשק IEnumerable. בנוסף חשוב לציין שעיקר התועלת במתודולוגיית Linq היא צורת הביטויים (Syntax) שמאפשרת לנו פשוט לציין מה אנחנו רוצים לעשות במקום להתמקד בדרך להגיע אל התוצאה הרצויה, נביא דוגמה:

Dim arr As IEnumerable(Of Integer) = New Integer() {0, 1, 2, 3, 4, 99}

Function WithoutLinq() As IEnumerable(Of Integer)

    Dim out = New List(Of Integer)

    For Each itm In arr

        If itm > 2 And itm < 10 Then

            out.Add(itm)

        End If

    Next

    Return out

End Function

Function UsingLinq() As IEnumerable(Of Integer)

    Return (From p In arr Where p > 2 And p < 10)

End Function

בדוגמה שני פונקציות שמחזירות את כל המספרים שגדולים מ2 וקטנים מ10 מתוך המערך arr שמוכרז בתחילת הדוגמה. ההבדל היחיד ביניהם הוא בצורת הכתיבה. בפונקציה WithoutLinq אנו מבצעים את הסינון ללא שימוש בLinq ולכן נאלצים לכתוב הרבה קוד שלא מעניין אותנו בכלל, כמו לולאת For Each או לולאת מונה וכדומה. להבדיל, בפונקציה UsingLinq, אני מקבל את אותה התוצאה ע"י שאני פשוט כותב "תוציא את כל האיברים שמתקיים בהם התנאי X" וLinq דואג לכל השאר.

בנוסף, Linq מהווה יסוד למספר טכנולוגיות נוספות כגון Linq to Sql.

מה זה Linq to Sql?

Linq to Sql היא טכנולוגיית מיפוי מחלקות Net. לטבלאות Sql, או בקיצור ORM הבנוי על מתודולוגיית Linq. בשונה מLinq האב, Linq to Sql מבצע את השאילתות על שרת הSql ע"י המרתן לשאילתות Sql רגילות, ועובד עם טיפוסים שמממשים את IQueryable.

אבני היסוד של Linq to Sql הם:

  • DataContext - נקודת הכניסה לLinq to Sql. הDataContext מכיל בתוכו את הORM, וכל שאילתא תהיה רלוונטית רק לגבי DataContext מסוים.
  • ITable - מממש גישה לטבלה בודדת. למרות שכעיקרון כל טבלה מיוצגת בORM של Linq to Sql ע"י מחלקה רגילה שיורשת ישירות מהטיפוס Object ולא חייבת לממש שום ממשק (Interface), במאמר זה אנו נתייחס לITable כמצביע לטבלה לא ידועה מראש.
  • IQueryable - אבן היסוד האולטימטיבית בשאילתות Linq to Sql, כל שאילתת Linq to Sql תישאל על IQueryable ותניב IQueryable (כברירת מחדל) כתוצאה.

להדגמה נביא קטע קוד בLinq: (הDataContext בדוגמה הוא של המסד נתונים Northwind)

Dim custQuery = _

    From cust In db.Customers _

    Where cust.City = "London"

ואת התוצאה בSql:

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]

FROM [dbo].[Customers] AS [t0]

WHERE [t0].[City] = @p0

-- @p0: Input String (Size = 6; Prec = 0; Scale = 0) [London]

-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20810.0

ומה קורה מתחת למכסה המנוע? (איך הדבר הזה באמת עובד?)

חשוב לציין שLinq זה בעיקר פיצ'ר של המהדר (Compiler) ולא תוספת לCLR של Net., כלומר שאין שום דרך בעולם לתרגם את הדוגמה האחרונה שהבאנו לשפת IL ללא שינוי מבני עמוק.

אז איך באמת תיראה הדוגמה הקודמת אחרי טיפול של המהדר?

משהו כזה: (הקוד עבר refactoring יסודי)

Dim db As DataContext = New NorthwindDataContext

Dim p As ParameterExpression = _

        Expression.Parameter(GetType(Customers), "p")

Dim city As MemberExpression = _

        Expression.MakeMemberAccess(p, GetType(Customers).GetProperty("City"))

Dim london As ConstantExpression = Expression.Constant("London")

Dim expr As BinaryExpression = Expression.Equal(city, london)

Dim lambda As LambdaExpression = _

        Expression.Lambda(Of Customers)(expr, New ParameterExpression() {p})

Dim custQuery = db.Customers.Where(lambda)

עכשיו נסביר מה קרה כאן שורה אחר שורה:

קודם כל צריך ליצור מופע של DataContext שאליו אנחנו מתייחסים

Dim db As DataContext = New NorthwindDataContext

מגדירים את האובייקט אותו נרצה לתשאל, במקרה שלנו זו הטבלה Customers

Dim p As ParameterExpression = Expression.Parameter(GetType(Customers), "p")

מגדירים את השדה (או האיבר) של האובייקט שאיתו נעשה את פעולת ההשוואה בהמשך

Dim city As MemberExpression = Expression.MakeMemberAccess(p, GetType(Customers).GetProperty("City"))

יוצרים מופע של ביטוי שיכיל את הערך שאותו נרצה לחפש בטבלה

Dim london As ConstantExpression = Expression.Constant("London")

יוצרים מופע של ביטוי ההשוואה על בסיס השדה והערך

Dim expr As BinaryExpression = Expression.Equal(city, london)

כאן קורה כל הקסם, אנו יוצרים ביטוי Linq שמכיל ביטוי Lambda. כלומר משהו כמו Delegate דינאמי (ניתן לשינוי!) שהוא יתקמפל לביטוי Sql רגיל

Dim lambda As LambdaExpression = Expression.Lambda(Of Customers)(expr, New ParameterExpression() {p})

כאן אנחנו מעבירים את ביטוי הLambda-Linq לProvider של Linq to Sql לצורך קימפול לשאילתת Sql

Dim custQuery = db.Customers.Where(lambda)

זה על גבול הגאונות! אז מה הבעיה?

הבעיה היא בכך שכפי שראיתם בהדגמה כל המנגנון של Linq מתבסס על טיפוסים ג'נריים, וכטיפוסים ג'נריים הם חייבים להיקבע בDesign Time. וזה יהווה בעיה בשני השורות האחרונות בטבלה.

  • הפונקציה Expression.Lambda היא ג'נרית וצריכה לקבל את טיפוס הטבלה כפרמטר.
  • הפנייה לטבלה באמצעות db.Customers מחייבת ידיעת הטיפוס מראש.

כאן בא לעזרתנו הממשק ITable. הממשק בא לתאר מחלקה כלשהי ששייכת לDataContext כלשהו. בנוסף ניעזר בפונקציה GetFuncType שמחזירה תיאור (לא מופע) של טיפוס Func(Of T, TResult) עם פרמטרים ג'נריים לפי דרישה - לו נזדקק ליצירת מופע של LambdaExpression בזמן ריצה.

כעת נביא דוגמה למימוש:

Function BuildQuery(ByVal db As DataContext, _

                    ByVal type As Type, _

                    ByVal field As PropertyInfo, _

                    ByVal filter As String) As IQueryable

    Dim source As ITable = db.GetTable(type)

    Dim p As ParameterExpression = Expression.Parameter(type, "p")

    Dim left As MemberExpression = _

            Expression.Property(p, type.GetProperty(field.Name))

    Dim right As ConstantExpression = Expression.Constant(filter)

    Dim expr As BinaryExpression = Expression.Equal(left, right)

    Dim argFunc As Type() = {type, GetType(Boolean)}

    Dim genFunc As Type = Expression.GetFuncType(argFunc)

    Dim lambda As LambdaExpression = _

            Expression.Lambda(genFunc, expr, New ParameterExpression() {p})

    Dim callExpr As MethodCallExpression = Expression.Call(GetType(Queryable), _

                                   "Where", New Type() {type}, _

                                   source.Expression, _

                                   Expression.Quote(lambda))

    Return source.Provider.CreateQuery(callExpr)

End Function

ועכשיו בעברית:

היות ואנחנו לא יודעים איזו טבלה נרצה לתשאל, אז נשתמש בפונקציה GetTable שמחזירה לנו ITable שמולו נוכל לעבוד בלי לדעת את טיפוס הטבלה

Dim source As ITable = db.GetTable(type)

יוצרים מופע של פרמטר ומעבירים לו את הטיפוס (לא מופע) של האובייקט בORM שמולו נרצה לעבוד

Dim p As ParameterExpression = Expression.Parameter(type, "p")

יוצרים ביטוי-מצביע לשדה שאותו נרצה לתשאל

Dim left As MemberExpression = Expression.Property(p, type.GetProperty(field.Name))

יוצרים מופע של ביטוי שיכיל את הערך שאותו נרצה לחפש בטבלה

Dim right As ConstantExpression = Expression.Constant(filter)

יוצרים מופע של ביטוי ההשוואה על בסיס השדה והערך

Dim expr As BinaryExpression = Expression.Equal(left, right)

מגדירים את הפרמטרים הג'נריים של ביטוי הLambda לטיפוס של הטבלה (הטיפוס של הפרמטר שמועבר לפונקציה) ובוליאני (הטיפוס שמוחזר ע"י הפונקציה)

Dim argFunc As Type() = {type, GetType(Boolean)}

מקבלים טיפוס (לא מופע) עבור ביטוי הLambda

Dim genFunc As Type = Expression.GetFuncType(argFunc)

יוצרים מופע של ביטוי Lambda-Linq על בסיס ביטוי ההשוואה שיצרנו קודם

Dim lambda As LambdaExpression = Expression.Lambda(genFunc, expr, New ParameterExpression() {p})

לצערנו לא נכללה בFramework פונקציה Where לא ג'נרית, ולכן אנחנו נעקוף את הבעיה הזאת ע"י יצירת ביטוי קריאה לפונקציה ונעביר לו את שם הפונקציה ואת הפרמטרים הג'נריים והרגילים

Dim callExpr As MethodCallExpression = Expression.Call(GetType(Queryable), Where", New Type() {type}, source.Expression, Expression.Quote(lambda))

כאן אנחנו מעבירים את ביטוי הLambda-Linq לProvider של Linq to Sql לצורך קימפול לשאילתת Sql

Return source.Provider.CreateQuery(callExpr)

בשימוש במתודולוגיה זו ייתכן ותרצו ליצור לעצמכם פונקציה Where לא ג'נרית באמצאות פיצ'ר ההרחבות (Extensions) בצורה כזאת:

<Extension()> _

Public Function Where(ByVal source As IQueryable, _

                      ByVal expr As LambdaExpression) As IQueryable

    Dim callExpr = Expression.Call(GetType(Queryable), "Where", _

                               New Type() {source.ElementType}, _

                               source.Expression, _

                               Expression.Quote(expr))

    Return source.Provider.CreateQuery(callExpr)

End Function

קינון שאילתות

עכשיו מגיע הכיף האמיתי. כפי שציינתי בפתיחה, כל ביטוי Linq to Sql נשאל על IQueryable ומחזיר IQueryable כתוצאה. הדבר מאפשר קינון של שניים או יותר שאילתות אחת על השנייה ולהניב שאילתא שלישית שתכלול את שניהם. באו ננסה לקנן שאילתת Linq סטנדרטית על גבי שאילתא שניצור ע"י הפונקציה שבנינו.

הנה הקוד:

Public Shared Sub NestedQueryExample()

    Dim db As New dbMap

    Dim table As Type = GetType(Contact)

    Dim field As PropertyInfo = table.GetProperty("lname")

    Dim allKatz As IQueryable = BuildQuery(db, table, field, "כץ")

    Dim first50 As IQueryable = From p In allKatz Take 50

End Sub

הסבר: המשתנה allKatz מכיל שאילתא שתחזיר את כל הרשומות מטבלת אנשי הקשר ששם משפחתם הוא "כץ". שימו לב להדגשה - המשתנה מכיל  את השאילתא ולא את התוצאה. כעת אנחנו מריצים שאילתת Linq סטנדרטית על המשתנה allKatz שתחזיר רק את ה50 הרשומות הראשונות.

כאן מתגלה עוד פן עוצמתי בLinq to Sql. שאילתת הSql שתתקבל  כתוצאה ממה שכתבנו לא תהיה מקוננת אלא תעבור אופטימיזציה לשאילתא אחת. הנה:

SELECT TOP (50) [t0].[ID], [t0].[fname], [t0].[lname], [t0].[street_id], [t0].[building], [t0].[entrance], [t0].[apartment], [t0].[tel1], [t0].[tel2], [t0].[tel3]

FROM [dbo].[Contacts] AS [t0]

WHERE [t0].[lname] = @p0

-- @p0: Input NVarChar (Size = 2; Prec = 0; Scale = 0) [כץ]

-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

נציין לסיום שביצוע השאילתא בפועל קורה רק בזמן שמנסים לפנות לאיבר מתוך השאילתא שאז לא ניתן לתשאל אותה יותר ע"י Linq to Sql אלא ע"י Linq האב בצד הלקוח.

סיכום

במאמר זה למדנו והדגמנו את הדרכים ליצירת שאילתות בזמן ריצה, קינון השאילתות הדינאמיות בשאילתות Linq רגילות, ותיארנו באופן כללי איך פועל Linq to Sql.

Tags: , ,

.NET 3.5

הערות

14/08/2009 15:30:22 #

drvvv

מאמר מעניין מאוד!
איפה אפשר למצוא מידע על ניסוחי השאילתות ב-linq?

drvvv

הוסף תגובה


(יציג את האייקון ה-Gravatar שלך)

  Country flag

biuquote
  • הערה
  • תצוגה מקדימה
Loading