Android 一篇很啰嗦的SQLite知識總結

版權聲明:本文為博主原創文章,未經博主允許不得轉載
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數據庫之前,有必要介紹一下這兩個重要的類:SQLiteDatabaseSQLiteOpenHelper,這是SQLite數據庫API中最基礎的兩個類

SQLiteDatabase

在Android中,SQLite數據庫的使用始于SQLiteDatabase這個類(SQLiteOpenHelper也是基于SQLiteDatabase來進行數據庫創建和版本管理,之后會詳細介紹),我們可以看下它的內部方法(未截全)

可以發現insertquery等熟悉的字眼,這些方法都是已經封裝好的,我們只需要傳入適當的參數即可完成諸如插入、更新、查詢等操作。當然SQLiteDatabase也提供了直接執行SQL語句的方法,如

  • execSQL

    可以執行insert、delete、update和CREATE TABLE之類有更改行為的SQL語句
  • rawQuery

    用于執行select語句

總結來說,我們可以將SQLiteDatabase看作是一個數據庫對象,通過調用其中的方法來創建、刪除、執行SQL命令,以及執行其他常見的數據庫管理任務。我們會在之后的章節中詳細講述如何使用SQLiteDatabase一步步搭建我們的本地數據庫

SQLiteOpenHelper

SQLiteOpenHelperSQLiteDatabase的輔助類,通過對SQLiteDatabase內部方法的封裝簡化了數據庫創建與版本管理的操作。它是一個抽象類,一般情況下,我們需要繼承并重寫這兩個父類方法:

  • onCreate

    在初次生成數據庫時才會被調用,我們一般重寫onCreate生成數據庫表結構并添加一些應用使用到的初始化數據
  • onUpgrade

    當數據庫版本有更新時會調用這個方法,我們一般會在這執行數據庫更新的操作,例如字段更新、表的增加與刪除等

此外父類方法中還有onConfigureonDowngradeonOpen,一般項目中很少用到它們,如果大家需要進一步了解可以看下這篇博客,這里就不贅述了

那么SQLiteOpenHelperSQLiteDatabase是如何關聯起來的呢?SQLiteOpenHelper中提供了getWritableDatabasegetReadableDatabase方法,其最終都調用了getDatabaseLocked,并在第一次調用(或數據庫版本更新)時執行我們之前在onCreateonUpgrade等方法中重寫的數據庫操作,這兩個方法的區別在于

  • 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,然后重寫onCreateonUpgrade方法

如何調用

創建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.openOrCreateDatabaseSQLiteDatabase.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.insertSQLiteDatabase.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提供queryrawQuery方法執行查詢的操作,查詢結束后返回一個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早已不是唯一的選擇,我們利用其打好基礎后,也不妨嘗試下各種熱門的數據庫框架,例如OrmLitegreenDAOObjectBoxRealm等等。各個框架都有自己的優點和不足之處,大家可以按需選擇適合自己的框架使用。由于博主并沒有每個框架都進行過分析和嘗試,在這強行科普那就是耍流氓了,所以直接拉幾篇寫得很好的對比博客來做外援吧~

相關博客鏈接
數據庫到底哪家強?
【Android 數據庫框架總結,總有一個適合你!】
Android數據庫框架GreenDao&Realm實戰分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。