在 Android 代码中创建和修改 SQLite 数据库,我们可以参考 Android 文档 Save data using SQLite,我们在 Android 中需要采取两个基本步骤来设置 SQLite 就可以和数据库互动了。如下所示:
Define a schema and contract-创建架构和契约类
Create a database using an SQLOpenHelper-使用 SQLiteOpenHelper 创建数据库
完成上面两步后就可以执行创建、读取、更新和删除我们的数据的操作了。
创建架构和契约类
创建数据库架构其实就是需要规划数据库结构。所以我们需要问两个问题:
表格的名称是什么?
表格里列的名称和数据类型是什么?
还记得下面的语句吗?
CREATE TABLE ( , , ...);
为什么要使用 Contract 类?
看个创建表格的两个例子:
String makeTableStatement = "CREATE TABLE entry (_id INTEGER PRIMARY KEY, entryid TEXT, title TEXT, subtitle TEXT)";
String SQL_CREATE_ENTRIES = "CREATE TABLE" + FeedEntry.TABLE_NAME + "(" + FeedEntry._ID + "INTEGER PRIMARY KEY," + FeedEntry.COLUMN_NAME_ENTRY_ID + "TEXT," + FeedEntry.COLUMN_NAME_TITLE+"TEXT);"
观察上述代码,我们发现第二个创建表格的语句中 表格名称和列名称存储在了常量里。 在生成 SQL 指令时,消除了出现拼写错误的可能性,或者不小心将不应该大写的字母大写了。如果想更改实际的列名称,也只需在一个地方更改下就行了。
总结使用 Contract 类的三个理由:
根据之前规划的数据库结构即我们定义的架构,我们在项目的主包下面创建一个包名为 data,然后在改包下创建一个新的 java 类名为 PetContract,将该类用 final 修饰,因为它只是用来提供常量,我们不需要扩展或为此外部类实现任何内容。使用我们定义的架构,为每个表格创建一个内部类,并为每个列标题创建常量,代码如下所示:
public final class PetContract {
private PetContract() {}
public static final class PetEntry implements BaseColumns {
public final static String TABLE_NAME = "pets";
public final static String _ID = BaseColumn._ID;
public final static String COLUMN_PET_NAME = "name";
public final static String COLUMN_PET_BREED = "breed";
public final static String COLUMN_PET_GENDER = "gender";
public final static String COLUMN_PET_WEIGHT = "weight";
public final static int GENDER_UNKONWN = 0;
public final static int GENDER_MALE = 1;
public final static int GENDER_FORMALE = 2;
}
}
在 dada 包中创建一个新的 PetDbHelper 类,该类继承自 SQLiteOpenHelper类。我们需要重写 onCreate() 和 onUpGrade() 方法,并且为数据库名称和版本创建常量,同时别忘了创建该类的构造函数,还要为用来创建表格的 SQLite 指令创建一个字符串常量。
/**
* Database helper for Pets app. Manages database creation and version management.
*/
public class PetDbHelper extends SQLiteOpenHelper {
public static final String LOG_TAG = PetDbHelper.class.getSimpleName();
/** Name of the database file */
private static final String DATABASE_NAME = "shelter.db";
/**
* Database version. If you change the database schema, you must increment the database version.
*/
private static final int DATABASE_VERSION = 1;
/**
* Constructs a new instance of {@link PetDbHelper}.
*
* @param context of the app
*/
public PetDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
/**
* This is called when the database is created for the first time.
*/
@Override
public void onCreate(SQLiteDatabase db) {
// Create a String that contains the SQL statement to create the pets table
String SQL_CREATE_PETS_TABLE = "CREATE TABLE " + PetEntry.TABLE_NAME + " ("
+ PetEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ PetEntry.COLUMN_PET_NAME + " TEXT NOT NULL, "
+ PetEntry.COLUMN_PET_BREED + " TEXT, "
+ PetEntry.COLUMN_PET_GENDER + " INTEGER NOT NULL, "
+ PetEntry.COLUMN_PET_WEIGHT + " INTEGER NOT NULL DEFAULT 0);";
// Execute the SQL statement
db.execSQL(SQL_CREATE_PETS_TABLE);
}
/**
* This is called when the database needs to be upgraded.
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// The database is still at version 1, so there's nothing to do be done here.
}
}
写了这么多我们也不知道数据库是否能正常运行,我们可以通过下面的方法进行检查。
查看数据库是否正常运转:
创建 displayDatabaseInfo() 方法,将其放置到我们代码中,只有 SQL 能正常运转时,该方法才能运转。
将数据库下载到本地,通过在终端使用 SQL 语句查看。
@Override
protected void onCreate(Bundle savedInstanceState) {
…
displayDatabaseInfo()
}
/**
* Temporary helper method to display information in the onscreen TextView about the state of
* the pets database.
*/
private void displayDatabaseInfo() {
// To access our database, we instantiate our subclass of SQLiteOpenHelper
// and pass the context, which is the current activity.
PetDbHelper mDbHelper = new PetDbHelper(this);
// Create and/or open a database to read from it
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// Perform this raw SQL query "SELECT * FROM pets"
// to get a Cursor that contains all rows from the pets table.
Cursor cursor = db.rawQuery("SELECT * FROM " + PetEntry.TABLE_NAME, null);
try {
// Display the number of rows in the Cursor (which reflects the number of rows in the
// pets table in the database).
TextView displayView = (TextView) findViewById(R.id.text_view_pet);
displayView.setText("Number of rows in pets database table: " + cursor.getCount());
} finally {
// Always close the cursor when you're done reading from it. This releases all its
// resources and makes it invalid.
cursor.close();
}
}
PetDbHelper mDbHelper = new PetDbHelper(this);
SQLiteDatabase db = mDbHelper.getReadableDatabase();
请求数据库,mDbHelper 将检查是否已经存在一个数据库?
如果不存在,则 PetDbHelper 的实例将使用 onCreate() 方法创建一个数据库,接着创建 SQLiteDatabase 的实例对象返回给 Activity。
如果数据库已经存在,PetDbHelper 的实例将不会调用 onCreate() 方法,相反,将创建一个 SQLiteDatabase 的实例对象并关联现有的数据库,然后将该对象返回给请求数据库的 Activity。
最终时帮助我们创建 SQLiteDatabase 对象与 shelter 数据库关联的 SQLiteDatabase 对象,之后我们就可以通过该对象向 shelter 数据库传达 SQLite 指令了。
从设备中提取数据库
了解了数据库的连接流程,我们就可以检测数据库是否正常运转。当然我们还可以通过另一种方式,通过查看设备的文件系统,使我们能够实际地看到 petDbHelper 类在创建数据库,然后当数据库实际创建好后看到 .db 文件出现。
在这之前我们可以打开手机应用信息,将 pets 应用中清楚缓存和存储数据。同时注释掉代码中的 displayDatabaseInfo() 方法。在 Android Studio 中点击 Android Device Monitor 按钮,选择我们当前使用的模拟器,转到 File Explorer,该功能可以让我们浏览设备的文件系统,点开 data 文件夹,再点开里面的 data 文件夹,在里面找到我们应用的包,再打开包,查找文件,我们会发现没有 .db 文件,这时我们的 getReadableDatabase() 方法尚未被调用。
当我们把刚才注释掉的 displayDatabaseInfo() 方法重新恢复,运行代码后,我们再查看文件系统中该包下有了 shelter.db 数据库文件了。再运行一次就不会再出现新的文件了。如果想看 shelter.db 文件中的内容,可以通过 DDMS 下载文件,保存到电脑本地上,通过 SQL 语言在终端查看。
在 Android 文档 Put information into a database 中,为我们举出了例子。
// Gets the data repository in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);
// Insert the new row, returning the primary key value of the new row
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);
ContentValues 中存储了大量的键值对,键是数据库中的列名称,值就是插入的值。举例:
ContentValues values = new ContentValues();
values.put(PetEntry.COLUMN_PET_NAME, "Garfield");
values.put(PetEntry.COLUMN_PET_BREED, "Tabby");
values.put(PetEntry.COLUMN_PET_GENDER, PetEntry.GENDER_MALE);
values.put(PetEntry.COLUMN_PET_WEIGHT, 7);
db.insert(PetEntry.TABLE_NAME, null, values);
注意 insert() 方法会返回新插入的 ID,如果出现错误它会返回 -1。
数据库查询方法
我们可以通过 db.rawquery() 方法来读取数据库,但不建议使用。就像存在 SQLiteDatabase.insert() 方法一样,我们可以使用 SQLiteDatabase.query() 方法来读取数据库,它会帮助我们构建查询,从而避免语法错误。我们看下 Android 文档来了解下:
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
BaseColumns._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_SUBTITLE
};
// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };
// How you want the results sorted in the resulting Cursor
String sortOrder =
FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";
Cursor cursor = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
);
观察上述代码,我们发现首先定义了一个字符串数组 projection,它其实是指我们想要获取的列名称,就像这样的语句:
SELECT name, breed FROM pets;
定义 projection 使我们能够指定想要获取的具体列,默认则获取所有列,类似于:
SELECT * FROM pets;
projection 大小会影响到性能。
调用 query() 方法,该方法具有大量输入参数,代表了 SELECT 语句的不同部分。第一个参数 projection 我们已经在上面介绍了,接下来 selection 和 selectionArgs 处理的是可选 WHERE 条件。例如根据 ID 选择单个宠物:
SELECT * FROM pets WHERE _id = 1;
使用 selection 和 selectionArgs 属性就是这样的:
// Define 'where' part of query
String selection = PetEntry._ID + "?";
// Specify arguments in placeholder order
String[] selectionArgs = {"1"};
selection 参数是 WHERE 关键字之后的类型为 String 的,这里使用 ? 作为占位符,然后填充为 selectionArgs 参数中的值,它是一个字符串数组,负责替换这里 selection 中的问号,这里将其设为 1。
为什么使用 ? 号和 selectionArgs ,而不是直接写成 1 ?
这里没有区别,我们可以将 selectionArgs 设为 null,将 ? 号改为 1 。但是在某些情况下,选择内容可能来自表格,使用占位符是一种安全措施,可以防止出现 SQL 注入攻击,也就是用户不按套路出牌,编辑一些代码类内容输入,使我们的查询语句出现歧义错误。
调用 query() 方法后会返回一个 Cursor 对象,它是一种可以捕获数据库中所有子集的对象。
什么是 Cursor
简而言之,Cursor 是指 代表数据库中多行内容的对象。假设如下是我们整个数据库的架构:
这里写图片描述
如果我们针对这个数据库中这个表格调用 query() 方法,我们可以指定希望返回的值,然后这些信息就会以 Cursor 对象的形式返回给我们。
如果我们的选择参数是
SELECT * FROM pets
我们获得的 Cursor 对象,其中包含数据库中的各行内容。
如果我们希望进一步指定仅选择 pets 表格中宠物名为 Toto 的行,我们获得的 CUrsor 对象就是其中所有行里的宠物名是 Toto 的行,结果如下:
我们再来看另一个示例,假设从 pets 表格中选择 name 和 breed,也就是将白鸽缩小为仅返回这两列:
也就是将表格缩小为仅返回这两列,返回的 Cursor 对象包含所有行,但是仅包含 name 和 breed 列的详情。
Cursor 包含了是我们能够访问和浏览 Cursor 对象的方法,如果 Cursor 包含多行的话就可以浏览各行。
Cursor.getCount()
Cursor 对象代表了数据库中的行和列,此外,他还提供了这些行中的当前位置,为了能获得特定的数据,我们需要将 Cursor 移动到我们所需要的精确行
当我们第一次获取 Cursor 时,位置从 -1 开始,这是无效的位置,首个可用位置为0,然后逐步递增。
Cursor.moveToFirst
该方法既将 Cursor 中的位置移动到结果中的第一行,这使我们能够访问第一条记录里的数据。
Cursor.moveToLast
Cursor.moveToPosition(int position)
该方法会将 Cursor 位置移动到指定位置。
上面方法返回的都是 Boolean 类型的值,这样可以帮助我们判断实际是否移动到了该位置。比如我们当前 Cursor 已经移动到了最后一行,再调用向下移动的方法就会返回 false。
使用 getColumnIndex(String columnName)方法,来根据名称来获取列的索引。举例:
对于 Cursor 要注意的一个事项是,使用完毕后,一定要记得调用 cursor.close(),这样会完全清空 Cursor 使其无效。仅在完全操作完毕后调用该方法。不关闭 Cursor 的话,会因内存泄漏而降低性能。