יום שבת, נובמבר 6

מבוא ל-SQLite


מה זה SQLite ובעצם בשביל מה צריך את זה?
SQLite הוא מנוע לניהול בסיס נתונים (Database).
מהו בסיס נתונים?
בסיס הנתונים הוא בסופו של דבר קובץ בו נרשמים הנתונים אותם צריכה האפליקציה לארגן ולשמור לטווח ארוך. דוגמאות לבסיסי נתונים: קטלוג מוצרים, רשימת לקוחות ופרטיהם.
האם אי אפשר להסתדר בלי מנוע בסיסי נתונים?
במקום להשתמש במנוע מוכן, דוגמת ה-SQLite לטיפול בבסיסי הנתונים, אפשר היה לבנות לסדר ולשמור טבלאות בקבצים, תוך שימוש בפקודות ג'אווה רגילות. על פי שיטה זו היה עלינו לדאוג לכתוב את כל הקוד הדרוש לטיפול בבסיס הנתונים כולל יכולות עידכון הנתונים, שליפה, מחיקה, חיפוש, שמירה וכו'. בהחלט עבודה מייגעת שמנוע בסיס הנתונים מבצע וחוסך מאיתנו.
מבנה בסיס הנתונים
בסיס הנתונים מאורגן בטבלאות. הטבלה מכילה אשומות, כשכל רשומה נקרא שורה - row. כל אחת מהרשומות\שורות מורכבת משדות או עמודות. דוגמא: ספר טלפונים. שם כל רשומה היא שורה, וכל שורה מורכבת מעמודות: שם, שם משפחה, רחוב, מספר, עיר, מס" טלפון.
מדוע SQLite דווקא?
SQLite הוא מנוע database מאד פופולרי אם לא הנפוץ ביותר בעולם. שלוש סיבות עיקריות לפופולריות שלו:
  1. הוא בחינם וחופשי לגמרי.
  2. הוא קומפקטי. הגרסה הנוכחית שלו תופשת כ- 150KB בלבד.
  3. הוא פשוט: אינו דורש server, אין אדמיניסטרציה מסובכת, אין קבצי config.
לשימחתינו הוא מובנה באנדרואיד. הוא נמצא כמובן גם באייפון, בסימביאן ובעוד מקומות רבים.
אני לא יכול שלא להוסיף עוד התייחסות לעובדה שהוא חינמי: הוא לא רק בחינם אלא אף מעבר לכך - אין צורך בשום רשיון או התחייבות. בעצם, במקום רשיון, בכל אחד מקבצי המקור של ה-SQLite מצורף הטקסט הבא: (מועתק מקובץ מקור)


** The author disclaims copyright to this source code.  In place of
** a legal notice, here is a blessing:
**
**    May you do good and not evil.
**    May you find forgiveness for yourself and forgive others.
**    May you share freely, never taking more than you give.
**
*************************************************************************

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

קישור לאתר הרישמי של SQLite.
מעבר לכך - השימוש SQLite לא מסובך.

בניית בסיס נתונים עם SQLite
נציג דוגמא לצורך הכרות ראשונית עם ה- SQLite. הדוגמא תשתמש ישירות במתודות של SQLite.  בפוסט המשך, נכיר class נוסף שאנדרואיד  מספק class ושמו SQLiteOpenHelper. הוא מפשט את השימוש בבסיס הנתונים, בין השאר ע"י הסתרת ה-API והצגת API פשוטים יותר.
אך נתחיל עם דוגמא פשוטה שאינה משתמשת  ב- Helper.
הדוגמא הבאה תציג בסיס נתונים עבור פנקס טלפונים. הכוונה כאן להציג דוגמא פשוטה, כך שלא "נתאמץ" להציג  UI מורכב ומרשים, אלא נתמקד בהדגמת בניית בסיס נתונים. בפוסטים ממשיכים נשכלל בהדרגה את המערכת.
תהליך העבודה בדוגמא הבאה יהיה לפי המתכונת הבאה:
  1. יצירת בסיס הנתונים.
  2. יצירת טבלה השייכת לבסיס הנתונים.
אחרי יצירת הטבלה, נבצע את הפעולות הבאות:
  1. הוספת רשומות לטבלה.
  2. שליפת רשומות מהטבלה והצגתן בפורמט ListView
  3. מחיקת כל הרשומות מהטבלה וסגירתה יחד עם כל בסיס הנתונים. 
    אחרי שלב 2 הנ"ל, יראה ה-UI כך:


      בתמונה אפשר לראות 4 רשומות, מתוכן אחת בעברית - רק לצורך הוכחת יכולת. קוד ה-java כולל class יחיד, כשהביצוע העשה במתודה onCreate, ומתודת עזר קטנה נוספת בשם mFillDbsTable למילוי הטבלה.
      לשם הנוחות הקוד מובא להלן, ובהמשכו נסביר את החלקים המעניינים.
      מצא קישור להורדת קבצי המקור בתחתית העמוד.

      1. public class mySqlIntro extends ListActivity {
      2.     private final String DB_NAME = "mcustomerDbName";
      3.     private final String TABLE_NAME = "mcustomeTableName";
      4.     SQLiteDatabase customersDB = null;
      5.     /** Called when the activity is first created. */
      6.     @Override
      7.     public void onCreate(Bundle savedInstanceState) {
      8.         super.onCreate(savedInstanceState);
      9.         ArrayList<String> results = new ArrayList<String>();
      10.         try {
      11.             customersDB =  this.openOrCreateDatabase(  DB_NAME, MODE_PRIVATE, null);
      12.             customersDB.execSQL("CREATE TABLE IF NOT EXISTS " +
      13.                     TABLE_NAME +
      14.                     " (Name VARCHAR," +
      15.                     " Street VARCHAR, Block INT, City VARCHAR, Tel VARCHAR);");
      16.             mFillDbsTable();
      17.              Cursor c = customersDB.rawQuery("SELECT Name, Street, Block, City, Tel  FROM "    +
      18.                     TABLE_NAME +
      19.                        " where Name == 'David' or Name == 'Yosi' or Block == 51 LIMIT 5", null);
      20.             if (c != null ) {
      21.                 if  (c.moveToFirst()) {
      22.                     do {
      23.                         String name = c.getString(c.getColumnIndex("Name"));
      24.                            String street = c.getString(c.getColumnIndex("Street"));
      25.                             int block = c.getInt(c.getColumnIndex("Block"));
      26.                               String city = c.getString(c.getColumnIndex("City"));
      27.                             String tel = c.getString(c.getColumnIndex("Tel"));
      28.                             results.add(name + ", " + street + "  "+block+ " "+ city+" "+tel );
      29.                     }while (c.moveToNext());
      30.                 }
      31.             }
      32.             setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,results));
      33.          } catch (SQLiteException se ) {
      34.             Log.e(getClass().getSimpleName(), "create/Open the database problem");
      35.         } finally {
      36.             if (customersDB != null)
      37.                 customersDB.execSQL("DELETE FROM " +   TABLE_NAME);
      38.                 customersDB.close();
      39.         }
      40.     }
      41.   
      42.     private void mFillDbsTable(){
      43.         try {
      44.             customersDB.execSQL("INSERT INTO " +
      45.                 TABLE_NAME +
      46.                 " Values ('Yosi','Jabotinsky', 2,'Tel Aviv','052-1232323');");
      47.             customersDB.execSQL("INSERT INTO " +
      48.                 TABLE_NAME +
      49.                 " Values ('David', 'Herzl', 3,'Afula','050-322323');");
      50.             customersDB.execSQL("INSERT INTO " +
      51.                 TABLE_NAME +
      52.                 " Values ('Haim','Sokolov', 5, 'Holon','054-3224111');");
      53.             customersDB.execSQL("INSERT INTO " +
      54.                 TABLE_NAME +
      55.                 " Values ('Idan','Weitzman', 51, 'Kfar-Saba','057-3276232');");
      56.             customersDB.execSQL("INSERT INTO " +
      57.                 TABLE_NAME +
      58.                 " Values ('שי','וויצמן', 51, 'כפר סבא','057-3276232');");
      59.         }
      60.         catch (SQLiteException se ) {
      61.             Log.e(getClass().getSimpleName(), "Could not create records");
      62.         }
      63.     }
      64.  }

      ראשית נציין את העובדה שה-   mySqlIntro extends ListActivity . זה יאפשר לנו לרשת  את המתודה setListAdapter להצגת בסיס הנתונים כרשימה ב- UI.
      נבחן את המתודה onCreate. כפי שכבר צויין למעלה היא מבצעת את הפעולות הבאות:
      1. יצירת בסיס הנתונים,
      2. יצירת הטבלה,
      3. הכנסת הנתונים לטבלה (זה יתבצע ע"י רוטינת העזר, רק כדי לפשט את הקוד), 
      4. גישה לטבלה עם שאילתה (query) והצבעה על התוצאות שהתקבלו עם סמן = ה-Cursor.
      5. העברת הנתונים למערך.
      6. הצגת המערך ע"י ListView ב-UI.
      7. מחיקת הטבלה.
      שלב 1: יצירת בסיס הנתונים - שורה 11 למעלה:

      customersDB =  this.openOrCreateDatabase(  DB_NAME, MODE_PRIVATE, null); 
      בסיס הנתונים שנוצר נשמר בקובץ בתוך:
      /data/data/<package name>/databases/

      MODE_PRIVATE מציין רק האפליקציה הזו תוכל לגשת לבסיס הנתונים.

      שלב 2: יצירת הטבלה - שורה 12 למעלה:
      1.            customersDB.execSQL("CREATE TABLE IF NOT EXISTS " +
      2.                     TABLE_NAME +
      3.                     " (Name VARCHAR," +
      4.                     " Street VARCHAR, Block INT, City VARCHAR, Tel VARCHAR);");
      בקטע הקוד הנ"ל בשורות 3-5 מוגדרות העמודות של כל שורה בטבלה, סה"כ 5 עמודות: Name, Street, Block, City, Tel. . שימו לב, לכל שדה מוגדר בוג המשתנה, בין אם זה VARCHAR או INT. למרות הגדרה זו, המערכת לא תתריע אם יכתבו העמודות אלה ערכים שאינם מתאימים לסוג שהוכרז כאן. יש להיות מודעים ולהזהר מטעויות כאן.

      שלב 3: הכנסת הנתונים לטבלה, מתבצע בתוך מתודת העזר: mFillDbsTable
      מוכנסות לטבלה 4 שורות המיצגות 4 רשומות בספר הטלפונים.
      שלב 4: שאילתה מהטבלה לצורך שליפת נתונים ממנה.

        Cursor c = customersDB.rawQuery("SELECT Name, Street, Block, City, Tel  FROM "    TABLE_NAME + " where Name == 'David' or Name == 'Yosi' or Block == 51 LIMIT 5", null);

      המתודה rawQuery מבצעת שאילתה ושולפת את כל 5 העמודות מכל אחת מהשורות שיענו אחד משלושת התנאים לפחות:
      Name == 'David' or Name == 'Yosi' or Block == 51
      במקרה, כל 4 השורות ענו על התנאים לכן כולן נשלפו לתוך האובייקט מסוד Cursur, שהוא למעשה Interface המאפשר גישה רנדומלית לתוצאות שהחזיר בסיס הנתונים. ה- cursor מצביע על כל הנתונים שהתקבלו בתשובה לשאילתה.

      שלב 5: כעת נעתיק את הנתונים שהתקבלו בשאילתה לתוך מערך נתונים. זה בהחלט לא פיתרון שיתאים עבור בסיסי נתונים גדולים שכן הוא בזבזני בזיכרון. בכל זאת בתרגיל שלנו נשתמש בהעתקת התוצאות לטבלה. ההעתקה מתבצעת בלופ הבא:
      1.          if (c != null ) {
      2.                 if  (c.moveToFirst()) {
      3.                     do {
      4.                         String name = c.getString(c.getColumnIndex("Name"));
      5.                            String street = c.getString(c.getColumnIndex("Street"));
      6.                             int block = c.getInt(c.getColumnIndex("Block"));
      7.                               String city = c.getString(c.getColumnIndex("City"));
      8.                             String tel = c.getString(c.getColumnIndex("Tel"));
      9.                             results.add(name + ", " + street + "  "+block+ " "+ city+" "+tel );
      10.                     }while (c.moveToNext());

      תאור הלופ: מתחילים ברשומה הראשונה עליה מצביע ה- Cursur (שורה 2) וממשיכים לסרוק את ה-Cursur כל עוד יש לו שורות נוספות עליהן הוא מצביע (שורה 10).
      בתוך הלופ מעתיקים את 5 העמודות לתוך 5 משתנים פרימיטיבים (שורה 4-8), ומסדרים הכל במערך  result (שורה 9).


      בשלב הבא נסרוק את האלמנטים עליהם מצביע ה-Cursor ונעתיק אותם לאובייקט הListView של ה-UI כך שהכל יוצג על הצג.

      שלב 6: העברת המערך results לתצוגה ב-ListView. השתמשנו כבר במתודה setListAdapter בפרק שהציג את ה-ListView.  גם שם עשינו שימוש ב-Layout סטנדרטי של האנדרואיד, שה-id שלו מיוצג ע"י  simple_list_item_1:

      setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,results));

      שלב 7: לבסוף, מחיקת הטבלה וסגירת ה-database.
      1.      } finally {
      2.             if (customersDB != null)
      3.                 customersDB.execSQL("DELETE FROM " +   TABLE_NAME);
      4.                 customersDB.close();
      5.         }
      6.  
      התחלנו בהצגת המושג בסיס נתונים, וסיימנו בדוגמה שיצרה טבלת נתונים ואף הציגה אותה. נמשיך ללמוד עוד על בבסיסי נתונים בקרוב. נכיר את מחלקת העזר SQLiteOpenHelper שתקל עלינו את יצירת בסיסי הנתונים.
      קישור להורדת קבצי הפרויקט.

      5 תגובות:

      1. יפה מאוד רונן!! ממש אהבתי את כול הפוסטים שלך..

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

        השבמחק
      3. שלום אתה יכול לעזור בנושא FACE RECOGNITION ?

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

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

        השבמחק
      5. תודה...תמשיך עם הפוסטים...ממש מכרה זהב

        השבמחק