美文网首页面经Android开发Android开发经验谈
Android 一篇很啰嗦的SQLite知识总结

Android 一篇很啰嗦的SQLite知识总结

作者: Anlia | 来源:发表于2018-01-23 12:12 被阅读285次

    版权声明:本文为博主原创文章,未经博主允许不得转载
    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实战分析

    相关文章

      网友评论

        本文标题:Android 一篇很啰嗦的SQLite知识总结

        本文链接:https://www.haomeiwen.com/subject/pncsaxtx.html