التعامل مع SQLite في Android
بسم الله الرحمن الرحيم
كثير من التطبيقات تحتاج إلى تخزين في البيانات اندرويد, كمذكرة, أو أسماء وأرقام أو تواريخ وغيرها, في اندرويد يوجد عدد من الخيارات لتخزين البيانات
كـ SharedPreferences لكنه كما هو مكتوب في Documentation .
اقتباسIf you have a relatively small collection of key-values that you’d like to save, you should use the SharedPreferences APIs.
إذا فهو مخصص لبيانات بسيطة, كحفظ اللغة التي اختارها المستخدم في التطبيق, لكن إذا كان حجم البيانات كبير, كحفظ عدد من المذكرات, أو تسجيل طلاب -وهو المثال الذي سوف نستعرضه-, سنحتاج لتعامل مع قواعد البيانات -Database-, في اندرويد نستخدم SQLite كقاعدة بيانات وفيها عدد من المميزات, فهي صغيرة الحجم, ولا تحتاج لأي إعدادت لاستخدامها, سريعة – فعلى سبيل المثال عند إدخال 25000 عنصر ستحتاج sqlite إلى 0.7, MySQL إلى 2.2, PostgreSQL إلى 4.9 ثانية.
لنبدأ إذا في المثال الخاص بنا وهو تطبيق فيه اسم الطالب والمادة المسجل فيها, وسنستعرض الأجزاء التي نحتاجها لكتابة هذا التطبيق.
في Documentation قسم التعامل مع قواعد البيانات إلى ثلاثة أقسام كالتالي
- تعريف Schema and Contract
- بناء قاعدة البيانات باستخدام SQLiteOpenHelper
- تطبيق عمليات CRUD على قاعدة البيانات
سأبدأ بخطوة, وهي تعريف class student بعد ذلك سنمر على هذه الخطوات, سيكون شكل class student كالتالي:
private String name;
private String course;
private String id;
public Student(String name, String course, String id) {
this.name = name;
this.course = course;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCourse() {
return course;
}
public void setCourse(String course) {
this.course = course;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
class بسيط جدا فيه ثلاث متغيرات, اسم الطالب, والمادة والمُعّرف.
تعريف Schema and Contract
بداية Schema نعني بها هيكل قاعدة البيانات, أو الشكل الخاص بها, ونعرفها داخل class نسميه contract بالطريقة التالية
public class StudentContract {
private StudentContract(){}
public static class StudentEntry implements BaseColumns {
public static final String TABLE_NAME = "student";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_COURSE = "course";
}
}
هناك العديد من التساؤلات بخصوص التعريف بهذه الطريقة, لذا دعنا نمر عليها
-
بداية تم تعريف private constructor , ببساطة لأنه ليس الغرض من هذا class أنتنشئ منه object وبهذا الأسلوب من التعريف أمنعك من أن تنشئ object.
-
inner class الغاية منه أن تعرف فيه اسم الجدول, وجميع الحقول الخاصه به, كم فعلنا مع Student Table, لماذا؟ أولا, يمكنك معرفة شكل الجدول بمجرد إلقاء نظرة على هذا class, ثانيا, تستخدم جميع المتغيرات التي عرفتها داخل هذا class في أي class أخر داخل التطبيق, وهذا يقلل من حدوث الخطأ, بالإضافة إلى أنه يسهل التعديل.
في حال أردنا إضافة جدول أخر, سنقوم بعمل inner class خاص به, لنفترض أننا نريد تعريف جدول لـ course سيكون شكله كالتالي
public class StudentContract {
private StudentContract(){}
public static class StudentEntry implements BaseColumns {
...
}
public static class CourseEntry implements BaseColumns {
public static final String TABLE_NAME = "course";
public static final String COLUMN_COURSE_NAME = "name";
public static final String COLUMN_COURSE_GRADE = "grade";
}
}
-
BaseColumns عبارة عن interface يعطيك id لكل عملية إدخال تقوم بها, بالإضافة إلى عدد rows لديك في الجدول, شكل interface كالتالي
public interface BaseColumns { /** * The unique ID for a row. * <P>Type: INTEGER (long)</P> */ public static final String _ID = "_id"; /** * The count of rows in a directory. * <P>Type: INTEGER</P> */ public static final String _COUNT = "_count"; }
قد لا يكون واضح الغاية والغرض منه لكن ثق تماما أنه بسيط وسيتضح لاحقا.
-
في حال أردنا تعريف أي متغير أو ثابت مشترك لجميع Tables فإنه يكون في class الـ contract, في حال أردنا تعريف أي متغير أو ثابت خاص بـ Table معين, فإنه يكون داخل inner class الخاص به.
بناء قاعدة البيانات باستخدام SQLiteOpenHelper
الان سوف ننتقل إلى بناء database الخاصة بنا, سنقوم في البداية بعمل extends لـ SQLiteOpenHelper وهو class سيساعدنا في بناء database, سيطلب منها عمل implement لثلاث دوال وهي
- onCreate(), والتي نستخدمها لإنشاء قاعدة البيانات الخاصة بنا.
- onUpgrade(), والتي نستخدمها في حال قمنا بإجراء أي تعديل على Schema الخاصة بالجدول.
- constructor يطابق super, سنمر عليه حالاً.
ليكون شكل class كالتالي
public class StudentHelper extends SQLiteOpenHelper {
public StudentHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
}
الان سنقوم بعمل implementation لهم دالة دالة, ونحدث الكود لنبدأ بالـ constructor, نلاحظ أنه يطلب اسم database – وليس اسم table -, بالإضافة إلى رقم النسخة الخاصة بـ database – تبدأ غالبا من 1, ومع كل تحديث تجريه على Schema الخاصة بالجداول تقوم بزيادة رقم الإصدار وهكذا-, و context و factory, سنقوم بتعريف متغيرات static لاسم واصدار Database ونجعلfactory بقيمة null, والـ context نحصل عليه عن طريق constructor
public class StudentHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "my_students.db";
public static final int DATABASE_VERSION = 1;
public StudentHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
}
لان سننتقل لـ onCreate هذه الـmethod تستدعى لمرة واحدة في أول إنشاء لـ Database , وسننفذ فيها أمر واحد فقط وهو إنشاء database, وسنقوم بعمل SQL query عادية جدا, ثم ننفذها داخل عن طريق db
public class StudentHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "my_students.db";
public static final int DATABASE_VERSION = 1;
public StudentHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String SQL_CREATE_ENTRIES = "CREATE TABLE " + StudentContract.StudentEntry.TABLE_NAME + " (" +
StudentContract.StudentEntry._ID + " INTEGER PRIMARY KEY," +
StudentContract.StudentEntry.COLUMN_NAME + " TEXT," +
StudentContract.StudentEntry.COLUMN_COURSE + " TEXT)";
db.execSQL(SQL_CREATE_ENTRIES);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
}
أما onUpgrade فيتم استدعاؤها إذا تغير رقم VERSION في التطبيق, وببساطة فهي تقوم بتحديث شكل الجدول عن طريق حذف الجدول السابق وإعادة إنشاء الجدول
public class StudentHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "my_students.db";
public static final int DATABASE_VERSION = 1;
public StudentHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String SQL_CREATE_ENTRIES = "CREATE TABLE " + StudentContract.StudentEntry.TABLE_NAME + " (" +
StudentContract.StudentEntry._ID + " INTEGER PRIMARY KEY," +
StudentContract.StudentEntry.COLUMN_NAME + " TEXT," +
StudentContract.StudentEntry.COLUMN_COURSE + " TEXT)";
db.execSQL(SQL_CREATE_ENTRIES);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + StudentContract.StudentEntry.TABLE_NAME);
onCreate(db);
}
}
وبهذا يكون اكتمل لدينا class StudentHelper.
تطبيق عمليات CRUD على قاعدة البيانات
CRUD هو اختصار لعمليات الـDatabase الرئيسية, الإنشاء CREATE, القراءة READ, التحديث UPDATE, الحذف DELETE, بعد أن قمنا بتعريف وبناء شكل قاعدة البيانات الخاصة بنا سنبدأ هنا في التعامل معها داخل الـ MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
نبدأ بـ CREATE والتي يقابلها INSERT في SQL, سنقوم بإضافة student لـ Database
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create database helper
StudentHelper mDbHelper = new StudentHelper(this);
// Gets the database in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(StudentContract.StudentEntry.COLUMN_NAME,"Ali");
values.put(StudentContract.StudentEntry.COLUMN_COURSE,"Android Programming");
long newRowId = db.insert(StudentContract.StudentEntry.TABLE_NAME,null,values);
if (newRowId == -1) {
// If the row ID is -1, then there was an error with insertion.
Toast.makeText(this, "Error with saving Student", Toast.LENGTH_SHORT).show();
} else {
// Otherwise, the insertion was successful and we can display a toast with the row ID.
Toast.makeText(this, "Student saved with row id: " + newRowId, Toast.LENGTH_SHORT).show();
}
}
}
في البداية قمنا بإنشاء object من نوع StudentHelper, بعد ذلك سننشئ object يستطيع القراءة والكتابة في database باستدعاء هذه الدالة getWritableDatabase, بعد ذلك نستخدم ContentValues لننشئ row جديد, ContentValues بسيط جدا فهو يستخدم key/value, الـ Key دائما ما يكون أسم العمود, و value هو القيمة المراد تخزينها.
ثم قمنا باستدعاء دالة insert التي تطلب منا اسم الجدول, و nullColumnHack سنجعل قيمته null, أخيرا ContentValues الذي قمنا بتجهيزه سلفا, ستعيد لنا دالة insert إما رقم row, أو -1 عند الخطا.
وبهذا نكون أتممنا أول عملية في CRUD
الان سنقوم بعملية READ ويقابلها SELECT في SQL, إذا سنقوم باسترجاع البيانات الموجودة داخل Database, سأزيل كود عملية Insert ليكون الكود أكثر وضوحا
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create database helper
StudentHelper mDbHelper = new StudentHelper(this);
// Gets the database in write mode
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// columns you want
String[] projection = {
StudentContract.StudentEntry._ID,
StudentContract.StudentEntry.COLUMN_NAME,
StudentContract.StudentEntry.COLUMN_COURSE
};
// SELECT FROM student WHERE "name" = 'Ahmed'
String selection = StudentContract.StudentEntry.COLUMN_NAME + " = ?";
String[] selectionArgs = { "Ali" };
// How you want the results sorted in the resulting Cursor
String sortOrder =
StudentContract.StudentEntry._ID + " DESC";
Cursor cursor = db.query(
StudentContract.StudentEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
if (cursor.moveToFirst()) {
do {
Toast.makeText(this,
cursor.getString(cursor.getColumnIndex(StudentContract.StudentEntry._ID)),
Toast.LENGTH_SHORT)
.show();
} while (cursor.moveToNext());
}
}
}
لدينا هنا عدة أمور لنوضحها
- قمنا بإنشاء object عن طريق استدعاء getWritableDatabase لنتمكن من قراءة البيانات.
-
projection ببساطة هي Array of String, نقوم من خلالها بإختيار الحقول التي نريد استرجاعها, في مثالنا قمنا بإرجاع الإسم والمادة بالإضافة إلىid, لكن لو أردنا فقط id فسنقوم بالتالي
وبهذا سيرجع لنا فقط id.String[] projection = { StudentContract.StudentEntry._ID };
-
selection هي string نقوم فيه بتحديد الشرط الذي نريد أسترجاع الـrow بناء عليه, ففي المثال نقول إذا كان name يساوي Ali رجع row
// SELECT FROM student WHERE "name" = 'Ahmed' String selection = StudentContract.StudentEntry.COLUMN_NAME + " = ?"; String[] selectionArgs = { "Ali" };
- selectionArgs هي Array of String نضع فيه القيم التي نريد التحقق منها كما في الكود السابق فهي مكملة لـ selection, selection تقوم بوضع الشرط, و selectionArgs تحتوي القيم.
-
sortOrder هي جملة string الغاية منها ترتيب البيانات الراجعة من الجدول تنازلي -DESC-, أو تصاعدي -ASC-
-
cursor هو عبارة عن مؤشر, يؤشر على البيانات الراجعة لنا, لتقريب الصورة, تخيل أن البيانات عبارة عن ملف نصي, والـ cursor عبارة عن ذاك المؤشر الذي يؤشر في بداية الملف.
-
قمنا باستدعاء دالة query وأعطيناها جميع المعلومات التي تحتاج لتعطينا البيانات التي نحتاج, اسم الجدول, والشرط, والقيم الخاصة بالشرط, والترتيب الخاص بالبيانات.
-
أخيرا قمنا بالتحقق إذا ما كان cursor يقف على بيانات أم لا, cursor.moveToFirst سترجع لنا قيمة false في حال لم يكن هناك أي بيانات, في حال كانت القيمة true, نقوم بأخذ row الأول وننتقل لثاني عن طريق cursor.moveToNext والتي في حال كنا في أخر row ستعيد لنا قيمة false ونخرج من loop
لقراءة البيانات من cursor استدعينا دالة getString والتي تحتاج int, فحصلنا عليها عن طريق دالة getColumnIndex والتي تعطيها أسم الحقل وترجع لك رقمه, لتوضيح أكثر لدينا ثلاث حقول _id, و name و course فيكون رقم index 0 للأول, 1 لثاني, 2 لثالث. وهكذا.
ننتقل الان للأمر الثالث من CRUD, وهو update, ويقابله UPDATE في SQL, الأمر في غاية البساطة, سنقوم بإضافة البيانات التي نرغب في تعديلها في ContentValues ثم نبحث عن row ونقوم بالتعديل عليه.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create database helper
StudentHelper mDbHelper = new StudentHelper(this);
// Gets the database in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// New value
ContentValues values = new ContentValues();
values.put(StudentContract.StudentEntry.COLUMN_NAME, "Ahmed");
// Which row to update, based on the name
String selection = StudentContract.StudentEntry.COLUMN_NAME + " LIKE ?";
String[] selectionArgs = { "Ali" };
int count = db.update(
StudentContract.StudentEntry.TABLE_NAME,
values,
selection,
selectionArgs);
Toast.makeText(this, "Number of student updated is : " + count, Toast.LENGTH_SHORT).show();
}
}
لا يوجد العديد من الأشياء الجديدة فقط نلاحظ استخدام دالة update, والتي ترجع لنا قيمة rows التي تم تعديلها.
أخيرا أمر DELETE, وقد يكون الأبسط, خصوصا بعد أن تعرفنا على بقية العمليات, ستحتاج إلى selection تعرف فيه query, ثم selectionArgs تعطيه القيم التي تحذف الـrow بناء عليها, وأخيرا تنفذ العملية بدالة delete التي تطلب من أسم الجدول و selection, و selectionArgs
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create database helper
StudentHelper mDbHelper = new StudentHelper(this);
// Gets the database in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// Define 'where' part of query.
String selection = StudentContract.StudentEntry.COLUMN_NAME + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { "Ali" };
// Issue SQL statement.
db.delete(StudentContract.StudentEntry.TABLE_NAME, selection, selectionArgs);
}
}
نقطة أخيرة يجب التنبيه عليها, دالتي getReadableDatabase و getWritableDatabase مكلفة جدا في الاستدعاء لذا مادمت تعتقد أنك ستحتاج إلى التعامل مع الـ Database , لا تستدعي دالة close, وجميل جدا أن تضع دالة close داخل onDestroy.
@Override
protected void onDestroy() {
mDbHelper.close();
super.onDestroy();
}
إلى هنا أكون قد وصلت إلى نهاية هذه المقالة, أتمنى أن أكون وفقت في تبسيط المعلومة, في حال كان هناك أي ملاحظات أو تساؤل يمكنك التواصل معي على حسابي في تويتر
والسلام عليكم ورحمة الله وبركاته.
المصادر
التعليقات (0)
لايوجد لديك حساب في عالم البرمجة؟
تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !