שאלה:
רציתי לדעת מה הדרך הנכונה יותר מבחינת ארכיטקטורה להריץ שאילתות דינמיות במערכת מבוססת שכבות ( Business logic ו -Data access ) כלומר,
אם יש ממשק משתמש בעל כמה שדות שונים וברצוני להריץ שאילתה אך ורק עם אותם שדות שמולאו מבלי להחזיק את משפט הSQL כתוב בקוד.
תשובה:
שאילתות Find הן אכן נושא מסובך. מסכי חיפוש כאלו באמת מאתגרים ובוחנים את הכוח של הארכיטקטורה שלנו.
דבר ראשון בנושא קבלת "החלטות ארכיטקטורה נכונות" צריך לדעת מה הארכיטקטורה הנוכחית הרי נושא כמו שאילתות Find למיניהן לא תקבע לנו את הארכיטקטורה.
מצב ראשון הוא בו הישויות העסקיות שלנו מוטמעות בתוך ה-DAL שלנו.
כלומר, לפי הדוגמה הידועה שלי על ניהול משק פרות, כאן יהיה לנו Cow ויהיה לנו CowDAL, כאשר CowDAL יחשוף מתודות כמו Create, Update, Delete שמקבלות פרה. כמו כן יהיה לנו משהו כמו GetAll שמחזיר מערך של פרות. עכשיו השאלה שלך היא איך נממש את CowDAL.Find שמחזיר מערך של פרות ומקבל פרמטרים. דבר ראשון שאני עושה כאן, זה ליצור מתודה ספציפית למסך חיפוש הספציפי שלנו בתוך CowDAL. משהו כמו FindAllCowsForMainSearchSreenASPX. כן, יש כאן אלמנט שאנחנו נותנים ל-GUI שלנו להכתיב את המבנה של ה-DAL, אבל באמת שהמבנה של ה-GUI מחייב מאתנו התייחסות ספציפית ב-DAL.
במתודה הזו נקבל את *כל* הפרמטרים שנקלטים בטופס (שם הפרה, גיל מינימלי לפרה, גיל מקסימלי לפרה, אחו של הפרה וכיו"ב). כל פרמטר שלא הוזן ישלח כ-Null ולכן גם נצטרך לעבוד עם Nullable Types.
המתודה תבנה דינמית SQL (או יותר טוב, תאציל את הבנייה הדינמית למחלקה שבאמת יודעת לטפל בזה), תבצע את השאילתא הדינמית ותבנה לפי התוצאות את מערך הפרות.
דוגמה למחלקת עזר כזו שבונה שאילתות בניתי אחרי 10 דקות שעבדתי בדוט נט. מה שאנחנו רואים למטה זה קובץ XML שהמטרה שלו היא להראות את המבנה של השאילתא. לכל שאילתא יש StaticParams שאלו פרמטרים שתמיד יהיו בשאילתא, ו-OptionalLines שיהיו קיימים אם ורק אם הפרמטרים בהם הם מותנים סופקו למחולל השאילתות. הרעיון הוא לשלוח מערך של נתונים למחולל השאילתות והוא כבר ידע לבנות את זה לבד. ההיגיון מאחורי זה גם מאוד קל ופשוט להבנה: ניצור מתודה למטרות חיפוש שהיא Strong typed מבחינת הפרמטרים ב-DAL, שתעביר את מספר השאילתא (או איזה מזהה כזה) ואת הפרמטרים הקיימים למחולל השאילתות שיחזיר לפי הוראות בנייה מראש את השאילתא.
<query>
<num>1</num>
<details>
<desc>שאילתא שתחזיר את המוציאים לאור</desc>
<type>דינימי</type>
<WebFormNames>WebForm1.asp,WebForm3.asp</WebFormNames>
</details>
<StaticParams>
<Param>
<ParamName>pub_id</ParamName>
<DataType>System.Data.SqlDbType.VarChar</DataType>
<Size>4</Size>
</Param>
<Param>
<ParamName>pub_id</ParamName>
<DataType>System.Data.SqlDbType.VarChar</DataType>
<Size>4</Size>
</Param>
</StaticParams>
<OptionalLines>
<Line>
<LineText>And City=@city</LineText>
<LineSimul>{O1}</LineSimul>
<Param>
<ParamName>City</ParamName>
<DataType>System.Data.SqlDbType.VarChar</DataType>
<Size>20</Size>
</Param>
</Line>
<Line>
<LineText>And country=@country</LineText>
<LineSimul>{O2}</LineSimul>
<Param>
<ParamName>country</ParamName>
<DataType>System.Data.SqlDbType.VarChar</DataType>
<Size>30</Size>
</Param>
</Line>
<Line>
<LineText>And state=@state</LineText>
<LineSimul>{O3}</LineSimul>
<Param>
<ParamName>state</ParamName>
<DataType>System.Data.SqlDbType.VarChar</DataType>
<Size>2</Size>
</Param>
</Line>
<Line>
<LineText>And Pub_name like '%@pub_Name%'</LineText>
<LineSimul>{O4}</LineSimul>
<Param>
<ParamName>pub_Name</ParamName>
<DataType>System.Data.SqlDbType.VarChar</DataType>
<Size>40</Size>
</Param>
</Line>
</OptionalLines>
<SQL>
Select City,
Country,
State,
Pub_name,
pub_id
From Publishers
Where IsNumber(Pub_id)
and Pub_id = @pub_id
{O1}
{O2}
{O3}
{O4}
</SQL>
</query>
מצב שני הוא בו ה-DAL שלנו מוטמע בתוך הישויות העסקיות.
לפי הדוגמה למעלה אין לנו בכלל CowDAL אלא על Cow עצמה יושבות מתודות כמו Create, Update ו-Delete שספצפיות למופע אחד של פרה (בניגוד ל-CowDAL שיעבוד עם מתודות סטטיות). כמו כן יהיו לנו כמה מתודת סטטיות כמו GetAllCows שיחזיר את כל הפרות. עכשיו השאלה היא שוב, מה עושים עם ה-Find (הסטטי) שלנו?
בדומה לקודם, נבנה מתודה ספציפית למסך החיפוש הספציפי שלנו שמקבלת את כל הפרמטרים שעשויים להישלח ואת אלו שהם null לא נוסיף לשאילתא.
נבנה את השאילתא הדינמית לפי הנתונים שנשלחו (וכמובן שעדיף שמחלקה אחרת תבצע את הבנייה הדינמית לפי הגדרות מראש כמו שהראנו קודם), נבצע אותה ונפרמט את התוצאות לפרות.
עכשיו עולה שאלה יותר מעניינת, למה הפרדתי בין שתי המקרים אם בפועל עושים בהם בדיוק אותו דבר?
במקרה השני (שבו ה-DAL מוטמע בתוך הישויות העסקיות) אפשר ורצוי לשלב Presistence layers או ORM. כלומר, שאת המימוש של מחולל השאילתות נשאיר לאיזה פריימוורק שאחראית על בניית השאילתא ואנחנו רק נדאג לדבר איתה בשפה שהיא מבינה. מרבית ה-ORMים יתנו לך איזה ממשק פאסודו-SQLי כזה למקרים בהם אתה רוצה להזין פרמטרים. למשל לא צריך אפילו להגיע למצב של מסך חיפוש, אלא מספיק לשאול "איך אני מחפש את כל הפרות ששם עדנה?" ושם כבר חייב להתגלות ממשק שבו אפשר לבקש את כל הפרות עם פרמטר מסויים. לדוגמה ActiveRecord מאפשר שימוש ב-NHibernate Expressions והקוד שלנו יראה משהו כזה:
public static Cow[] FindAllWithTheseProperties(string Name, decimal? ID,
, DateTime? BetweenBirthDate, DateTime? AndThisBirthDate)
{
List<ICriterion> Crit = new List<ICriterion>();
if (ID != null)
Crit.Add(Expression.Eq("Id", ID));
if (!string.IsNullOrEmpty(Name))
{
SimpleExpression FirstName = Expression.Like("FirstName", Name, MatchMode.Anywhere);
SimpleExpression FamilyName = Expression.Like("FamilyName", Name, MatchMode.Anywhere);
ICriterion ExpressionThatSearchsOneNameInAll = Expression.Or(FirstName, FamilyName);
Crit.Add(ExpressionThatSearchsOneNameInAll);
}
if (BetweenBirthDate != null)
if (AndThisBirthDate != null)
Crit.Add(Expression.Between("BirthDate", BetweenBirthDate,AndThisBirthDate));
else
Crit.Add(Expression.Eq("BirthDate", BetweenBirthDate));
List<Order> order = new List<Order>();
order.Add(Order.Desc("FirstName"));
order.Add(Order.Desc("Id"));
return ((Cow[])(ActiveRecordBase.FindAll(typeof(Cow), order.ToArray(), Crit.ToArray())));
}
צריך לשים לב שלמעשה מה שקיבלנו (ומה שנקבל בעוד ORMים אחרים) זה ממשק פאסודו-SQL. למען האמת אנחנו עובדים הכי קרוב לברזלים שאפשר, אנחנו כותבים Expression.Eq ל-ID בידיעה ברורה שזה יוביל למשפט SQL מאוד ספציפי. בהקשר של מסך חיפוש, אנו גם בודקים ש-ID לא ריק וככה השאילתא באמת בונה את עצמה דינמית.
אחר כך ננסה לחפש בשם הפרטי ושם המשפחה של הפרה לשם (אם סופק אחד כזה), ננסה לחפש את כל הפרות שנולדו בתאריך לידה ספציפי אם סופק רק תאריך לידה אחד, ואם סופקו 2 תאריכי לידה אז נחפש ביניהם. אחר כך ניצור את הסידור של מערך הפרות שלנו.
כל הזמן הזה אנחנו עובדים בפאסודו SQL ברמת האפליקציה אבל שאנחנו בונים את הקריטריונים לשאילתא אנחנו יודעים לעבוד עם פרמטרים שלא סופקו בזה שאנחנו פשוט לא משלבים אותם לחיפוש.
קישור להמשך הדיון המאוד מעניין (מומלץ בחום): http://www.tapuz.co.il/tapuzforum/main/Viewmsg.asp?forum=831&msgid=87733298