文章目录
概述
SQLite 数据库已经了解得差不多了。
下面将侧重与如何在应用中创建数据库,然后学习如何插入及查询数据。
起始项目
Pets 应用 的初始代码可以从 GitHub 仓库获得,使用 Git 命令克隆到初始代码:
git clone -b starting-point https://github.com/HEY-BLOOD/Pets.git
之后导入 到 Android Studio 中运行,。
定义架构和协定
SQL 数据库的主要原则之一是架构,即数据库组织方式的正式声明。架构反映在您用于创建数据库的 SQL 语句中。您可能会发现创建伴随类(称为协定类)很有用,该类以系统化、自记录的方式明确指定了架构的布局。
协定类是定义 URI、表和列名称的常量的容器。通过协定类,您可以在同一软件包的所有其他类中使用相同的常量。这样一来,您就可以在一个位置更改列名称并将其传播到整个代码中。
组织协定类的一种良好方法是将对整个数据库而言具有全局性的定义放入类的根级别。然后,为每个表创建一个内部类。每个内部类都枚举相应表的列。
Schema
架构就是规划好的 数据库基本结构,如 表名、字段名、字段类型等。
比如 Pets 应用的数据库架构如下(只含一个 pets 数据表):
pets | ||||
---|---|---|---|---|
_id(INTEGER) | name(TEXT) | breed(TEXT) | gender(INTEGER) | weight(INTEGER) |
…… | …… | …… | …… | …… |
创建 Contract 类
因为将要用到大量与数据相关的类,所以创建一个名叫 data
的 Java 包,这样可以将具有特定数据功能的类与 Activity 类区分开来。
然后创建 PetContract
类,作为数据库的协定。因为这个类是用于提供常量构建数据库的,所以加上 final
关键字而不能被扩展,并定义一个 私有且无参数的构造函数,使得不能在外部创建实例。
再定义表名 和字段名,PetContract.java
:
package com.example.pets.data;
import android.provider.BaseColumns;
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 = BaseColumns._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";
/**
* Possible values for the gender of the pet.
*/
public static final int GENDER_UNKNOWN = 0;
public static final int GENDER_MALE = 1;
public static final int GENDER_FEMALE = 2;
}
}
当数据库的所需的所有常量都定义完成后,在需要用到的地方更新代码,比如在 EditorActivty
中 mGender
性别变量在赋值时应使用 PetEntry
中预定的常量,如:
mGender = PetEntry.GENDER_MALE; // Male
别忘了先导入这个内部类:
import com.example.pets.data.PetContract.PetEntry;
更改详细的代码更改,参考 此链接。
使用 SQLiteOpenHelper 创建数据库
定义了数据库架构的协定后,应实现用于创建和维护数据库和表的SQLiteOpenHelper
抽象类 。
Android 会将您的数据库存储在您应用的私有文件夹中。您的数据安全无虞,因为在默认情况下,其他应用或用户无法访问此区域。
SQLiteOpenHelper
类包含一组用于管理数据库的实用 API。当您使用此类获取对数据库的引用时,系统仅在需要时才执行可能需要长时间运行的数据库创建和更新操作,而不是在应用启动期间执行。您仅需调用 getWritableDatabase()
或 getReadableDatabase()
即可。
如需使用 SQLiteOpenHelper
,请创建一个用于替换 onCreate()
和 onUpgrade()
回调方法的子类。您可能还需要实现 onDowngrade()
或 onOpen()
方法,但这些方法并非必需。
继承 SQLiteOpenHelper
这个实现类将具有以下功能:
- 在首次访问应用使创建要使用的数据库
- 下次在设备上访问应用时,读取现有的数据库
- 在数据库版本发送更改时,更新数据库的架构
为此需要以下步骤:
- 创建一个类 ,继承自
SQLiteOpenHelper
- 分别为数据库的名称和 数据库版本定义常量
- 提供一个构造函数
- 实现
onCreate()
方法,这会在首次创建数据库时使用 - 实现
onUpgrade()
方法,在数据库架构出现变化时使用
最终 SQLiteOpenHelper
的实现类 PetDbHelper
如下,在 data
Java 包中创建:
package com.example.pets.data;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;
import com.example.pets.data.PetContract.PetEntry;
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(@Nullable Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@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.
}
}
数据库名称定义为 shelter.db
,版本 1
,一个调用了父类方法的构造函数。
在 onCreate
方法中 定义创建数据库的 SQL语句,并 调用数据库对象 的 execSQL
创建数据库。
由于目前数据库架构为首次创建,无须更改,所以 onUpgrade
方法留空处理。
不过这些都还只是定义,目前并没有使用到这个扩展类。
创建并连接数据库
现在用于 创建和 管理数据库的 PetDbHelper
类已经准备好了,如何使用它来创建数据库?
可以在 CatalogActivity 中定义一个 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();
}
}
- 8 行,实例化 PetDbHelper 对象(继承自 SQLiteOpenHelper 抽象类)
- 11 行,创建数据库 或 打开已经存在的数据库,
getReadableDatabase()
只能获取只读的数据库对象,如要 执行 插入、删除等操作需使用getWritableDatabase()
方法 - 15 行,从数据表中查询所有记录,保存在 Cursor 对象中
- 16 ~ 25 行,将 数据表的行号 显示 在 界面当中,并始终关闭 数据库的游标对象
不要忘了在 CatalogActivty
的 onCreate
方法中 调用 辅助方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_catalog);
…………
displayDatabaseInfo();
}
最后运行应用,如无误应该能看到如下的一行文本,提示数据库中 有 0 条数据,因为这是首次创建的数据库,未插入任何数据:
将信息添加到数据库
通过将 ContentValues
对象传递给 insert()
方法,将数据插入到数据库中:
// Gets the data repository in write mode
SQLiteDatabase db = dbHelper.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);
insert()
的第一个参数就只是表名。
第二个参数将指示框架在 ContentValues
为空(即您没有 put
任何值)时应执行哪些操作。如果您指定列名称,框架会插入一行,并将该列的值设置为 null。如果您指定 null
(如此代码示例所示),框架在没有值时不会插入行。
insert()
方法会返回新创建行的 ID;如果在插入数据时出错,会返回 -1。如果您的数据与数据库中已有的数据之间存在冲突,就会出现这种情况。
通过菜单项插入虚假宠物
在 CatalogActivity
的菜单项中 有两个选项,分别 为 Insert Dummy Data
和 Delete All Pets
。当我们点击 Insert Dummy Data 时将直接添加一只宠物到数据库中。
所以在 CatalogActivity.java 中定义一个插入虚拟数据的辅助方法 insertPet()
:
/**
* Helper method to insert hardcoded pet data into the database. For debugging purposes only.
*/
private void insertPet() {
// Gets the database in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// Create a ContentValues object where column names are the keys,
// and Toto's pet attributes are the values.
ContentValues values = new ContentValues();
values.put(PetEntry.COLUMN_PET_NAME, "Toto");
values.put(PetEntry.COLUMN_PET_BREED, "Terrier");
values.put(PetEntry.COLUMN_PET_GENDER, PetEntry.GENDER_MALE);
values.put(PetEntry.COLUMN_PET_WEIGHT, 7);
// Insert a new row for Toto in the database, returning the ID of that new row.
// The first argument for db.insert() is the pets table name.
// The second argument provides the name of a column in which the framework
// can insert NULL in the event that the ContentValues is empty (if
// this is set to "null", then the framework will not insert a row when
// there are no values).
// The third argument is the ContentValues object containing the info for Toto.
long newRowId = db.insert(PetEntry.TABLE_NAME, null, values);
}
接着 在 点击 nsert Dummy Data 菜单项时 的回调中 , 调用 插入虚拟宠物的方法 ,并更新 显示的记录条数(CatalogActivity 中)
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// User clicked on a menu option in the app bar overflow menu
switch (item.getItemId()) {
// Respond to a click on the "Insert dummy data" menu option
case R.id.action_insert_dummy_data:
insertPet();
displayDatabaseInfo();
return true;
// Respond to a click on the "Delete all entries" menu option
case R.id.action_delete_all_entries:
// Do nothing for now
return true;
}
return super.onOptionsItemSelected(item);
}
程序运行如下 每插入一次虚拟数据,数据库中的行数 就会 加 1
,代码更改前后 差异对比
通过编辑器插入宠物
想要通过编辑器插入宠物 ,首先要获得 编辑器中 输入的内容,然后将其 封装成 ContentValues
对象 并写入到数据库中。
而 这一系列 的操作都应该是在编辑完宠物信息后,点击提交时触发:
因此可以采取和 插入虚拟时采取同样的方法,当点击提交时,通过调用一个辅助方法 来 插入宠物信息,并在插入完成后 退出到父级 Activity,在 EditorActivity
中 更改:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// User clicked on a menu option in the app bar overflow menu
switch (item.getItemId()) {
// Respond to a click on the "Save" menu option
case R.id.action_save:
// Save pet to database
insertPet();
// Exit activity, back to parent Activity
finish();
return true;
………………
}
return super.onOptionsItemSelected(item);
}
然后 在 EditorActivity
中 实现,用于插入宠物的 insertPet
辅助方法:
/**
* Get user input from editor and save new pet into database.
*/
private void insertPet() {
// Read from input fields
// Use trim to eliminate leading or trailing white space
String nameString = mNameEditText.getText().toString().trim();
String breedString = mBreedEditText.getText().toString().trim();
String weightString = mWeightEditText.getText().toString().trim();
int weight = Integer.parseInt(weightString);
// Create database helper
PetDbHelper mDbHelper = new PetDbHelper(this);
// Gets the database in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// Create a ContentValues object where column names are the keys,
// and pet attributes from the editor are the values.
ContentValues values = new ContentValues();
values.put(PetEntry.COLUMN_PET_NAME, nameString);
values.put(PetEntry.COLUMN_PET_BREED, breedString);
values.put(PetEntry.COLUMN_PET_GENDER, mGender);
values.put(PetEntry.COLUMN_PET_WEIGHT, weight);
// Insert a new row for pet in the database, returning the ID of that new row.
long newRowId = db.insert(PetEntry.TABLE_NAME, null, values);
// Show a toast message depending on whether or not the insertion was successful
if (newRowId == -1) {
// If the row ID is -1, then there was an error with insertion.
Toast.makeText(this, "Error with saving pet", Toast.LENGTH_SHORT).show();
} else {
// Otherwise, the insertion was successful and we can display a toast with the row ID.
Toast.makeText(this, "Pet saved with row id: " + newRowId, Toast.LENGTH_SHORT).show();
}
}
在 insertPet
辅助 方法 只获取了 宠物的 姓名、品种和体重,而并没有获取 性别,可在 封装 ContentValues
对象时却 直接使用了 成员变量中 mGender
。
这是因为在 编辑器的性别 Spinner 创建就已经绑定了 监听器,每当 选择不同的性别时就会实时获取到,所以在 insertPet
方法中就不需要再去获取性别。
然后需要更改 CatalogActivity 用于显示 数据库 记录的 displayDatabaseInfo()
方法的调用位置,因为当我们进入编辑器时,CatalogActivity 并没有被销毁,而是处于暂停的状态,而当我们在编辑器中编辑完数据并保存后,会直接退回到 CatalogActivity,这时候原本的 CatalogActivity 应该是从停止的状态返回到启动的状态,而不会调用 onCreate()
方法,而是调用 onStart()
方法,所以将显示数据库记录的 displayDatabaseInfo()
辅助方法移到 onStart()
方法中。
@Override
protected void onStart() {
super.onStart();
displayDatabaseInfo();
}
这样当我们从编辑器中保存数据后,再回到 CatalogActivity,就能够立即看到插入数据后的总记录数。代码更改前后 差异对比
数据库查询方法
选择 宠物信息已经成功添加到数据库当中了,问题是 如何 将数据库中的宠物信息 显示出来呢,这就需要 对数据库进行读取。
读取数据库最直接的方法是 调用 execSQL()
或 rawQuery()
方法来 执行 SQL 查询语句,但这个方法不安全,有 SQL 注入的风险存在,同时也在 组织查询语句也 容易出现错误,因此不推荐。
更好的 方法是 使用 query()
方法来查询数据,并充分利用 数据库协定中定义的常量,以此避免 更多的 错误情况。
如需从数据库中读取信息,请使用 query()
方法,向其传递您的选择条件和所需的列。该方法合并了 insert()
和 update()
元素,不过列列表定义了要提取的数据(“预测值”),而不是要插入的数据。查询结果会包含在 Cursor
对象中返回给您。
SQLiteDatabase db = dbHelper.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
);
第一个参数为被查询表的名称,第二个 参数是一个包含 查询字段名称的 字符串数组。
第三个参数和第四个参数(selection
和 selectionArgs
)会合并起来创建一个 WHERE 子句。由于这两个参数是与选择查询分开提供的,因此它们在合并之前会进行转义。这样一来,您的选择语句就不受 SQL 注入的影响。如需详细了解所有参数,请参阅 query()
参考。
Cursor 对象
Cursor 类型是什么样的数据?
query()
方法返回的 Cursor 对象包含 查询到的结果集,其实就是一组数据而已,可以想象成二维表格,有很多行且每行 又表示一个实体的信息。
如需查看光标中的某一行,请使用 Cursor
move 方法之一,您必须始终在开始读取值之前调用该方法。由于光标从位置 -1 开始,因此调用 moveToNext()
会将“读取位置”置于结果的第一个条目上,并返回光标是否已经过结果集中的最后一个条目。对于每一行,您都可以通过调用 Cursor
get 方法之一(例如 getString()
或 getLong()
)读取列的值。对于每个 get 方法,您必须传递所需列的索引位置,您可以通过调用 getColumnIndex()
或 getColumnIndexOrThrow()
获取该位置。遍历结果之后,请对光标调用 close()
以释放其资源。例如,以下代码展示了如何获取存储在光标中的所有项目 ID 并将其添加到列表中:
List itemIds = new ArrayList<>();
while(cursor.moveToNext()) {
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedEntry._ID));
itemIds.add(itemId);
}
cursor.close();
查询并显示宠物信息
下面将在 Pets 应用中 使用 query()
方法查询数据,再通过返回 的 Cursor 对象 读取查询到的内容,并显示到主界面 CatalogActivity 中。
查询并显示宠物的代码可以写在 displayDatabaseInfo()
辅助方法中,因此这个方法原本就是 查询 数据库记录数的方法。
首先将原本的 rawQuery()
查询方法,改为使用 query()
查询所有 宠物信息:
/**
* Temporary helper method to display information in the onscreen TextView about the state of
* the pets database.
*/
private void displayDatabaseInfo() {
// Create and/or open a database to read from it
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,
PetEntry.COLUMN_PET_NAME,
PetEntry.COLUMN_PET_BREED,
PetEntry.COLUMN_PET_GENDER,
PetEntry.COLUMN_PET_WEIGHT,
};
// Perform a query on the pets table
Cursor cursor = db.query(
PetEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
null, // The columns for the WHERE clause
null, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // The sort order
);
TextView displayView = (TextView) findViewById(R.id.text_view_pet);
try {
………………
} finally {
// Always close the cursor when you're done reading from it. This releases all its
// resources and makes it invalid.
cursor.close();
}
}
在 try 语句中读取 查询到的所有宠物信息,并显示到 主界面中:
try {
// Create a header in the Text View that looks like this:
//
// The pets table contains <number of rows in Cursor> pets.
// _id - name - breed - gender - weight
//
// In the while loop below, iterate through the rows of the cursor and display
// the information from each column in this order.
displayView.setText("Number of rows in pets database table: " + cursor.getCount() + "\n\n");
displayView.append(String.format("%s - %s - %s - %s - %s\n",
PetEntry._ID, PetEntry.COLUMN_PET_NAME, PetEntry.COLUMN_PET_BREED,
PetEntry.COLUMN_PET_GENDER, PetEntry.COLUMN_PET_WEIGHT));
// Figure out the index of each column
int idColumnindex = cursor.getColumnIndexOrThrow(PetEntry._ID);
int nameColumnindex = cursor.getColumnIndexOrThrow(PetEntry.COLUMN_PET_NAME);
int breedColumnindex = cursor.getColumnIndexOrThrow(PetEntry.COLUMN_PET_BREED);
int genderColumnindex = cursor.getColumnIndexOrThrow(PetEntry.COLUMN_PET_GENDER);
int weightColumnindex = cursor.getColumnIndexOrThrow(PetEntry.COLUMN_PET_WEIGHT);
// Iterate through all the returned rows in the cursor
while (cursor.moveToNext()) {
// Use that index to extract the String or Int value of the word
// at the current row the cursor is on.
int currentID = cursor.getInt(idColumnindex);
String currentName = cursor.getString(nameColumnindex);
String currentBreed = cursor.getString(breedColumnindex);
int currentGender = cursor.getInt(genderColumnindex);
int currentWeight = cursor.getInt(weightColumnindex);
// Display the values from each column of the current row in the cursor in the TextView
displayView.append(String.format("%d - %s - %s - %d - %d\n",
currentID, currentName, currentBreed, currentGender, currentWeight));
}
}
- 9 行,提示查询到的总记录数量。
- 10 ~ 12 行,打印表头
- 15 ~ 19 行,获取每个字段 在 查询到的结果集中的 列索引位置
- 22 ~ 33 行,读取每行 数据的所有字段,并显示(由于行索引是从 -1 (表头)开始的,所以当首次 调用
cursor.moveToNext()
方法时,就会 转到 索引 为 0 的第一行数据中)
更改完成后运行应用,代码更改前后差异对比
总结
成功向 Android 应用中添加了 SQLiteDatabase,学习了如何向数据库输入数据和查询数据,以便在用户界面显示数据,尽管比较简陋。
通过创建 PetDbHelper 类(继承自 SQLiteOpenHelper)来创建和管理数据库,还使用了Android 框架类 例如 SQLiteDatabase、ContentValues 和 Cursor。
参考
想知道为何数据库经常用圆柱体来表示?详情请参阅此处。
Save data using SQLite | Android Developers