版權聲明:本文為博主原創文章,未經博主允許不得轉載
Github:github.com/AnliaLee
首發地址:Anlia_掘金
大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論
前言
博主這兩天心血來潮準備回顧下SQLite的知識,然而網上查找資料的過程是痛苦的,因為很少有一篇博客能把SQLite的入門知識講全的,得好幾篇合著來看才行,因此我的瀏覽器選項卡基本上是這樣的
正所謂自己動手豐衣足食,我不想以后忘了還要這樣再來一次,所以決定自己重新總結一番,集眾家之長來篇大雜燴。本篇博客內容很多,為了盡量涵蓋到所有知識不得已寫得很啰嗦,大家可以看著目錄按需跳著看,如果有什么遺漏或寫錯的地方歡迎大家指出來~
什么是SQLite
關于SQLite的簡介網上有很多很多,其中看到一篇博客總結得很好,遂直接引用啦~以下摘自Android黃金篇-SQLite數據庫
SQLite介紹
SQLite是一款輕量級的關系型數據庫,它的運算速度非常快,占用資源很少,通常只需要幾百K的內存就足夠了,因而特別適合在移動設備上使用。
SQLite特點
-
輕量級
使用 SQLite 只需要帶一個動態庫,就可以享受它的全部功能,而且那個動態庫的尺寸想當小。 -
獨立性
SQLite 數據庫的核心引擎不需要依賴第三方軟件,也不需要所謂的“安裝”。 -
隔離性
SQLite 數據庫中所有的信息(比如表、視圖、觸發器等)都包含在一個文件夾內,方便管理和維護。 -
跨平臺
SQLite 目前支持大部分操作系統,不至電腦操作系統更在眾多的手機系統也是能夠運行,比如:Android和IOS。 -
多語言接口
SQLite 數據庫支持多語言編程接口。 -
安全性
SQLite 數據庫通過數據庫級上的獨占性和共享鎖來實現獨立事務處理。這意味著多個進程可以在同一時間從同一數據庫讀取數據,但只能有一個可以寫入數據。 -
弱類型的字段
同一列中的數據可以是不同類型
SQLite數據類型
存儲類 | 存儲類 |
---|---|
NULL | 值是一個 NULL 值 |
INTEGER | 值是一個帶符號的整數,根據值的大小存儲在 1、2、3、4、6 或 8 字節中 |
REAL | 值是一個浮點值,存儲為 8 字節的 IEEE 浮點數字 |
TEXT | 值是一個文本字符串,使用數據庫編碼(UTF-8、UTF-16BE 或 UTF-16LE)存儲 |
BLOB | 值是一個 blob 數據,完全根據它的輸入存儲 |
SQLite具有以下五種常用的數據類型:
存儲類 | 存儲類 |
---|---|
NULL | 值是一個 NULL 值 |
INTEGER | 值是一個帶符號的整數,根據值的大小存儲在 1、2、3、4、6 或 8 字節中 |
REAL | 值是一個浮點值,存儲為 8 字節的 IEEE 浮點數字 |
TEXT | 值是一個文本字符串,使用數據庫編碼(UTF-8、UTF-16BE 或 UTF-16LE)存儲 |
BLOB | 值是一個 blob 數據,完全根據它的輸入存儲 |
調試工具
調試SQLite數據庫推薦使用SQLite Expert(Personal),簡單易用
當然你也可以直接用adb shell查看數據庫,這個就看個人喜愛了
SQLiteDatabase與SQLiteOpenHelper
在講如何使用SQLite數據庫之前,有必要介紹一下這兩個重要的類:SQLiteDatabase 和 SQLiteOpenHelper,這是SQLite數據庫API中最基礎的兩個類
SQLiteDatabase
在Android中,SQLite數據庫的使用始于SQLiteDatabase這個類(SQLiteOpenHelper也是基于SQLiteDatabase來進行數據庫創建和版本管理,之后會詳細介紹),我們可以看下它的內部方法(未截全)
可以發現insert、query等熟悉的字眼,這些方法都是已經封裝好的,我們只需要傳入適當的參數即可完成諸如插入、更新、查詢等操作。當然SQLiteDatabase也提供了直接執行SQL語句的方法,如
-
execSQL
可以執行insert、delete、update和CREATE TABLE之類有更改行為的SQL語句 -
rawQuery
用于執行select語句
總結來說,我們可以將SQLiteDatabase看作是一個數據庫對象,通過調用其中的方法來創建、刪除、執行SQL命令,以及執行其他常見的數據庫管理任務。我們會在之后的章節中詳細講述如何使用SQLiteDatabase一步步搭建我們的本地數據庫
SQLiteOpenHelper
SQLiteOpenHelper是SQLiteDatabase的輔助類,通過對SQLiteDatabase內部方法的封裝簡化了數據庫創建與版本管理的操作。它是一個抽象類,一般情況下,我們需要繼承并重寫這兩個父類方法:
-
onCreate
在初次生成數據庫時才會被調用,我們一般重寫onCreate生成數據庫表結構并添加一些應用使用到的初始化數據 -
onUpgrade
當數據庫版本有更新時會調用這個方法,我們一般會在這執行數據庫更新的操作,例如字段更新、表的增加與刪除等
此外父類方法中還有onConfigure、onDowngrade、onOpen,一般項目中很少用到它們,如果大家需要進一步了解可以看下這篇博客,這里就不贅述了
那么SQLiteOpenHelper和SQLiteDatabase是如何關聯起來的呢?SQLiteOpenHelper中提供了getWritableDatabase和 getReadableDatabase方法,其最終都調用了getDatabaseLocked,并在第一次調用(或數據庫版本更新)時執行我們之前在onCreate、onUpgrade等方法中重寫的數據庫操作,這兩個方法的區別在于
-
getWritableDatabase
打開一個可 讀/寫 的數據庫,如果數據庫不存在,則會自動創建一個數據庫,最終返回SQLiteDatabase對象 -
getReadableDatabase
打開一個可 讀 的數據庫,其他同上
創建數據庫
一些實操之前要知道的東西
一般來說,創建SQLite數據庫的方法有三,我們按照使用頻率從高到低的順序列出這三種方法
- 繼承SQLiteOpenHelper,調用getWritableDatabase / getReadableDatabase打開或創建數據庫(推薦初學者使用)
- 調用SQLiteDatabase.openOrCreateDatabase打開或創建數據庫
- 調用Context.openOrCreateDatabase打開或創建數據庫
它們最終都是要調用SQLiteDatabase.openDatabase方法,下圖可以簡單地概括它們的調用關系
那么我們不妨看下openDatabase干了啥
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
DatabaseErrorHandler errorHandler) {
SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler);
db.open();//繼續執行打開或創建數據庫的操作
return db;
}
解釋一下各個參數
-
String path
數據庫文件路徑 -
CursorFactory factory
用于構造自定義的Cursor子類對象,在執行查詢操作時返回,若傳入 null 則使用默認的factory構造Cursor -
int flags
用于控制數據庫的訪問模式,可傳入的參數有- CREATE_IF_NECESSARY:當數據庫不存在時創建該數據庫文件
- ENABLE_WRITE_AHEAD_LOGGING:繞過數據庫的鎖機制,以多線程操作數據庫的方式進行讀寫
- NO_LOCALIZED_COLLATORS:打開數據庫時,不根據本地化語言對數據庫進行排序
- OPEN_READONLY:以只讀方式打開數據庫
- OPEN_READWRITE:以讀寫方式打開數據庫
-
DatabaseErrorHandler errorHandler
當檢測到數據庫損壞時進行回調的接口,一般沒有特殊需要傳入 null 即可
我們作為初學者剛入門不必過多地深究每個參數的含義及使用場景,知道有這么些內容,日后擴展技能起個索引作用就行,因此這里就不費篇幅繼續深挖了
關于創建數據庫的路徑
關于這點很有必要單獨開個小節啰嗦一下,很多資料都忽略了這個或者講得不是很清楚。數據庫的默認路徑為
/data/data/<package_name>/databases/
一般情況下我們在創建數據庫時path參數只需傳入“xxx.db”,系統自動會在該默認路徑下創建名為“xxx.db”的數據庫文件,這樣做最大的好處就是安全,要想拿到這個文件我們得先root手機(據說模擬器中可以直接拿,我沒親自試驗過),而且這個數據庫文件也會隨著App的刪除而刪除。但某些場景下,例如我們要取出數據庫文件進行調試,在默認路徑下創建數據庫就顯得不那么方便了。因此,我們可以在內部存儲或sd卡中創建該數據庫文件,例如我們要傳入的path可以寫成這樣
Environment.getExternalStorageDirectory().getPath() + "/SQLiteTest/xxx.db"
那么數據庫文件xxx.db就會放在內存的SQLiteTest目錄中,需要注意的是,如果SQLiteTest目錄不存在,系統會拋出異常
這是因為我們前文提到的SQLiteDatabase.openDatabase方法中db.open()后續并沒有創建目錄的相關代碼,所以我們需要手動去創建該目錄(記得配置權限)
File dir = new File(Environment.getExternalStorageDirectory().getPath() + "/SQLiteTest/");
if (!dir.exists()) {
dir.mkdir();
}
目錄創建之后我們就可以繼續執行創建數據庫的操作了
接下來我們以創建數據庫文件test.db,建一張test表為例,按順序講講這三種創建數據庫的方法
一、繼承SQLiteOpenHelper(推薦初學者使用)
按照之前講的,我們需要先繼承SQLiteOpenHelper,然后重寫onCreate與onUpgrade方法
如何調用
創建MySQLiteOpenHelper
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
public static final String FILE_DIR = Environment.getExternalStorageDirectory().getPath() + "/SQLiteTest/";
public static final int DATABASE_VERSION = 1;
public static final String TABLE_NAME = "test";
public MySQLiteOpenHelper(Context context, String name){
this(context, name, null, DATABASE_VERSION);
}
public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
super(context, name, factory, version, errorHandler);
}
@Override
public void onCreate(SQLiteDatabase db) {
File dir = new File(FILE_DIR);
if (!dir.exists()) {
dir.mkdir();
}
try{
db.execSQL("create table if not exists " + TABLE_NAME +
"(id text primary key,name text)");
}
catch(SQLException se){
se.printStackTrace();
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if(newVersion > oldVersion){
String sql = "DROP TABLE IF EXISTS " + TABLE_NAME;
db.execSQL(sql);
onCreate(db);
}
}
}
在Activity中調用getWritableDatabase / getReadableDatabase(我這里把動態申請權限的代碼也貼出來,之后就省略了)
public class MainActivity extends AppCompatActivity {
public static final String DATABASE_NAME = FILE_DIR + "test.db";
private static final int CODE_PERMISSION_REQUEST = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
//申請寫入權限
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, CODE_PERMISSION_REQUEST);
} else {
createDB();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch(requestCode) {
case CODE_PERMISSION_REQUEST:
if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
createDB();
} else{
}
break;
default:
break;
}
}
private void createDB(){
MySQLiteOpenHelper sqLiteOpenHelper = new MySQLiteOpenHelper(this,DATABASE_NAME);
SQLiteDatabase database = sqLiteOpenHelper.getWritableDatabase();
}
}
用SQLite Expert看下結果
二、調用SQLiteDatabase.openOrCreateDatabase打開或創建數據庫
先來看下openOrCreateDatabase方法的源碼
public static SQLiteDatabase openOrCreateDatabase(File file, CursorFactory factory) {
return openOrCreateDatabase(file.getPath(), factory);
}
public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory) {
return openDatabase(path, factory, CREATE_IF_NECESSARY, null);
}
public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory,
DatabaseErrorHandler errorHandler) {
return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler);
}
可以看到openOrCreateDatabase實際上是以CREATE_IF_NECESSARY模式打開創建數據庫的
如何調用
在Activity中執行相應代碼
private static final String FILE_DIR = Environment.getExternalStorageDirectory().getPath() + "/SQLiteTest/";
public static final String DATABASE_NAME = FILE_DIR + "test.db";
private void createDB(){
File dir = new File(FILE_DIR);
if (!dir.exists()) {
dir.mkdir();
}
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(DATABASE_NAME, null);
database.execSQL("create table if not exists " + "test" +
"(id text primary key,name text)");
}
結果一樣的我就不重復貼出來了
三、調用Context.openOrCreateDatabase打開或創建數據庫
Context.openOrCreateDatabase的具體實現在ContextImpl類中(關于Context的知識大家可以自行查閱資料了解)
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory,
DatabaseErrorHandler errorHandler) {
checkMode(mode);
File f = getDatabasePath(name);
int flags = SQLiteDatabase.CREATE_IF_NECESSARY;
if ((mode & MODE_ENABLE_WRITE_AHEAD_LOGGING) != 0) {
flags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING;
}
if ((mode & MODE_NO_LOCALIZED_COLLATORS) != 0) {
flags |= SQLiteDatabase.NO_LOCALIZED_COLLATORS;
}
SQLiteDatabase db = SQLiteDatabase.openDatabase(f.getPath(), factory, flags, errorHandler);
setFilePermissionsFromMode(f.getPath(), mode, 0);
return db;
}
可以看出Context.openOrCreateDatabase與SQLiteDatabase.openOrCreateDatabase本質上沒有太大區別,只是多了一個mode參數用于設置操作模式,可傳入的參數有
-
MODE_PRIVATE
默認的模式,創建的數據庫文件只能通過調用該模式的應用程序訪問(或所有應用程序共享相同的用戶ID),我們一般設置這個參數即可 -
MODE_ENABLE_WRITE_AHEAD_LOGGING
功能和之前講的設置ENABLE_WRITE_AHEAD_LOGGING一樣 -
MODE_NO_LOCALIZED_COLLATORS
功能和之前講的設置NO_LOCALIZED_COLLATORS一樣 -
MODE_WORLD_READABLE
設置后當前文件可以被其他應用程序讀取,官方不建議在API 17以上的版本設置該參數(Android 7.0以上會直接拋出異常:XXX no longer supported) -
MODE_WORLD_WRITEABLE
設置后當前文件可以被其他應用程序寫入,官方不建議在API 17以上的版本設置該參數(Android 7.0以上會直接拋出異常:XXX no longer supported)
如何調用
private static final String FILE_DIR = Environment.getExternalStorageDirectory().getPath() + "/SQLiteTest/";
public static final String DATABASE_NAME = FILE_DIR + "test.db";
private void createDB(){
File dir = new File(FILE_DIR);
if (!dir.exists()) {
dir.mkdir();
}
SQLiteDatabase database = this.openOrCreateDatabase(DATABASE_NAME,MODE_PRIVATE,null);
database.execSQL("create table if not exists " + "test" +
"(id text primary key,name text)");
}
數據庫的相關操作
這里只介紹簡單的增刪改查操作,其余的大家可以自行查閱資料了解
增
SQLiteDatabase提供了insert方法
public long insert(String table, String nullColumnHack, ContentValues values) {
try {
return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
} catch (SQLException e) {
Log.e(TAG, "Error inserting " + values, e);
return -1;
}
}
各參數含義如下
-
String table
要插入數據的表的名稱 -
String nullColumnHack
當values參數為空或者里面沒有內容的時候,執行insert是會失敗(底層數據庫不允許插入一個空行),為了防止這種情況,我們要在這里指定一個列名,如果發現將要插入的行為空行時,就會將指定的這個列名的值設為null,然后再向數據庫中插入。若values不為null并且元素的個數大于0,則一般將nullColumnHack設為null -
ContentValues values
ContentValues類似一個map.通過鍵值對的形式存儲值
如何調用
像前文所說的,我們可以通過SQLiteDatabase.insert和SQLiteDatabase.execSQL兩種方式添加數據
ContentValues values = new ContentValues();
values.put("id","1");
values.put("name","name1");
database.insert("test",null,values);
database.execSQL("insert into test(id, name) values(2, 'name2')");
database.close();
結果如圖
刪
同樣的SQLiteDatabase提供delete方法刪除數據
public int delete(String table, String whereClause, String[] whereArgs) {
acquireReference();
try {
SQLiteStatement statement = new SQLiteStatement(this, "DELETE FROM " + table +
(!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
try {
return statement.executeUpdateDelete();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}
-
String table
表名稱 -
String whereClause
條件語句,相當于where關鍵字,可以使用占位符分隔多個條件 -
String[] whereArgs
對應條件語句的值的數組,注意若有多個值則需與selection中的多個條件(?符號)一一對應
如何調用
我們先添加幾條數據用來測試
database.execSQL("insert into test(id, name) values(3, 'name3')");
database.execSQL("insert into test(id, name) values(4, 'name4')");
database.execSQL("insert into test(id, name) values(5, 'name5')");
執行刪除語句
String whereClause = "id=?";
String[] whereArgs = {"3"};
database.delete("test",whereClause,whereArgs);
database.execSQL("delete from test where name = 'name4'");
database.close();
改
SQLiteDatabase.update用于更新數據
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE);
}
updateWithOnConflict大家可以自行查閱資料了解,這里只簡單解釋一下各個參數的含義
-
String table
表名稱 -
ContentValues values
ContentValues類似一個map.通過鍵值對的形式存儲值 -
String whereClause
條件語句,相當于where關鍵字,可以使用占位符分隔多個條件 -
String[] whereArgs
對應條件語句的值的數組,注意若有多個值則需與selection中的多個條件(?符號)一一對應
如何調用
ContentValues values = new ContentValues();
values.put("name","update2");
String whereClause = "id=?";
String[] whereArgs={"2"};
database.update("test",values,whereClause,whereArgs);
database.execSQL("update test set name = 'update5' where id = 5");
database.close();
查
SQLiteDatabase提供query和rawQuery方法執行查詢的操作,查詢結束后返回一個Cursor對象。Cursor是一個游標接口,提供了遍歷查詢結果的方法,由于本篇博客重點不是這個,就不展開講了。我們接著看query方法
public Cursor query(String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having,
String orderBy) {
return query(false, table, columns, selection, selectionArgs, groupBy,
having, orderBy, null /* limit */);
}
public Cursor query(String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having,
String orderBy, String limit) {
return query(false, table, columns, selection, selectionArgs, groupBy,
having, orderBy, limit);
}
public Cursor query(boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit) {
return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
groupBy, having, orderBy, limit, null);
}
public Cursor query(boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit, CancellationSignal cancellationSignal) {
return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
groupBy, having, orderBy, limit, cancellationSignal);
}
query方法的參數很多,大家簡單了解下就行
-
boolean distinct
是否去除重復記錄 -
String table
表名稱 -
String[] columns
要查詢的列名稱數組,例如這句查詢語句的加黑部分:SELECT id, name FROM test -
String selection
條件語句,相當于where關鍵字,可以使用占位符分隔多個條件 -
String[] selectionArgs
對應條件語句的值的數組,注意若有多個值則需與selection中的多個條件(?符號)一一對應 -
String groupBy
分組語句,對查詢的結果進行分組 -
String having
分組條件,對分組的結果進行限制 -
String orderBy
排序語句 -
String limit
分頁查詢限制 -
CancellationSignal cancellationSignal
取消操作的信號,一般用于設置查詢取消時的后續操作,如果沒有則設置為null。如果操作取消了,query語句運行時會拋出異常(OperationCanceledException)
如何調用
前文提到執行查詢SQL語句的方法是rawQuery,同樣在這里我們對照著query方法看看調用過程(如果數據量大的話查詢操作是需要放在單獨的線程中去執行的)
調用query方法,各個參數我就不一一試了,簡單舉幾個例子
if(database!=null){
Cursor cursor = database.query ("test",null,null,null,null,null,null);
while (cursor.moveToNext()){
String id = cursor.getString(0);
String name=cursor.getString(1);
Log.e("SQLiteTest query","id:"+id+" name:"+name);
}
database.close();
}
或者
if(database!=null){
Cursor cursor = database.rawQuery("SELECT * FROM test", null);
while (cursor.moveToNext()){
String id = cursor.getString(0);
String name=cursor.getString(1);
Log.e("SQLiteTest query","id:"+id+" name:"+name);
}
database.close();
}
加上多個條件限制
if(database!=null){
String selection = "id=? or name=?";
String[] selectionArgs = {"1","update2"};
Cursor cursor = database.query ("test",null,selection,selectionArgs,null,null,null);
while (cursor.moveToNext()){
String id = cursor.getString(0);
String name=cursor.getString(1);
Log.e("SQLiteTest query","id:"+id+" name:"+name);
}
database.close();
}
或者
if(database!=null){
Cursor cursor = database.rawQuery("SELECT * FROM test WHERE id=? or name=?", new String[]{"1","update2"});
while (cursor.moveToNext()){
String id = cursor.getString(0);
String name=cursor.getString(1);
Log.e("SQLiteTest query","id:"+id+" name:"+name);
}
database.close();
}
SQLite的替代者們
隨著技術的更迭,原生的SQLite早已不是唯一的選擇,我們利用其打好基礎后,也不妨嘗試下各種熱門的數據庫框架,例如OrmLite、greenDAO、ObjectBox、Realm等等。各個框架都有自己的優點和不足之處,大家可以按需選擇適合自己的框架使用。由于博主并沒有每個框架都進行過分析和嘗試,在這強行科普那就是耍流氓了,所以直接拉幾篇寫得很好的對比博客來做外援吧~
相關博客鏈接
數據庫到底哪家強?
【Android 數據庫框架總結,總有一個適合你!】
Android數據庫框架GreenDao&Realm實戰分析