Khi dữ liệu cần lưu trữ lớn và thường là có quy tắc chung, chẳng hạn như thông tin user… thì chúng ta nên lưu vào các hệ thống cơ sở dữ liệu. Trong bài này chúng ta sẽ tìm hiểu về cơ sở dữ liệu SQLite có trong Android.
Định nghĩa mô hình dữ liệu
Một trong những thành phần chủ chốt của một cơ sở dữ liệu SQL là các bảng, nói một cách lý thuyết thì các bảng này định nghĩa cách mà dữ liệu được tổ chức trong cơ sở dữ liệu. Chúng phản ánh chính xác các câu lệnh SQL mà bạn dùng để tạo các bảng cơ sở dữ liệu.
Bên cạnh các bảng trong cơ sở dữ liệu, chúng ta sẽ tạo các lớp mô hình và các lớp điều khiển, các lớp mô hình sẽ mô phỏng lại cột trong các bảng còn các lớp điều khiển cung cấp các phương thức để thực hiện các thao tác trên các bảng đó. Nếu bạn đã từng học mô hình MVC (Model-View-Controller) thì các lớp này là phần Model và Controller trong mô hình này.
Một trong những cách thiết kế các lớp mô hình là định nghĩa một lớp đại diện cho toàn bộ cơ sở dữ liệu, sau đó định nghĩa các lớp nội (inner class) bên trong lớp chính đó để đại diên cho từng bảng.
Android có lớp interface android.provider.BaseColumns
hỗ trợ định nghĩa bảng, điểm đặc biệt của lớp này là có sẵn một thuộc tính có tên là _ID
cho bạn, tất nhiên bạn cũng không nhất thiết phải implement lớp interface này. Ví dụ:
package com.phocode; import android.provider.BaseColumns; public final class BlogDatabase { public BlogDatabase() {} public static abstract class EntryTable implements BaseColumns { public static final String TABLE_NAME = "Entry"; public static final String COLUMN_TITLE = "title"; public static final String COLUMN_DATE = "date"; public static final String COLUMN_CONTENT = "content"; } }
Thao tác với cơ sở dữ liệu
Sau khi đã định nghĩa các lớp mô hình, chúng ta sẽ định nghĩa các lớp điều khiển có phương thức thực hiện các thao tác truy vấn với cơ sở dữ liệu như INSERT, UPDATE, DELETE…
Chúng ta có thể định nghĩa một vài câu truy vấn mẫu bên trong các lớp mô hình, ví dụ:
package com.phocode; import android.provider.BaseColumns; public final class BlogDatabase { public BlogDatabase() {} public static abstract class EntryTable implements BaseColumns { public static final String TABLE_NAME = "Entry"; public static final String COLUMN_TITLE = "title"; public static final String COLUMN_DATE = "date"; public static final String COLUMN_CONTENT = "content"; private static final String COMMA = ","; private static final String TYPE_INT = "INTEGER"; private static final String TYPE_DATETIME = "DATETIME"; private static final String TYPE_TEXT = "TEXT"; private static final String QUERY_CREATE_RECORD = "" + "CREATE TABLE " + TABLE_NAME + "(" + _ID + " INTEGER PRIMARY KEY" + COMMA + COLUMN_TITLE + TYPE_TEXT + COMMA + COLUMN_DATE + TYPE_DATETIME + COMMA + COLUMN_CONTENT + TYPE_TEXT + ")"; private static final String QUERY_DELETE_RECORD = "" + "DROP TABLE IF EXISTS " + TABLE_NAME; } }
Những câu truy vấn ví dụ ở trên có thể khác tùy thuộc vào cơ sở dữ liệu mà bạn dùng. Mặc định dữ liệu trong cơ sở dữ liệu sẽ được lưu trong bộ nhớ Internal, tức là chỉ có ứng dụng của chúng ta mới có thể truy cập vào các dữ liệu này.
Android cung cấp lớp android.database.sqlite.SQLiteOpenHelper
có các phương thức thực hiện việc mở kết nối đến cơ sở dữ liệu, chạy các câu truy vấn… Thường thì chúng ta sẽ định nghĩa một lớp kế thừa từ lớp này và override 3 phương thức bắt buộc là phương thức khởi tạo và 2 phương thức onCreate()
và onUpgrade().
Ví dụ chúng ta định nghĩa lại lớp BlogDatabase
như sau:
package com.phocode; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; public final class BlogDatabase { public BlogDatabase() {} public static class EntryTableHelper extends SQLiteOpenHelper { public static final String TABLE_NAME = "Entry"; public static final String COLUMN_ID = "id"; public static final String COLUMN_TITLE = "title"; public static final String COLUMN_DATE = "date"; public static final String COLUMN_CONTENT = "content"; private static final String COMMA = ","; private static final String TYPE_INT = "INTEGER"; private static final String TYPE_DATETIME = "DATETIME"; private static final String TYPE_TEXT = "TEXT"; private static final String QUERY_CREATE_RECORD = "" + "CREATE TABLE " + TABLE_NAME + "(" + COLUMN_ID + " INTEGER PRIMARY KEY" + COMMA + COLUMN_TITLE + TYPE_TEXT + COMMA + COLUMN_DATE + TYPE_DATETIME + COMMA + COLUMN_CONTENT + TYPE_TEXT + ")"; private static final String QUERY_DELETE_TABLE = "" + "DROP TABLE IF EXISTS " + TABLE_NAME; public static final int DATABASE_VERSION = 1; public static final String DATABASE_NAME = "BlogDatabase.db"; public EntryTableHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(QUERY_CREATE_RECORD); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(QUERY_DELETE_TABLE); onCreate(db); } } }
Trong đoạn code trên chúng ta gộp phần định nghĩa các cột trong bảng và các phương thức thao tác với trong một lớp luôn và lớp này kế thừa từ lớp SQLiteOpenHelper.
Khi cần thực hiện thao tác với bảng chúng ta chỉ cần tạo một đối tượng từ lớp này và truyền vào một đối tượng Context,
chúng ta có thể lấy từ phương thức Activity.getApplicationContext().
public EntryTableHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); }
Bên trong hàm khởi tạo chúng ta gọi đến phương thức khởi tạo của lớp cha, phương thức này nhận vào một đối tượng Context,
tên file cơ sở dữ liệu, ở đây là file BlogDatabase.db,
nếu file này chưa tồn tại thì lớp này sẽ tạo một file mới, tiếp theo là một đối tượng SQLiteDatabase.CursorFactory,
chúng ta có thể truyền vào null
vì ở đây cũng chưa cần đến, và cuối cùng số phiên bản cơ sở dữ liệu, số phiên bản ở đây chẳng qua là các con số chúng ta dùng để phân biệt các cơ sở dữ liệu mà chúng ta đã định nghĩa chứ không phải số phiên bản CSDL SQLite hay cái gì đó tương tự.
@Override public void onCreate(SQLiteDatabase db) { db.execSQL(QUERY_CREATE_RECORD); }
Phương thức onCreate()
tự động được gọi khi cần, phương thức này nhận vào một đối tượng SQLiteDatabase
và thực hiện tạo bảng. Để thực hiện bất kì câu truy vấn nào thì chúng ta chỉ cần gọi đến phương thức execSQL()
và đưa vào một chuỗi truy vấn là được.
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(QUERY_DELETE_TABLE); onCreate(db); }
Phương thức onUpgrade()
được gọi khi muốn cập nhật lại định nghĩa về bảng, chẳng hạn như thêm bảng mới, thêm/xóa cột…
Tạo mới bản ghi
Để tạo mới các bản ghi vào cơ sở dữ liệu thì chúng ta tạo đối tượng android.content.ContentValues
rồi truyền vào phương thức SQLiteDatabase.insert().
Ví dụ:
package com.phocode; import android.content.ContentValues; import android.app.Activity; import android.os.Bundle; import java.text.SimpleDateFormat; import java.util.Date; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(this.getApplicationContext()); ContentValues values = new ContentValues(); values.put(helper.COLUMN_ID, 1); values.put(helper.COLUMN_TITLE, "About PhoCode"); values.put(helper.COLUMN_DATE, new SimpleDateFormat("dd/mm/yyyy hh:mm:ss").format(new Date())); values.put(helper.COLUMN_CONTENT, "PhoCode - Open source is the future"); helper.getWritableDatabase().insert(helper.TABLE_NAME, null, values); } }
Phương thức getWritableDatabase()
sẽ trả về một đối tượng SQLiteDatabase,
từ đó chúng ta có thể gọi phương thức insert()
để tạo mới một bản ghi trong bảng. Phương thức này nhận tên bảng, danh sách các cột cho phép nhận giá trị NULL, chúng ta có thể truyền vào “null” và phương thức này sẽ không tạo mới bản ghi vào mà có cột rỗng, tham số thứ 3 của phương thức insert()
là đối tượng ContentValues.
Đọc bản ghi từ cơ sở dữ liệu
Để lấy các bản ghi thì chúng ta sử dụng phương thức rawQuery()
của đối tượng SQLiteDatabase
từ phương thức SQLiteOpenHelper.getReadableDatabase(),
phương thức rawQuery()
sẽ trả về một đối tượng android.database.Cursor,
ví dụ:
BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(getApplicationContext()); SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT * FROM " + helper.TABLE_NAME, null);
Phương thức rawQuery()
nhận vào 2 tham số là một câu lệnh SQL và một chuỗi điều kiện, chuỗi điều kiện này chính là mệnh đề WHERE
trong câu truy vấn, ở đoạn code trên chúng ta không có điều kiện nào cả nên để null.
Các bản ghi được trả về sẽ được lưu trong một đối tượng Cursor.
Đối tượng Cursor
lưu các bản ghi theo dạng danh sách và trong đối tượng này có một biến con trỏ, con trỏ này sẽ trỏ tới từng bản ghi trong danh sách đó. Chúng ta có thể lấy dữ liệu của từng cột trong từng bản ghi theo chỉ số hoặc theo tên cột, ví dụ:
if(cursor != null) { if(cursor.moveToFirst()) do { int id = cursor.getInt(0); String title = cursor.getString(cursor.getColumnIndex(helper.COLUMN_TITLE)); String date = cursor.getString(cursor.getColumnIndex(helper.COLUMN_DATE)); String content = cursor.getString(cursor.getColumnIndex(helper.COLUMN_CONTENT)); } while(cursor.moveToNext()); }
Để phòng trường hợp phương thức query()
không trả về bản ghi nào, chúng ta nên kiểm tra xem đối tượng Cursor
có bằng null
không trước.
if(cursor.moveToFirst())
Phương thức moveToFirst()
sẽ chuyển con trỏ về dòng đầu tiên trong danh sách.
while(cursor.moveToNext());
Phương thức moveToNext()
sẽ chuyển con trỏ đến dòng tiếp theo trong danh sách.
int id = cursor.getInt(0); String title = cursor.getString(cursor.getColumnIndex(helper.COLUMN_TITLE));
Tại mỗi dòng, chúng ta có thể lấy giá trị của từng cột trong đó bằng các phương thức như getInt(), getFloat(), getString()...
các phương thức này nhận vào chỉ số cột trong bảng, các cột này được đánh thứ tự từ 0. Nếu không biết số thứ tự của cột nào thì có thể dùng phương thức getColumnIndex(),
phương thức này nhận vào tên cột và trả về số thứ tự của cột đó.
Xóa bản ghi
Để xóa bản ghi thì chúng ta dùng phương thức delete()
của đối tượng SQLiteDatabase
lấy từ phương thức SQLiteOpenHelper.getWritableDatabase().
Ví dụ:
BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(getApplicationContext()); SQLiteDatabase db = helper.getWritableDatabase(); String whereClause = "id = ? AND date <= ?"; String[] arguments = { "1", "01-01-2016 12:00:00" }; db.delete(helper.TABLE_NAME, whereClause, arguments);
Phương thức delete()
nhận vào 3 tham số, tham số đầu tiên là tên bảng, tham số thứ 2 là chuỗi mệnh đề WHERE
có các tham số, tham số thứ 3 là mảng các tham số truyền vào tham số thứ 2.
Trong đoạn code trên, tham số thứ 2 là "id = ? AND date <= ?"
, còn tham số thứ 3 là { "1", "01-01-2016" },
từ 2 tham số này Android sẽ xây dựng nên câu truy vấn như sau:
DELETE From Entry WHERE id = 1 AND date <= '01-01-2016 12:00:00'
Tức là các dấu chấm hỏi "?"
trong tham số thứ 2 sẽ lần lượt được thay thế bằng các giá trị trong tham số thứ 3.
Lý do tại sao Android lại chia mệnh đề WHERE
làm 2 tham số là để phòng chống kỹ thuật tấn công SQL Injection, nếu muốn bạn có thể tìm hiểu thêm trên Google.
Cập nhật bản ghi
Để cập nhật bản ghi thì chúng ta dùng phương thức update()
của lớp SQLiteDatabase
lấy từ phương thức SQLiteOpenHelper.getReadableDatabase().
Ví dụ:
BlogDatabase.EntryTableHelper helper = new BlogDatabase.EntryTableHelper(getApplicationContext()); SQLiteDatabase db = helper.getReadableDatabase(); ContentValues values = new ContentValues(); values.put(helper.COLUMN_TITLE, "PhoCode"); values.put(helper.COLUMN_DATE, new SimpleDateFormat("dd/mm/yyyy hh:mm:ss").format(new Date())); values.put(helper.COLUMN_CONTENT, "Android Programming"); String whereClause = "id = ?"; String[] arguments = { "1" }; db.update(helper.TABLE_NAME, values, whereClause, arguments);
Phương thức update()
nhận vào tên bảng, đối tượng ContentValues
chứa các cột được cập nhật và giá trị mới của chúng, mệnh đề WHERE
và tham số cho mệnh đề WHERE
như với phương thức delete()
ở trên.