数据库
前言
众所周知,数据存储在每个应用中都会用到,那所用到的技术应该怎么选呢,这里Android给开发者提供了几种方法去保存常用应用数据,至于你想选择哪一种方式,取决于你的特定需求;例如这个数据是本应用私有的还是跟其它应用共享以及数据存储所需的内存空间等
- Shared Preferences:这种方式通常用来保存私有原始数据,以键值对形式存储;这也就意味着这些数据只能由本应用访问
- Internal Storage:这种方式是将私有数据保存在内部存储(设备内存)中,实际上是使用文件流进行读写
- External Storage:这种方式是将公共数据存储在共享外部存储上;也就是将数据存储在SD卡上,存储在这上面说明数据是开放的,其它应用可以直接访问;这跟上面一种都是平常所说的文件存储
- SQLite Databases:这种方式是将结构化数据存储在私有数据库中;这也就是常说的数据库存储,使用SQLite保存数据,这些数据是私有的
- Network Connection:这种方式是将数据存储在Web服务器上,也就是通常所说的网络存储
笔者上一篇文章讲述了第二种和第三种方式的使用封装,这篇文章来掰掰第四种方式,也就是数据库存储
数据库存储
本文所含代码随时更新,可从这里下载最新代码
传送门Mango
做Android开发的绝大多数应该都用过Sqlite了,开发者可以使用它来保存应用私有数据,这里简单介绍下Sqlite
- SQLite是一个进程内库:它实现了一个独立的,无服务器,零配置的事务SQL数据库引擎,且支持SQL语句
- SQLite是一个嵌入式SQL数据库引擎:与大多数其他SQL数据库不同,SQLite没有单独的服务器进程; SQLite直接读写普通磁盘文件;数据库文件格式是跨平台的,可以在32位和64位系统之间自由复制数据库
- SQLite是一个紧凑的库:启用所有功能后,库大小可能小于600KiB,具体取决于目标平台和编译器优化设置;通常情况下给与的内存越多,运行越快;即使在低内存环境中,性能通常也非常好,SQLite可以比文件系统I / O更快
- Sqlite最重要的一点是开源的
现在一些主流的移动设备,比如Android,iPhone,pad等都在使用Sqlite存储数据,这也就意味着我们必须要掌握SQLite的开发
其实SQLite还有一个最大的特点是你可以把各种类型的数据保存到任何字段中,而不用关心字段声明的数据类型是什么;一般其它数据库采用的是静态数据类型,即你定义的字段类型是什么,那存入的数据就得是相应的数据类型;但是SQLite采用的是动态数据类型,会根据存入值自动判断;比如可以在Integer类型的字段中存放字符串,或者在布尔型字段中存放浮点数,或者在字符型字段中存放日期型值;但有一种情况例外:定义为INTEGER PRIMARY KEY的字段只能存储64位整数, 当向这种字段保存除整数以外的数据时,将会产生错误
数据库创建
内置存储数据库
创建新SQLite数据库的推荐方法是创建SQLiteOpenHelper的子类并覆盖onCreate()方法,在该方法中可以执行SQLite命令以在数据库中创建表
/**
* @Description TODO(数据库及表创建)
* @author cxy
* @Date 2018/11/5 11:07
*/
public class SQLiteDBHelper extends SQLiteOpenHelper{
//类还没有实例化,只能是static修饰才能用来做参数
private static final String DATABASE_NAME = "mango";
private static final String TABLE_USER = "user";
private static final int DATABASE_VERSION = 1;
/**
* 重载构造方法
* @param context
*/
public SQLiteDBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
/**
* 构造方法
* @param context 上下文
* @param name 数据库名
* @param factory 游标工厂,默认为null,即使用默认工厂
* @param version 数据库版本号
*/
public SQLiteDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
/**
* 数据库第一次创建时被调用
* 用来创建表
* 或者一些数据初始化操作
* @param db
*/
@Override
public void onCreate(SQLiteDatabase db) {
String dataBaseSql = "create table if not exists " + TABLE_USER+ "(uid integer primary key autoincrement,name varchar(20),sex varchar,role varchar(10))";
db.execSQL(dataBaseSql);
}
/**
* 升级软件时更新数据库表结构
* 通过比对版本号
* 一般做删除数据表,并建立新的数据表操作
* @param db
* @param oldVersion 上一次版本号
* @param newVersion 最新版本号
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
/**
* 打开一个只读数据库
* 如果数据库不存在,Android系统会自动生成一个数据库,接着调用onCreate()方法
* @return 获取一个用于操作数据库的SQLiteDatabase实例
*/
@Override
public SQLiteDatabase getReadableDatabase() {
return super.getReadableDatabase();
}
/**
* 创建或打开一个读写数据库
* 如果数据库不存在,Android系统会自动生成一个数据库,接着调用onCreate()方法
* @return 获取一个用于操作数据库的SQLiteDatabase实例
*/
@Override
public SQLiteDatabase getWritableDatabase() {
return super.getWritableDatabase();
}
}
- SQLiteOpenHelper是Android提供的一个供开发者使用SQLiteDatabase的辅助类,用来管理数据库及表的创建和版管理
- 重写构造方法,传递必要的参数,在这里数据库还没有创建,只是构建一个用于创建,打开或者管理数据库的辅助类实例
- 重写onCreate方法,当数据库第一次创建时,这个方法被调用(不需要用户手动调用),用来创建表和初始化数据
- 重写onUpgrade方法,当数据库版本变化时被调用;比如淘宝APP要升级,同时一些表结构变了,像表User多了一个字段,但是用户手机上还是原来的数据库,表User没有新的字段,那这时候APP要想正常工作就需要修改版本号,在这个方法中去对表User做一些修改
- getReadableDatabase和getWritableDatabase方法都是用来获取一个用于操作数据库的SQLiteDatabase实例,但getWritableDatabase() 方法以读写方式打开数据库,一旦数据库的磁盘空间满了,数据库就只能读而不能写,倘若使用getWritableDatabase()打开数据库就会出错;getReadableDatabase()方法先以只读方式打开数据库,如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读方式打开数据库
- 数据库真正的创建是在调用getReadableDatabase和getWritableDatabase两个方法的时候,创建完后才会回调onCreate方法创建表;并且这两个方法在数据库upgrade时可能会花费较长时间,不要在主线程调用
注意:在这里有一个版本号的规范问题,在API24中,在打开或者创建数据库时,如果之前的数据库版本号与最新的版本号不一致,同时之前的版本号是0,那只会回调onCreate方法(这也是首次使用默认历史版本号为0);如果经过了N个版本更新后,新的版本号比历史版本号大,那就回调onUpgrade方法;如果新的版本号小于历史版本号,那就回调onDowngrade方法,这个方法有一个默认实现是抛出异常;所以编写版本号的时候一定要注意
public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
throw new SQLiteException("Can't downgrade database from version " +
oldVersion + " to " + newVersion);
}
外置存储数据库
上面方法创建数据库,db文件是存放在内置存储中,路径在data/data/包名/databases 目录下,如果某些情况下出现问题需要获取数据库文件进行分析,那显然操作内置存储是很不方便的,并且内置存储内存空间有限,对数据存储限制比较大;这时候我们就可以在外置存储中创建数据库文件,通常做法是提前使用工具创建好数据库文件,并创建好相关表,然后放到工程assets目录或者raw目录下,在程序运行时将db文件拷贝到SD卡,再通过SQLiteOpenHelper访问这个数据库
这里推荐大家一款SQLite可视化工具SQLiteManager,可用它来创建一个db文件,并且创建你需要的表
我这里创建了一个mango.db数据库文件,并且创建了一个user表,字段与上面一样
那接下来怎么让程序使用到这个SD卡上的db文件呢?如果无从下手,那就看看默认的getReadableDatabase和getWritableDatabase方法是怎么创建数据库的
不管是getReadableDatabase还是getWritableDatabase方法,都没做啥处理,将逻辑全部转到了getDatabaseLocked方法,那就来看看这个方法(API24)
private SQLiteDatabase getDatabaseLocked(boolean writable) {
......
SQLiteDatabase db = mDatabase;
try {
mIsInitializing = true;
if (db != null) {
if (writable && db.isReadOnly()) {
db.reopenReadWrite();
}
} else if (mName == null) {
//如果传入的数据库名是null,那就使用默认路径创建数据库
db = SQLiteDatabase.create(null);
} else {
try {
if (DEBUG_STRICT_READONLY && !writable) {
final String path = mContext.getDatabasePath(mName).getPath();
db = SQLiteDatabase.openDatabase(path, mFactory,
SQLiteDatabase.OPEN_READONLY, mErrorHandler);
} else {
//如果名称不为null,那就构建data/data/包名/databases/mName路径
db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
mFactory, mErrorHandler);
}
} catch (SQLiteException ex) {
if (writable) {
throw ex;
}
final String path = mContext.getDatabasePath(mName).getPath();
db = SQLiteDatabase.openDatabase(path, mFactory,
SQLiteDatabase.OPEN_READONLY, mErrorHandler);
}
}
onConfigure(db);
//这里根据版本号判断是否需要数据库升级还是降级
final int version = db.getVersion();
if (version != mNewVersion) {
if (db.isReadOnly()) {
throw new SQLiteException("Can't upgrade read-only database from version " +
db.getVersion() + " to " + mNewVersion + ": " + mName);
}
db.beginTransaction();
try {
if (version == 0) {
onCreate(db);
} else {
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
onOpen(db);
if (db.isReadOnly()) {
Log.w(TAG, "Opened " + mName + " in read-only mode");
}
mDatabase = db;
return db;
} finally {
mIsInitializing = false;
if (db != null && db != mDatabase) {
db.close();
}
}
}
这里可以看到一句很重要的话就是当mName不为null的时候,调用
mContext.openOrCreateDatabase
这里的mContext是我们在构造SQLiteOpenHelper传进来的,它是Activity的上下文,指的是ContextWrapper,具体实现是ContextImpl(具体可以参考博主的从Activity加载源码深入理解ActivityThrad的工作逻辑),进到这个类看看
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) {
return openOrCreateDatabase(name, mode, factory, null);
}
@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;
}
这里表明默认情况下数据库文件是通过ContextWrapper的实现类创建的,并且是存放在内置存储中,路径data/data/包名/databases/mName
那解决方法也就出来了,我们只要定义一个类,继承ContextWrapper,重写openOrCreateDatabase方法,获取SD卡上的db文件路径,然后再调用SQLiteDatabase类的openOrCreateDatabase方法即可获取SQLiteDatabase实例,然后返回;
这样操作以后,getDatabaseLocked方法中的mContext.openOrCreateDatabase具体实现就是我们编写的这个方法了
/**
* @Description TODO(加载SD卡上的db)
* @author cxy
* @Date 2018/11/5 17:04
*/
public class DataBaseContext extends ContextWrapper{
public DataBaseContext(Context base) {
super(base);
}
/**
* 重写这个方法,加载SD卡的数据库
* @param name
* @param mode
* @param factory
* @param errorHandler
* @return
*/
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {
String dbPath = FileStorageTools.getInstance(this).makeFilePath("/Android/data/"+getPackageName()+"/database/"+SQLiteDBHelper.DATABASE_NAME+".db");
File file = new File(dbPath);
if (!file.exists()) throw new NullPointerException("DB File is not exists");
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(dbPath,null);
return database;
}
}
其中的FileStorageTools类可查看博主上一遍文章
接下来只需要在实例化SQLiteDBHelper的时候传递这个自定义的Context对象就可以了
DataBaseContext baseContext = new DataBaseContext(context);
SQLiteDBHelper mHelper = new SQLiteDBHelper(baseContext);
通过SQLiteDatabase 对象的getPath()方法,你就知道你使用的数据库存放在哪了
编写DAO
我们先写个父类
/**
* @Description TODO()
* @author cxy
* @Date 2018/11/5 18:12
*/
public class BaseDao {
protected WeakReference<DataBaseContext> mContext;
protected SQLiteDBHelper mHelper;
public BaseDao(Context context) {
mContext = new WeakReference<>(new DataBaseContext(context));
mHelper = new SQLiteDBHelper(baseContext);
}
}
我们前面创建了一个user表,然后再编写对应的dao
/**
* @Description TODO(提供对user的操作)
* @author cxy
* @Date 2018/11/5 14:50
*/
public class UserDao extends BaseDao{
private String TAG = UserDao.class.getSimpleName();
public UserDao(Context context) {
super(context);
}
public void addUser(User user){
SQLiteDatabase database = mHelper.getWritableDatabase();
String sql = "insert into " + SQLiteDBHelper.TABLE_USER + "(uid,name,sex,role) values(?,?,?,?)";
database.execSQL(sql,new String[]{user.getUid(),user.getName(),user.getSex(),user.getRole()});
database.close();
}
/**
*
* @param user
* @return 插入的新纪录的行号,即是第多少行
*/
public long addUserValues(User user){
SQLiteDatabase database = mHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("uid",user.getUid());
values.put("name",user.getName());
values.put("sex",user.getSex());
values.put("role",user.getRole());
long rowNum = database.insert(SQLiteDBHelper.TABLE_USER,null,values);
database.close();
return rowNum;
}
public void updateUser(String uid,String name,String sex){
SQLiteDatabase database = mHelper.getWritableDatabase();
String sql = "update " + SQLiteDBHelper.TABLE_USER + " set name = ? where uid = ? and sex = ?";
database.execSQL(sql,new String[]{name,uid,sex});
database.close();
}
public void updateMoreUser(String uid,String uid2,String uid3,String name){
SQLiteDatabase database = mHelper.getWritableDatabase();
String sql = "update " + SQLiteDBHelper.TABLE_USER + " set name = ? where uid in (?,?,?)";
database.execSQL(sql,new String[]{name,uid,uid2,uid3});
database.close();
}
/**
*
* @param sex
* @param name
* @return 受影响的行数总和
*/
public int updateUserValues(String sex,String name){
SQLiteDatabase database = mHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name",name);
int rowNum = database.update(SQLiteDBHelper.TABLE_USER,values,"sex = ?",new String[]{sex});
database.close();
return rowNum;
}
public void delUser(String uid){
SQLiteDatabase database = mHelper.getWritableDatabase();
String sql = "delete from " + SQLiteDBHelper.TABLE_USER + " where uid = ?";
database.execSQL(sql,new String[]{uid});
database.close();
}
public void delAllUser(){
SQLiteDatabase database = mHelper.getWritableDatabase();
String sql = "delete from " + SQLiteDBHelper.TABLE_USER ;
database.execSQL(sql);
database.close();
}
/**
*
* @param sex
* @return 受影响的行数总和
*/
public int delUserValues(String sex){
SQLiteDatabase database = mHelper.getWritableDatabase();
int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,"sex = ?",new String[]{sex});
database.close();
return rowNum;
}
/**
*
* @return 受影响的行数总和
*/
public int delALLUserValues(){
SQLiteDatabase database = mHelper.getWritableDatabase();
int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,null,null);
database.close();
return rowNum;
}
public User getUser(String uid){
User user = new User();
SQLiteDatabase database = mHelper.getWritableDatabase();
String sql = "select * from "+ SQLiteDBHelper.TABLE_USER + " where uid = ?";
Cursor cursor = database.rawQuery(sql,new String[]{uid});
while (cursor.moveToNext()) {
user.setUid(cursor.getInt(cursor.getColumnIndex("uid"))+"");
user.setName(cursor.getString(cursor.getColumnIndex("name")));
user.setSex(cursor.getString(cursor.getColumnIndex("sex")));
user.setRole(cursor.getString(cursor.getColumnIndex("role")));
}
cursor.close();
database.close();
return user;
}
}
/**
* @Description TODO()
* @author cxy
* @Date 2018/11/5 14:53
*/
public class User {
private String uid;
private String name;
private String sex;
private String role;
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "User{" +
"uid='" + uid + '\'' +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", role='" + role + '\'' +
'}';
}
}
在应用启动的时候要记得将db文件拷贝到SD卡
private void moveDBFile(){
boolean isDbFileExist = true;
File dbFile;
String path = "/Android/data/"+getPackageName()+"/database/";
File parent = new File(FileStorageTools.getInstance(this).makeFilePath(path));
if (!parent.exists()) {
parent.mkdirs();
isDbFileExist = false;
dbFile = FileStorageTools.getInstance(this).makeFile(parent, SQLiteDBHelper.DATABASE_NAME+".db");
} else {
dbFile = FileStorageTools.getInstance(this).makeFile(parent, SQLiteDBHelper.DATABASE_NAME+".db");
if (!dbFile.exists()) {
isDbFileExist = false;
}
}
if (!isDbFileExist) {
FileStorageTools.getInstance(this).moveResourceFileToExternalStorage(dbFile,FileStorageTools.getInstance(this).getStreamFromRaw(R.raw.mango));
}
}
我这里简单写了几个数据库操作的方法,数据库操作无外乎增删改查,这些操作SQLiteDatabase 都提供了api供我们使用,我们重点要掌握它的execSQL()和rawQuery()方法; execSQL()方法可以执行insert、delete、update和CREATE TABLE等有更改行为的SQL语句; rawQuery()方法用于执行select语句
假如你不会编写Sql语句,不用担心,Android很贴心的, SQLiteDatabase专门提供了对应于添加、删除、更新、查询的操作方法: insert()、delete()、update()和query();你只需要传递相应的参数就能完成数据库操作,有没有很激动;对于熟悉SQL语法的程序员而言,直接使用execSQL()和rawQuery()方法执行SQL语句其实更方便
执行SQL语句的方法有了,那就要学会编写相应的SQL语句了
插入操作
- 它的SQL语句格式:insert into 表名(字段列表) values(值列表)
举例:
insert into user(uid,name,sex,role) values("1","tom","1","2")
这种写法就是一个字段对应一个值,但是实际上这种写法不利于开发效率,当字段多的时候,你需要对语句进行拼接,很容易出现错误,而且使得语句变得非常长,不利于维护;同时,这些值有可能是用户输入的,里面可能会包含特殊符号,比如单引号,&类似的符号,为了保证语句正确执行,你必须对每个字段进行转义,对每条语句都这样操作很麻烦
针对这种问题,SQLiteDatabase类提供了一个重载后的execSQL(String sql, Object[] bindArgs)方法,使用这个方法可以解决前面提到的问题,因为这个方法支持使用占位符参数(?)
我们就可以这样写了
insert into user(uid,name,sex,role) values(?,?,?,?)
SQLiteDatabase.execSQL(sql,new String[]{user.getUid(),user.getName(),user.getSex(),user.getRole()});
- 另一种非SQL语句写法
ContentValues values = new ContentValues();
values.put("uid",user.getUid());
values.put("name",user.getName());
values.put("sex",user.getSex());
values.put("role",user.getRole());
database.insert(SQLiteDBHelper.TABLE_USER,null,values);
insert方法需要三个参数
- 第一个参数:要插入数据的表的名称
- 第二个参数:指定空值字段的名称;当第三个参数为空或者里面没有内容的时候,我们insert是会失败的(底层数据库不允许插入一个空行),构建成的sql语句:insert into user() values(),显然这不满足标准SQL的语法;为了防止这种情况,我们要在这里指定一个列名,到时候如果发现第三个参数为null或者其元素个数为0,就会将你指定的这个列名的值设为null,然后再向数据库中插入,这时候构建成的语句:insert into user(name) values(NULL)
- 第三个参数:一个ContentValues对象,类似一个map,通过键值对的形式存储值,key为列名,values为值
第三个参数如果不为null且元素个数不为0,也就是通过putXXX方法存储了值,那第二个参数就可以为null
更新操作
- 它的SQL语句格式:update 表名 set 字段名=值 where 条件子句
举例:
update user set name = ? where uid = ?
database.execSQL(sql,new String[]{name,uid});//执行
这就是将指定uid的一行记录中的name字段更新成指定值
这里条件表达式可以用and或者or来表示
//这个表示两个条件必须都满足才更新行记录
String sql = "update user set name = ? where uid = ? and sex = ?";
database.execSQL(sql,new String[]{name,uid,sex});
//只要有一个条件满足就更新行记录
String sql = "update user set name = ? where uid = ? or sex = ?";
database.execSQL(sql,new String[]{name,uid,sex});
有时候条件表达式中一个字段有多个值范围的,比如你要更新uid等于1,2,3,4的多个行记录,这时候要用到 in
String sql = "update user set name = ? where uid in (?,?,?)";
database.execSQL(sql,new String[]{name,uid,uid2,uid3});
- 非SQL语句写法
public int updateUserValues(String uid,String name){
SQLiteDatabase database = mHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name",name);
int rowNum = database.update(SQLiteDBHelper.TABLE_USER,values,"sex = ?",new String[]{sex});
database.close();
return rowNum;
}
这里通过调用SQLiteDatabase 的update方法
- 第一个参数是表名
- 第二个参数是要修改的列名对应的值(也就是每行记录中字段对应的值)
- 第三个参数就是判断条件(把SQL语句中的where后面的拿过来就可以了)
- 第四个参数是条件对应的值
删除操作
- 它的SQL语句格式:delete from 表名 where 条件子句
举例:
String sql = "delete from user where uid = ?";
database.execSQL(sql,new String[]{uid});
条件表达式同更新操作中一样
如果要删除所有数据,去掉条件表达式即可
String sql = "delete from user " ;
database.execSQL(sql);
- 非SQL语句写法
public int delUserValues(String uid){
SQLiteDatabase database = mHelper.getWritableDatabase();
int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,"uid = ?",new String[]{uid});
database.close();
return rowNum;
}
delete方法参数与更新操作中一样
如果要删除全部数据,第二个参数和第三个参数传入null
public int delALLUser(){
SQLiteDatabase database = mHelper.getWritableDatabase();
int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,null,null);
database.close();
return rowNum;
}
注意:update和delete方法的返回值表明影响的行数总和,insert方法返回值是新插入行的行ID,如果发生错误,则为-1
查询操作
毫无疑问,查询操作应该是用的最频繁的也是最复杂多变的一个操作了,SQLiteDatabase给我们提供了query和rawQuery两类查询方法,它们都会返回一个Cursor对象,Cursor是一个游标接口,代表数据集的游标,提供了遍历查询结果的方法,它的API如下
Cursor API
- int getCount() :获得查询后的数据总条数
- boolean isFirst():光标是否指向第一个条目
- boolean isLast():光标是否指向最后一个条目
- boolean isBeforeFirst():光标是否指向第一行之前的位置
- boolean isAfterLast():光标是否指向最后一行之后的位置
- boolean isNull(int columnIndex):指定列数据是否为null
- boolean move(int offset):将光标从当前位置向前或向后移动相对量。 正偏移向前移动,负偏移向后移动。 如果最终位置超出结果集的边界,则结果位置将固定为-1或count();返回值是是否移动成功
- boolean moveToPosition(int position):将光标移动到绝对位置。 有效值范围是-1 <= position <= count;返回值是是否移动成功
- boolean moveToFirst():将光标移动到第一行,返回值是是否移动成功
- boolean moveToLast():将光标移动到最后一行,返回值是是否移动成功
- boolean moveToNext():将光标移动到下一行,返回值是是否移动成功
- boolean moveToPrevious():将光标移动到上一行,返回值是是否移动成功
- int getColumnIndex(String columnName):返回给定列名称的从零开始的索引,如果该列不存在,则返回-1
- int getInt(int columnIndex):返回当前行指定列的值
- long getLong(int columnIndex):同上
- float getFloat(int columnIndex):同上
- String getString(int columnIndex):同上
查询方法
再看几个查询方法
- public Cursor rawQuery(String sql, String[] selectionArgs)
这第一个参数是执行查询的SQL语句,第二个是语句中对应占位符的值
-
public Cursor query(String table, String[] columns, String selection,String[] selectionArgs, String groupBy, String having, String orderBy)
- table:表名
- columns:需要返回的列,传入null将返回所有列(尽量不要传入null,将会从消耗多余的性能)
- selection:查询条件子句,相当于select语句where关键字后面的部分,在条件子句允许使用占位符“?”
- selectionArgs:对应于selection语句中占位符的值,值在数组中的位置与占位符在语句中的位置必须一致,否则就会有异常
- groupBy:用于设定返回行的分组方式,传递null表示返回的行不会被分组;相当于select语句group by关键字后面的部分
- having:决定哪一行被放到Cursor中的过滤器,相当于select语句having关键字后面的部分;传递null会导致所有的行都包含在内,前提是groupBy属性也设置为null
- orderBy:用于设定行的排列方式(依据列值),相当于select语句order by关键字后面的部分;传递null表示使用默认的排序方式,可能是无序排列
-
public Cursor query(String table, String[] columns, String selection,String[] selectionArgs, String groupBy, String having,
String orderBy, String limit)这里比上面方法多了一个参数limit,设置query语句返回行的数量,执行分页功能,相当于select语句limit关键字后面的部分;传递null表示没有设置limit语句;注意格式为String,传递的时候需要传递数字字符串,例如“10”
-
public Cursor query(boolean distinct, String table, String[] columns,String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit)这个方法比上面的又多了一个参数distinct,是用来去重的,设置为true,每一行的数据必须唯一
查询样例
它的SQL语句格式:select column1, column2,…columnN from 表名 where 条件子句 group by 分组字句 having … order by 排序子句 (顺序一定不能错)
举例:
- where
//查询user表所有记录
select * from user
//查询uid=2的记录
select * from user where uid = 2
//查询uid大于2的记录
select * from user where uid > 2
//查询uid等于2或者3的记录
select * from user where uid in (2,3)
//查询uid既不是 2也不是 3的记录
select * from user where uid not in (2,3)
//查询uid在2和5之间的记录
select * from user where uid between 2 and 5
//查询uid等于2且sex=1的记录
select * from user where uid =2 and sex = 1
//查询uid等于2或者且sex=1的记录
select * from user where uid =2 or sex = 1
//查询name以jack开头的记录
select * from user where name like 'jack%'
//查询name以jack结尾的记录
select * from user where name like '%jack'
//查询name包含jack的记录
select * from user where name like '%jack%'
- limit
//查询user表前10条记录
select * from user limit 10
//查询user表从第10条开始的后10条记录
select * from user limit 10 offset 10
- group by
假如表里很多条记录,记录着每个人每个月的收入,这里就会存在多条记录是同一个人的
然后我要查出来每个人总共收入是多少,这时候可以用到group by了,将同一个人的多条记录合并成一条记录,会让其中的收入字段值累加,从而的得到每个人的总收入,表里现有记录如下
uid | name | income |
---|---|---|
1 | jack | 1000 |
2 | jack | 1000 |
3 | tom | 500 |
4 | aliex | 500 |
5 | tom | 1000 |
6 | jack | 2000 |
select name,income from user group by name
执行这个语句后,查出来的记录如下
name | income |
---|---|
jack | 4000 |
tom | 1500 |
aliex | 500 |
- order by
select name,income from user group by name order by income asc
这个语句执行的结果就是在上面的基础上将记录按照income字段升序排列,得到如下结果
name | income |
---|---|
aliex | 500 |
tom | 1500 |
jack | 4000 |
还可以用 order by income desc,这个就是降序排列
- having
having 子句在由 group by 子句创建的分组上设置条件,在一个查询中,having 子句必须放在 group by 子句之后,必须放在 order by 子句之前
比如现在有这样一个例子:
有三个人,他们中了很多次奖,中奖记录存放在user表中,现在老板想看下每个人总共中了多少奖,也就是拿了多少钱,同时对中奖人的条件是中奖次数要大于1次,也就是说只中奖一次的人就不要给我看了
先看看原始数据
uid | name | income |
---|---|---|
1 | jack | 1000 |
2 | jack | 1000 |
3 | tom | 500 |
4 | aliex | 500 |
5 | tom | 1000 |
6 | jack | 2000 |
执行下面这个语句
select name,income from user group by name having count(name) > 1
这里的count是计数功能,count(name) > 1表示每个分组中的分组标志name要出现一次以上,得到如下结果
name | income |
---|---|
jack | 4000 |
tom | 1500 |
因为name=aliex的分组中name只出现了一次,所以被排除了
事务(Transaction)
事务是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行;使用事务的好处是可以保证数据的一致性和完整性(避免异常和错误等导致的数据信息异常) ;比如我要批量的插入一批数据,要么全插入成功,要么一条也别插入
事务(Transaction)具有以下四个标准属性,通常根据首字母缩写为 ACID
- 原子性(Atomicity):确保工作单位内的所有操作都成功完成,否则,事务会在出现故障时终止,之前的操作也会回滚到以前的状态
- 一致性(Consistency):数据库从一个一致性状态变到另一个一致性状态(一系列操作后,所有的操作和更新全部提交成功,数据库只包含全部成功后的数据就是数据的一致性;相反,由于系统异常或数据库出现故障导致只有部分数据更新成功,这不是我们需要的最终数据,这就是数据的不一致)
- 隔离性(Isolation):使事务操作相互独立和透明(事务内部操作的数据对其它事务是隔离的,在一个事务执行完之前不会被其他事务影响和操作)
- 持久性(Durability):确保已提交事务的结果或效果在系统发生故障的情况下仍然存在
事务的使用:
主要四步:beginTransaction -> sql操作 -> setTransactionSuccessful -> endTransaction
//往数据库批量插入记录
public boolean batchInsertBySql(List<String> list) {
SQLiteDatabase db = null;
try {
db = sqlConnection.getWritableDatabase();
db.beginTransaction();//开始事务
for (String str : list) {
StringBuffer sql = new StringBuffer(200);
sql.append("insert into terminal_deviceinfo values(")
.append(str)
.append(");");
db.execSQL(sql.toString());
}
db.setTransactionSuccessful();//设置事务的标志为成功,这样执行到endTransaction() 时提交当前事务,如果不调用此方法会回滚事务
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
try {
if (null != db) {
db.endTransaction();//由事务的标志决定是提交事务,还是回滚事务
db.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
SQL注入
SQL注入是比较常见的网络攻击方式,注入通常在请求用户输入时发生,比如需要用户输入姓名,但用户却输入了一个 SQL 语句,这个语句能做的事太多了,非常严重的是删除数据库了
案列
- 案例一
比如用户登录,需要输入用户名,然后用户输入了
tom'; delete from user;
此时一个疏忽的程序猿去判断表里有没有这个人,通过如下语句
select * from user where name='{$name}'
那这个语句最后就是
select * from user where name='tom'; delete from user
像这种堆叠查询语句很明显会对数据库造成非常大的损失
- 案例2
在登录界面,需要用户输入用户名和密码,然后用户输入的用户名如下
‘or 1 = 1 --
这时候又一个疏忽的程序猿这样去数据库判断用户存不存在
String sql = "select * from user where name=' "+userName+" ' and password=' "+password+" ' "
这个语句接上上面用户的输入内容
select * from user where name= '‘ or 1 = 1 -- and password = ' '
where后面接了两个条件,name= '‘ 或者 1 = 1,那这必然成立的,且后面接了 – 表示后面判断password 的语句就变成了注释,就没有作用了;那这样用户就随便登录了
防止SQL注入
编写SQL语句的时候切记不要直接使用字符串拼接的方式,使用参数化SQL,可以使用 ? 这个占位符填充参数值的位置;Google在Sqlite里弄了很多重载的函数,像上面提到的几种查询方法,就是参数化SQL的效果,参数化处理就是为了防止SQL注入
对用户输入的内容进行字符串过滤,判断输入内容是否包含一些特殊符号和SQL关键字
总的说来,防范一般的SQL注入只要在代码规范上下点功夫就可以了