מדריך| פיתוח ב ASP.NET – מיפוי מידע מבסיס נתונים לאובייקטים

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

ניקח לדוגמא את טבלת המשתמשים מהמדריך הקודם.
נניח ואנו רוצים למפות את כל המידע לתוך פקד (ASP.NET Repeater), נוכל לרשום דבר כזה:

קוד:
SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["conString"].ConnectionString); string sql = "SELECT * FROM Users"; SqlCommand cmd = new SqlCommand(sql, connection); SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataSet ds = new DataSet(); adapter.Fill(ds); repeater.DataSource = ds; repeater.DataBind();

כמו במדריך קודם, אנחנו שולפים את הנתונים ל DataSet אבל הפעם אנחנו לא פולטים את ה DataSet שורה שורה אלא שולחים אותו ל Repeater.
בקוד HTML אנחנו רושמים:

קוד:
<asp:Repeater ID="repeater" runat="server"> <ItemTemplate> <tr> <td><%# Eval("ID") %></td> <td><%# Eval("Username") %></td> <td><%# Eval("Password") %></td> </tr> </ItemTemplate> </asp:Repeater>

שימו לב שבערת הפונקציה Eval אנחנו יכולים לגשת לעמודות ב DataSet.
ה ASP.NET Repeater יודע אוטומית מה לעשות כאשר הוא מקבל DataSet.

אובייקט של משתמש

ניצור תיקייה חדשה בשם App_Code (לחיצה ימינית על הפרוייקט -> Add -> Add ASP.NET Folder -> App_Code)
בתוך App_Code ניצור תיקייה בשם Entities וניצור שם מחלקה חדשה בשם User.cs

נירצה לעצב את המחלקה User כך שהיא תכיל את כל התכונות שיש ל User בבסיס הנתונים (ID, Username, Password):

קוד:
class User { public int ID { get; set; } public string Username { get; set; } public string Password { get; set; } }

כעת, בעמוד שלנו (Default.aspx) נוכל ליצור מופעים של המחלקה User ולמלא אותם במשתמשים:

קוד:
User userA = new User(); userA.ID = 1; userA.Username = "arik312"; userA.Password = "32ddsf"; User userB = new User(); userB.ID = 2; userB.Username = "xpc234"; userB.Password = "5sdfsd3";

נוסיף את המופעים לרשימה:

קוד:
List<User> users = new List<User>(); users.Add(userA); users.Add(userB);

כעת נראה שנוכל לשלוח ל Repeater את רשימת המשתמשים כמו שיכולנו לשלוח את ה DataSet:

קוד:
repeater.DataSource = users; repeater.DataBind();


DataSet מול אובייקטים

אז מהם למעשה ההבדלים בין DataSet לבין מה שעשינו פה?

DataSet היא מחלקה כללית ללא אובייקט (מטרה) מסויים.
DataSet היא מאד גמישה, הרשומות נשמרות בתוך טבלה שהעמודות שלה יכולות להשתנות בהתאם לאיך שהן מסודרות בבסיס הנתונים.
DataSet נקרא לפעמים Loosly typed object, אין ל DataSet מטרה ספציפית.

ההיתרון של DataSet הוא שקל מאד למלא אותו, קל מאד לקבל אותו מבסיס נתונים והוא גם מציג שיטות לעידכון מידע בבסיס נתונים (כמו שראינו במדריך הקודם).

החיסרונות של DataSet:

  • מאחר ואין לו אובייקט, קשה מאד לנהל אותו בצורה יחודית, כל DataSet ינוהל בצורה כללית בלי יחס למידע שאותו הוא מייצג.
  • מאחר ואין לו אובייקט והוא גמיש מאד, כל שגיאה (למשל גישה לשדה שלא קיים) תגרור שגיאת זמן ריצה ולא שגיאת קומפילציה, מה שהופך את הטיפול בשגיאות לקשה מאד.
  • קשה יותר להפריד בין שכבת המשתמש לשכבת הלוגיקה עם DataSets (פירוט במדריך הבא).

לעומת זאת, במקום להשתמש ב DataSets נוכל להשתמש באובייקטים שהם מופעים למלקחות שאנחנו מגדירים מראש (כמו המחלקה שהגדרנו למשתמשים).
כמו ש DataSet שומר רשימה של רשומות מבסיס הנתונים, נוכל לשמור רשימה של האובייקטים שהגדרנו.

אובייקטים כאלה נקראים Strict typed objects, הם מגדירים מטרה מאד ספציפית ומשמשים לשימור של אובייקטים מוגדרים מראש.
בנוסף לכך, קל מאד לבצע מניפולציות על אובייקטים כאלו מה שהופך את ניהול בסיס הנתונים להרבה יותר גמיש.

שליפה מבסיס הנתונים

ראינו כבר איך ניתן לשלוף אל DataSet מבסיס הנתונים.
כעת נראה כיצד ניתן לשלוף אל רשימה של אובייקטים מבסיס הנתונים.

נניח ואנחנו רוצים את כל המשתמשים מבסיס הנתונים אל רשימה של המחלקה User שנקראת users.
נפתח חיבור לבסיס נתונים ואז ניצור DataReader:

קוד:
string sql = "SELECT * FROM Users"; SqlCommand cmd = new SqlCommand(sql, connection); connection.Open(); SqlDataReader reader = cmd.ExecuteReader();

כעת נקרא את כל השורות בטבלה של המשתמשים ועבור כל שורה ניצור אובייקט של משתמש שנמלא אותו בערכים של השדות ונכניס אותו לרשימה של המשתמשים:

קוד:
while(reader.Read()) { User user = new User(); user.ID = (int)reader["ID"]; user.Username = reader["Username"].ToString(); user.Password = reader["Password"].ToString(); users.Add(user); }

(שימו לב ש DataReader הוא רשימה של משתנים מסוג object ולכן יש צורך בהמרה לסוג הרצוי).
וכמובן שבסוף נסגור את החיבור לבסיס הנתונים ע"י connection.Close().

DataSet מול DataReader

אתם כנראה שואלים את עצמכם למה לא שלפתי קודם כל את המידע אל אובייקט של DataSet ורק אז מ DataSet למלא את הרשימה של האובייקטים של המשתמשים.
בשביל זה כדאי שתיקראו את הדוקומנטציה באתר של מיקרוסופט על שימוש ב DataAdapter כדי למלא DataSet:
http://msdn.microsoft.com/en-us/libr…vs.110%29.aspx

The Fill method uses the DataReader object implicitly to return the column names and types that are used to create the tables in the DataSet, and the data to populate the rows of the tables in the DataSet.

כלומר, מאחורי הקלעים DataAdapter משתמש ב DataReader כדי למלא את ה DataSet הנתון.
DataAdapter היא מחלקה ממש מורכבת ו DataAdapter.Fill היא פונקציה מאד מורכבת אך כדי להעביר את הנקודה, ננסה להמחיש את אופן הפעולה שלה:

קוד:
void Fill(DateTable dt) { connection.Open(); SqlDataReader reader = cmd.ExecuteReader(); while(reader.Read()) { foreach(Key key in reader.Keys) dt[key] = reader[key]; } connection.Close(); }

(ל DataAdapter.Fill יש גירסה שמקבלת DataTable ולא DataSet, מאחר ו DataSet יכול להכיל מספר רב של טבלאות חשבתי שיהיה יותר פשוט להמחיש את אופן הפעולה של DataAdapter.Fill על טבלה יחידה).

פה אני מנסה להמחיש לכם את אופן הפעולה של DataAdapter.Fill. כמובן שלא מדובר בקוד האמיתי, הקוד האמיתי הרבה יותר מורכב אבל אני רק רוצה להמחיש לכם את האופן שבו DataAdapter משתמש כבר ב DataReader כדי למלא את ה DataSet.

אז במה עדיף להשתמש, ב DataReader או ב DataSet?
השאלה הזאת תלויה מאד במטרה של התוכנית.

אם מטרת התוכנית היא לשלוף את הנתונים מבסיס הנתונים בדיוק כפי שהם מוצגים ואז להציג אותם למשתמש או לערוך אותם ישירות אז לדעתי מומלץ להשתמש ב DataSet.

אם המטרה שלנו היא למלא אובייקטים שיצרנו מראש במידע מבסיס נתונים (כמו שעשינו עם הדוגמא של הרשימה של המשתמשים) אז מומלץ להשתמש ב DataReader.
הסיבה לכך היא: אם נשתמש ב DataSet למטרה זו, בכל זאת נצתרך אחרי זה לעבור שוב בלולאה על ה DataSet כדי לשלוף את המידע שלו אל האובייקטים.
אך אנחנו יודעים כבר שמאחורי הקלעים, אופן שליפת הנתונים אל DataSet מתבצע ע"י DataReader, לכן שימוש ב DataSet למטרה זו יצור מצב של קוד כפול:

1. שליפת נתונים מבסיס נתונים אל DataSet
2. שליפת נתונים מ DataSet אל רשימה של אובייקטים

במקום:

1. שליפת נתונים מבסיס הנתונים אל רשימה של אובייקטים
2. זהו.

קישורים בין טבלאות

כמו שראינו במדריך הראשון בסידרה, ניתן ליצור קשרים בין טבלאות שונות בבסיס התונים, קשר יחיד לרבים, קשר רבים לרבים…
נראה כעת כיצד ראוי לטפל בקשרים אלו בעת מיפוי מידע מבסיס הנתונים אל אובייקטים.

ניזכר במערכת הפשוטה של המשתמשים והחיות:

פיתוח ב ASP.NET - מיפוי מידע מבסיס נתונים לאובייקטים

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

נוכל ליצור מחלקה כזו:

קוד:
class Pet { public int ID { get; set; } public string PetName { get; set; } public string Type { get; set; } public int Owner { get; set; } }

ממבט ראשון, המודל הזה נראה בסדר לחלוטין עד שאנחנו מתחילים לחשוב מה המטרה בכלל בלמפות טבלאות מבסיס הנתונים אל אובייקטים מוגדרים מראש (נקרא לפעמים גם ORM).

המטרה היא ליצור סביבת עבודה שמאפשרת עבודה עם אובייקטים בלי מגע ישיר עם בסיס הנתונים או עם מושג שקשור לבסיס הנתונים שלנו.

כשאנחנו מסתכלים על השדה public int Owner אנחנו רואים ניסיון לקשר חיה מסויימת אל בעלים שלה אך האופן שבו הקישור הזה נעשה הוא אופן שמסגיר את מנגנון הפעולה של בסיס הנתונים שלנו.

המתכנת שאחראי על טיפול באובייקטים לא צריך לדעת ש Owner מייצג את המפתח הראשי של המשתמש שאליו החיה שייכת, אותו מתכנת פשוט צריך לדעת ש Owner מייצג של המשתמש שאליו החיה שייכת ומאחר שלמתכנת יש גישה למחלקה User, הוא יידע איך לטפל בבעלים של החיה.

לכן במקום הקוד למעלה, יותר נכון לרשום מחלקה כזו:

קוד:
class Pet { public int ID { get; set; } public string PetName { get; set; } public string Type { get; set; } public User Owner { get; set; } }

קוד ליצירת רשימה של משתמשים:

קוד:
List<User> PopulateUsers(string Sql) { List<User> list = new List<User>(); SqlConnection connection = new SqlConnection("Connection string…"); SqlCommand cmd = new SqlCommand(Sql, connection); connection.Open(); SqlDataReader reader = cmd.ExecuteReader(); while(reader.Read()) { User user = new User(); user.ID = (int)reader["ID"]; user.Username = reader["Username"].ToString(); user.Password = reader["Password"].ToString(); list.Add(user); } connection.Close(); return list; }

הקוד זהה למה שעשינו מקודם כדי למלא רשימה של משתמשים מבסיס הנתונים, אך הפעם שמנו את הקוד בתוך פונקציה.

קוד ליצירת רשימה של חיות:

קוד:
List<Pet> PopulatePets(string Sql) { List<Pet> list = new List<Pet>(); SqlConnection connection = new SqlConnection("Connection string…"); SqlCommand cmd = new SqlCommand(Sql, connection); connection.Open(); SqlDataReader reader = cmd.ExecuteReader(); while(reader.Read()) { Pet pet = new Pet(); pet.ID = (int)reader["ID"]; pet.PetName = reader["PetName"].ToString(); pet.Type = reader["Type"].ToString(); User owner = PopulateUsers("SELECT * FROM Users WHERE ID = " + reader["Owner"])[0]; pet.Owner = owner; list.Add(pet); } connection.Close(); return list; }

פה אנחנו עושים פחות או יותר אותו דבר כמו שאנחנו עושים עם המשתמשים אך הפעם עבור כל חיה, אנחנו משתמשים בפונקציה שיצרנו ליצירת רשימת משתמשים כדי להשיג אובייקט של הבעלים של החיה ואז אנחנו שומרים אותו בתוך האובייקט של החיה.

במדריך הבא נדבר יותר על הפרדת חלקי קוד ומשימות