SQLite学习笔记(七)

版权声明:本文为博主原创文章,转载请注明出处: https://blog.csdn.net/qq_38182125/article/details/89458521

概述

本篇文章的主题是讲述在 Android 中应用 SQLite 的实例,结构如下所示:

  • SQLiteOpenHelper 的应用
  • SQLiteDatabase 的 CRUD 操作
  • 开启事务
  • SQLite 的其他注意事项

一、SQLiteOpenHelper

在 Android 中使用 SQLite 的时候需要借助一个类:SQLiteOpenHelper,这是一个抽象类,继承这个抽象类的时候,我们必须实现以下2个方法:

/**
 * 创建数据库的时候回调的方法
 * @param db 数据库对象
 */
public abstract void onCreate(SQLiteDatabase db);

/**
 * 数据库版本更新时回调的方法
 * @param db 数据库对象
 * @param oldVersion 数据库的旧版本
 * @param newVersion 数据库的新版本
 */
public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);

所以我们可以提供一个子类来继承 SQLiteOpenHelper,如下所示:

public class MySQLiteHelper extends SQLiteOpenHelper {

    private static final String TAG = "MySQLiteHelper";

    /**
     * @param context 上下文对象
     * @param name 数据库名称
     * @param factory 游标工厂
     * @param version 数据库版本,大于等于1
     */
    protected MySQLiteHelper(@Nullable Context context, @Nullable String name,
                          @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    /**
     * 数据库创建时回调的方法
     * @param db 数据库对象
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        Log.d(TAG, "-------------onCreate------------");
    }

    /**
     * 数据库版本更新时回调的方法
     * @param db 数据库对象
     * @param oldVersion 数据库的旧版本
     * @param newVersion 数据库的新版本
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(TAG, "-------------onUpgrade------------");
    }

    /**
     * 数据库打开时回调的方法
     * @param db 数据库对象
     */
    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
        Log.d(TAG, "-------------onOpen------------");
    }

}

在这里我们还需要定义它的构造方法,构造方法中的参数如下所示:

  • context :上下文对象。
  • name:数据库名称。
  • factory:游标工厂,一般我们使用不到这个参数,直接传 null 即可。
  • version:数据库的版本,传入的数必须是一个大于等于 1 的数。

除了 onCreateonUpgrade 这两个抽象方法之外,有时候我们也会重写 onOpen 方法,这个方法会在数据库打开的时候回调,为了了解这些方法的回调时机,我们通过 Log 来确定,接着在 MainActivity 中我们创建 MySQLiteHelper 对象并创建一个数据库对象,然后观察日志输出:

public class MainActivity extends AppCompatActivity {

    private MySQLiteHelper helper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        createDatabase();
    }

    private void createDatabase() {
        helper = new MySQLiteHelper(this, "default.db", null, 1);
        SQLiteDatabase db = helper.getWritableDatabase();
    }
}

MainActivity 所做的事很简单,创建了一个 MySQLiteHelper 对象,然后通过 getWritableDatabase 方法获取数据库对象,接着我们运行该工程,查看 Log 日志:

D/MySQLiteHelper: -------------onCreate------------
D/MySQLiteHelper: -------------onOpen------------

可以看到 onCreateonOpen 方法被回调了。此时我们的存储空间中就会存在一个叫做 default.db 的数据库,default.db 只会创建一次,所以当我们下次再打开时,onCreate 方法不再被回调,只会回调 onOpen 方法。

1. onCreate

前面说到 onCreate 方法会在数据库创建的时候进行回调,而数据库中存储和管理数据我们需要创建表、视图、索引等,所以在 onCreate 中创建表等是再适宜不过的了。如下所示:

假设要创建一张 Contacts 表用于存放联系人信息,它的 SQL 语句如下所示:

CREATE TABLE Contacts (
	id integer PRIMARY KEY,
	name text NOT NULL,
	phone text NOT NULL DEFAULT 'UNKNOWN', 
	unique(name, phone));

这张表有一个主键 id,联系人名字 name 和电话 phone,其中 namephone 的联合值具有唯一性约束,在 onCreate 中,我们需要先将上述 SQL 语句写在一个字符串上,然后通过调用 SQLiteDatabase#execSQL 方法来执行 SQL 语句,完成表的创建,如下所示:

@Override
public void onCreate(SQLiteDatabase db) {
    Log.d(TAG, "-------------onCreate------------");
    // 创建 Contacts 表
    String sql = "CREATE TABLE Contacts (" +
                 "id integer PRIMARY KEY," +
                 "name text NOT NULL,"     +
                 "phone text NOT NULL DEFAULT 'UNKNOWN', " +
                 "unique(name, phone))";
    db.execSQL(sql);
}

通过这种方式,我们在通过 MySQLiteHelper 获取数据库对象时就会创建出一张 Contacts 表了。接下来我们来看如何对表进行增删查的操作。

2. onUpgrade

Android 官方文档对于 onUpgrade 有如下描述:

Called when the database needs to be upgraded. The implementation should use this method to drop tables, add tables, or do anything else it needs to upgrade to the new schema version.
 
The SQLite ALTER TABLE documentation can be found here. If you add new columns you can use ALTER TABLE to insert them into a live table. If you rename or remove columns you can use ALTER TABLE to rename the old table, then create the new table and then populate the new table with the contents of the old table.
 
This method executes within a transaction. If an exception is thrown, all changes will automatically be rolled back.

我们对这段话做一个总结可以得出下面几点:

  • 如果我们需要在数据库中添加新表,我们需要调用此方法。
  • 如果我们需要调用 ALTER TABLE 语句添加/删除字段或重命名表,也应当调用此方法。
  • 该方法会在一个事务中执行,如果抛出异常就会自动回滚。

假设我们需要在我们的数据库中添加一张新表 Person,如下所示:

CREATE TABLE Person(
	name text NOT NULL,
	address text NOT NULL DEFAULT 'UNKNOWN',
	sex text NOT NULL);

onUpgrade 方法的代码如下所示:

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    String sql = "CREATE TABLE Person (" +
            "name text NOT NULL,"     +
            "address text NOT NULL DEFAULT 'UNKNOWN', " +
            "sex text NOT NULL)";
    db.execSQL(sql);
}

那么在 Activity 中,我们又要如何触发 onUpgrade 方法呢?其实很简单,将版本号升上去就可以了,如下所示:

helper = new MySQLiteHelper(this, "default.db", null, 2);

在原先的代码中我们的版本号是1,当我们改为2时,就会回调 onUpgrade 方法使得数据库完成升级。


二、在 Android 中操作数据库

这一部分介绍的是如果通过获取得到的 SQLiteDatabase 对象进行 DML 语句的操作,主要有以下4部分:

  • INSERT 插入数据
  • UPDATE 更新数据
  • DELETE 删除数据
  • SELECT 查询数据(核心)

接下来我们就来逐一介绍它们。

1. 插入数据

在插入数据的时候,我们有两种方式可以选择:

  • 直接使用 SQL 语句,然后通过 SQLiteDatabase#execSQL 方法执行相应的 SQL 语句即可;
  • 通过调用 SQLiteDatabase#insert 方法插入数据。

1.1 直接使用 SQL 语句

我们在 MainActivity 中通过一个按钮来添加相应的数据,我们想要添加的数据的 SQL 语句如下所示:

INSERT INTO Contacts(id, name, phone) VALUES(1, 'Marck', '10086');

例子如下所示:

SQLiteDatabase db = helper.getWritableDatabase();
String sql = "INSERT INTO Contacts(id, name, phone) VALUES(1, 'Marck', '10086')";
db.execSQL(sql);
db.close();

通过这种方式,我们成功地插入了一条数据。需要特别注意的是在每次插入完成之后,如果暂时不会使用到数据库,应当对其及时关闭。接下来看第二种方法。

1.2 调用 SQLiteDatabase#insert

除了直接使用原始的 SQL 语句之外,Android 还提供了一些内置方法来协助我们对数据库进行操作。insert 方法的定义如下:

/**
 * 插入数据的方法
 * @param table 表示插入数据表的名称
 * @param nullColumnHack values 为空时可以通过在 nullColumnHack 中指定可为空的列名称,
 *            为这些列显示地插入NULL值,一般我们很少使用这个参数,直接设置为null即可。
 * @param values 插入的字段值
 * @return 返回值为新插入的值的 rowid,如果插入发生错误返回 -1
 */
public long insert(String table, String nullColumnHack, ContentValues values)

那么接下来我们就通过例子来展示如何通过调用 insert 方法插入数据:

SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("id", 1);
values.put("name", "Marck");
values.put("phone", "10086");
db.insert("Contacts", null, values);
db.close();

可以看到我们先创建了一个 ContentValues 对象,然后通过 put 方法插入对应的值,put 有多个重载方法,put 的第一个参数是字段名称,第二个参数是字段值。put 有多个重载方法,可支持插入 Integer、String、Byte、Float、Boolean、Short、Long、Double、byte[] 类型的值。

2. 更新数据

更新数据同样也有两种方式可供选择:

  • 直接使用 SQL 语句
  • 调用 SQLiteDatabase#update 方法

2.1 直接使用 SQL 语句

假设我们要将之前插入数据的 phone 字段值改为 10010,相应的 SQL 语句如下所示:

UPDATE Contacts SET phone='10010' WHERE name='Marck';

在 Java 代码中,如下所示:

SQLiteDatabase db = helper.getWritableDatabase();
String sql = "UPDATE Contacts SET phone='10010' WHERE name='Marck'";
db.execSQL(sql);
db.close();

通过这种方式我们就能成功地更新语句了,同样的在使用完成之后不要忘记关闭数据库。接下来看第二种方法:

2.2 调用 SQLiteDatabase#update

我们首先还是看到 update 方法的定义:

/**
 * 更新数据的方法
 * @param table 表示数据表的名称
 * @param values 表示要更新的数据
 * @param whereClause where子句
 * @param whereArgs where子句的占位符
 * @return 修改的行数
 */
public int update(String table, ContentValues values, String whereClause, String[] whereArgs)

它的使用如下所示,还是以刚才的例子做示例:

SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("phone", "10010");
db.update("Contacts", values, "name='Marck'", null);
db.close();

在这里 ContentValues 对象存储的就是我们需要更新的字段以及更新的值,由于这里我们只需要更新一个字段,所以只 put 了一组数据。

接着第三个参数是 where 子句,也就是 SQL 语句中 WHERE 关键字后面的语句,我们这里传入的是 name= 'Marck',表示要更新的是 name 为 “Marck” 的数据。由于我们没有使用到占位符,所以第四个参数直接传入 null 即可。

那么第四个参数应该怎么用呢?我们先看示例,还是以上面的例子为例:

SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("phone", "10010");
db.update("Contacts", values, "name=?", new String[]{"Marck"});
db.close();

在第三个参数的时候,我们传入的是 new=?,其中 ? 及所谓的占位符,它会通过第四个参数中的 String 数组中的值进行替换,最终我们得到的结果还是一样的,这种方式提供给了我们更大的灵活性。

3. 删除数据

删除数据同样也有两种方式:

  • 直接使用 SQL 语句
  • 调用 SQLiteDatabase#delete方法

3.1 直接使用 SQL 语句

删除其实和更新的操作是非常相似的,我们这里要把我们前面创建的那条数据删除,SQL 语句如下:

DELETE FROM Contacts WHERE name = 'Marck';

在我们的 Android 代码中,使用如下:

SQLiteDatabase db = helper.getWritableDatabase();
String sql = "DELETE FROM Contacts WHERE name = 'Marck'";
db.execSQL(sql);
db.close();

通过这种直接执行 SQL 语句的方式,我们就能成功删除数据。接下来我们介绍使用内置方法的方式。

3.2 调用 SQLiteDatabase#delete

老规矩,先看 delete 方法的定义:

/**
 * 删除数据的方法
 * @param table 表示数据表的名称
 * @param whereClause where子句
 * @param whereArgs where子句的占位符
 * @return 删除的行数
 */
public int delete(String table, String whereClause, String[] whereArgs)

其实 delete 方法和 update 方法非常地相似,它甚至比它还少了一个 ContentValues 参数,至于原因也很简单,delete 删除的是整条数据,而不是某条数据的一部分。这里就直接上代码,不再赘述参数的含义:

SQLiteDatabase db = helper.getWritableDatabase();
db.delete("Contacts", "name='Marck'", null);
db.close();

下面的代码也可以实现相同的删除效果:

SQLiteDatabase db = helper.getWritableDatabase();
db.delete("Contacts", "name=?", new String[]{"Marck"});
db.close();

4. 查询

数据库中的查询操作是最为重要的,也是最为复杂的,在 Android 中查询数据中的数据有两种方式:

  • 通过调用 rawQuery 方法查询
  • 通过调用 query 方法查询

在介绍这两个方法之前,我们先来了解一下游标 Cursor。

4.1 游标 Cursor

首先要明确一点,无论是通过 rawQuery 还是 query 方法进行查询,最终得到的都是一个 Cursor 对象。Cursor 对象里面存储的就是我们的查询结果,它相当于是一个虚拟的结果集。在使用中我们直接把它当成游标即可。在 Cursor 中有几个关键的方法需要我们了解:

  • moveToNext:这个方法会将 Cursor 移动到下一个字段上,它的返回的是一个 boolean 值,为 true 说明下一条记录依然存在,为 false 则表示已经走到了表的最后一行,后面已经没有数据了。
  • getColumnIndex:这个方法的参数传入的是字段的名称,返回的是该字段的下标,它经常与 getXXX 方法一起使用,如果返回 -1 说明该字段不存在。
  • getXXX:这个方法用于获取一行数据的某个字段值,例如 getInt 方法获得的是 int 类型的值,getString 方法获得的是 String 类型的值。它传入的参数是字段所在的下标。
  • closeclose 方法用于关闭 Cursor,千万不要忘记在使用完 Cursor 之后调用该方法,否则很容易造成内存泄漏。

对于游标对象的使用我们采用的是遍历的方式,一般形式如下:

Cursor cursor = db.query(...);
while(cursor.moveToNext()) {
	int _id = cursor.getColumnIndex("id");
	int id = curosr.getInt(_id);
	// 获取数据...
}
cursor.close();

它的遍历形式如下图所示,假设我们的结果集中有 4 条数据:
在这里插入图片描述
稍微解释下这张图:

  • 字段下标就是上图的红色数字,它的值从 0 开始,从左到右递增。
  • moveToNext 方法用于数据行的移动,每一次调用该方法,都会将游标移动到下一行数据(如果下行有数据的话)。
  • getXXX 方法用于获取当前行的字段值,假如此时 Cursor 位于最后一行,那么 getInt("id") 的返回值就是 10。

4.2 调用 rawQuery

rawQuery 顾名思义就是执行原始 SQL 查询语句的方法,它的定义如下:

/**
 * 执行原始SQL查询语句的方法
 * @param sql 查询的SQL语句
 * @param selectionArgs SQL语句的占位符
 */
public Cursor rawQuery(String sql, String[] selectionArgs)

既然执行的是 SQL 的原始语句,那么我们就来回忆一下 SQL 的 SELECT 语句的一般形式是什么样的:

SELECT [DISTINCT] heading 
FROM table_name       -- 检索的表名 
WHERE predicate       -- where子句
GROUP BY columns      -- 分组
HAVING predicate      -- 过滤分组
ORDER BY columns      -- 排序方式
LIMIT offset, count;  -- 限定结果集的大小和范围

这就是 SELECT 语句的完整形式了,这里只做一些简单的介绍,具体的 SELECT 语句介绍可以查看这篇文章:SQLite学习笔记(二)

接下来假设数据库的 Contacts 表中有30条数据,这30条数据的id值为1~30,我们想要检索出 id 值大于10且小于20的值,可以写出如下 SQL 语句:

SELECT * FROM Contacts WHERE id>=10 AND id<=20;

在 Android 代码中,我们的代码如下所示:

SQLiteDatabase db = helper.getWritableDatabase();
String sql = "SELECT * FROM Contacts WHERE id>=10 AND id<=20";
Cursor cursor = db.rawQuery(sql, null);
while (cursor.moveToNext()){
    int id = cursor.getInt(0);
    String name = cursor.getString(1);
    String phone = cursor.getString(2);
}
cursor.close();
db.close();

如果我们用使用占位符的形式进行查询的话,则 rawQuery 方法的参数改为:

String sql = "SELECT * FROM Contacts WHERE id>=? AND id<=?;";
Cursor cursor = db.rawQuery(sql, new String[]{"10", "20"});

接下来介绍 query 方法的使用。

4.3 调用 query

一样的,我们先来看 query 方法的定义,query 有好几个重载方法,我们只看下面这个重载方法即可:

/**
 * 执行数据库查询的方法
 * @param distinct 对应SQL语句中的DISTINCT关键字,为true表示添加这个关键字,用于结果去重
 * @param table 查询的表名
 * @param columns 查询的字段名称,查询所有时指定null即可
 * @param selection 查询条件,也就是where子句
 * @param selectionArgs where子句占位符的取值
 * @param groupBy 分组条件,为null表示select中没有此关键字
 * @param having 分组的过滤条件,为null表示select中没有此关键字
 * @param orderBy 排序规则,为null表示select中没有此关键字
 * @param limit 结果集的大小和范围,为null表示select中没有此关键字
 */
public Cursor query(boolean distinct, String table, String[] columns,
            String selection, String[] selectionArgs, String groupBy,
            String having, String orderBy, String limit)

query 方法的参数虽然很多,但是都是与 SELECT 的 SQL 表现形式息息相关的,熟悉 SELECT 的应该感觉不会多难。接下来我们就仍然以上面的查询为例子,看看 query 方法如何使用:

SQLiteDatabase db = helper.getWritableDatabase();
Cursor cursor = db.query("Contacts", null, "id>=? AND id<=?", 
        new String[]{"10","20"}, null, null, null, null);
while (cursor.moveToNext()){
    int id = cursor.getInt(0);
    String name = cursor.getString(1);
    String phone = cursor.getString(2);
}
cursor.close();
db.close();

我们在这里就直接使用了带有占位符的形式进行查询了,后4个参数均为 null 表示 select 中不需要 group by、having、order by 以及 limit 关键字。而第二个参数为 null 表示查询表中的所有字段,所以它的查询就相当于如下SQL 语句:

SELECT * FROM Contacts WHERE id>=10 AND id<=20;

和我们前面所说的 SQL 语句是一模一样的。


三、开启事务

当我们需要执行批量的插入、删除、更新等操作时,开启事务是个不错的选择。其实在默认的情况下,SQLite 中的每条 SQL 语句自成事务,这种操作也成为隐式事务,这意味着每执行一条 SQL 语句就会开启、关闭一次事务。而当执行批量操作时,频繁的开启、关闭事务可能会相当的耗时,这时候我们就可以考虑开启事务。

开启事务非常简单,只需调用 SQLiteDatabase#beginTransaction 方法即可开启一个事务。而当插入数据成功之后,可以通过调用 SQLiteDatabase#setTransactionSuccessful 表示提交事务,在事务处理完成之后,通过调用 SQLiteDatabase#endTransaction 关闭事务。

我们通过一个例子来了解,假设我们需要一次插入100条数据,代码如下所示:

// 1.开启事务
SQLiteDatabase db = helper.getWritableDatabase();
for (int i = 1; i <= 100; i++) {
    String sql = "INSERT INTO Contacts(id, name, phone) VALUES(" + i + ", 'Marck" + i + "', " + i + ")";
    db.execSQL(sql);
}
// 2.提交事务
db.setTransactionSuccessful();
// 3.关闭事务
db.endTransaction();
db.close();

开启事务除了在批量操作时可以加快速度外,还可以保证的一致性。例如某个表中保存的是客户的账户余额,当客户A向客户B转100块时,需要在A的账户中减100,B的账户加100。如果让这两条操作语句单独执行的话,有可能会发生错误,例如当更新A的余额时突然发生了异常,那么B的账户余额就没有增加。而开启事务之后,如果发生这种情况,会对事务进行回滚,从而杜绝了这类事情的发生。


四、SQLite的其他注意事项

1. 关于表字段的删除

SQLite只支持添加表字段但是不支持删除表字段,所以删除表字段只能采用复制表的思想,即创建一个新表保留原表想要的字段、再将原表删除。

2. 关于SQLite 的优化操作

  • 在批量处理数据的时候,开启事务,通过减少事务频繁的开启和关闭从而有效地提高速度。
  • 在使用完 Cursor 之后应当及时地调用 close 方法关闭,避免造成内存泄漏。
  • ContentValues 底层是一个 HashMap,由于 HashMap 在扩容的时候需要对整个 Map 进行复制,比较消耗时间。所以在可预知的情况下尽量指定一个合理的 HashMap 初始化容量值。
  • 数据库的操作属于本地IO,通常会比较耗时(特别是在表比较大的情况下),建议将这些耗时的操作放入子线程中进行处理。
  • 如果检索速度较慢时,考虑使用索引会是个不错的选择,但是要千万注意索引虽然能加快检索的速度,但是表的大小会明显增加,并且执行插入、更新、删除数据的操作时也会更加慢,不正确地利用索引有可能会造成负优化。

3. ListView 的支持

ListView 通过提供两个两种适配器 SimpleCursorAdapter 和 CursorAdapter 对 Cursor 提供了适配支持,使得我们无需对 Cursor 进行处理即可将其中的数据展示到 ListView 上。


参考

2019校招Android面试题解1.0(上篇)


希望这篇文章对你有所帮助~

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/89458521