If you do something and it turns out pretty good, then you should go do something else wonderful, not dwell on it for too long. Just figure out what’s next.你如果出色地完成了某件事,那你应该再做一些其他的精彩事儿。不要在前一件事上徘徊太久,想想接下来该做什么。——乔布斯
小弟初学安卓,该文算是小弟的学习过程,课后笔记与一些自己的思考,希望在自己的自学路上留下印记,也许有一些自己想的不对的地方,希望各位前辈斧正。
小弟之前没完全搞清单例模式,结果跑了火车,赶紧改正,向不幸看到小弟说瞎话的同志们道歉!
预备知识点
1.什么是SQLite
“SQL”就已经暴露出它与数据库的联系,没错,SQLite是一个轻量的关系型数据库。它已内置于Android系统中,我们无需进行安装,在编写程序要用到时,直接用代码创建即可,非常方便,而且效率很高,占用资源非常少,速度也很快。SQLite相比一般得数据库使用更简单,它在创建表字段时,可以不指定表字段类型。
2.准备创建SQLite数据库
要想方便的创建数据库,需要用到Android为我们提供的一个帮助类——SQLiteOpenHelper(SQLite开启助手?)这是一个抽象类,它其中有两个重要的抽象方法,onCreate()和onUpgrade(),我们就是利用它们来对数据库进行创建和升级(按我们的需要操作)。因此我们需要自建一个类来继承SQLiteOpenHelper类,并重写这两个方法。另外还必须要重写SQLiteOpenHelper的构造方法,它有两个构造方法可供选择:
1.
public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
2.
public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,
DatabaseErrorHandler errorHandler) {
if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
mContext = context;
mName = name;
mFactory = factory;
mNewVersion = version;
mErrorHandler = errorHandler;
}
一般选择第一个构造方法,它的第二个参数name就是指的要创建的数据库的名字(注意,该名字需要有扩展名,例如“database.db”),第三个参数一般写null,除非你想在查询时返回的是自己定义的cursor(游标),否则就传入null来使用默认的cursor,何为cursor,我们待会再来讲解(实际上小弟还未学习如何自定义cursor,未来补上)。第四个参数顾名思义,version(版本号),这是个整型值,一般写1(当然你写2写100都行)。
第二个构造方法多了一个参数,DatabaseErrorHandler 对象,小弟学艺不精,未能找到详细资料,但从名字和代码看,该构造方法与数据库异常有关,而且我们看到,这个代码中要求version(版本号)必须大于或等于1,不然就会抛出异常。
详细代码操作请往下看。
3.Cursor是啥
这个词直译是“游标”,它对于查询数据非常重要,小弟可能说的有些不对,不过按我个人的理解,它就类似于下图这种感觉(用Excel演示的):
大家看这个绿色的框每次移动都只是罩住了一行的表格(记录),cursor就像这样,在数据库的表中各行游走(受控制),将上面图当作数据库表,当绿色框罩住第二行(如同cursor处在数据库表的第0行),此时,这个cursor对象就能获取这一行的日期、学习内容信息,若字段类型为字符串则使用cursor.getString()来获取数据,当然也能获取别的类型数据。而当我们要获取下一行记录时,就必须把cursor移动到下一行(下一个position),那么移动cursor有这些方法来操作:
- cursor.moveToFirst() //将cursor移动到首位置(position==0,第一条记录),且该方法会返回一个boolean值,若查询结果为空则返回false,可用做判断查询结果。需要注意的是,使用cursor时,如果未进行过移动操作,cursor默认处于-1的位置。
- cursor.moveToNext() //移动到下一位置,同样会返回一个boolean值,如果cursor已经超过了最后一条记录的位置则返回false。
- cursor.moveToLast() //将cursor移动到最底位置,同样有返回值,若查询结果为空则返回false。
- cursor.moveToPrevious() //与moveToNext()相反,向上移动
- cursor.moveToPosition() //移动到一个指定位置。同样会返回boolean值,如果指定位置存在则为true,否则为false。
- ……
就着代码继续讲
0.*自定义Schema类
这一步是我在《Android编程权威指南(第二版)》中学到的,不是必须的,但觉得很有道理。首先,我们现在项目包下新建一个包database(database相关.java文件都应该放入其中,如DataBaseHelper),然后建立schema的java类,它是做什么用的呢?首先展示代码:
package com.qdf.test.database;
public class BookDbSchema {
public static final class BookTable {
public static final String NAME = "books";
public static final class Cols {
public static final String BOOK_NAME = "book_name";
public static final String PRICE = "price";
}
}
}
这段代码不长,首先说明,我待会将要在数据库中创建一个名叫books的表,那么大家根据代码可以看出来了,我在这个类中创建了一个内部类BooksTable,在其中创建了静态字符串常量NAME即“表名”,又在其中创建了一个内部类Cols(列),其中也都是静态字符串常量,可以看出这些是我定义的数据表字段。这是一种模式,这种做法结构十分清晰,我们可以在其它类中引用,并且很方便的修改和新增元素。当然如果我们还要新建表,还可在Schema下再创建内部类。至于至于为什么要叫Schema,有兴趣的朋友可以网络搜索下关键字“SQL Schema”,Schema对于SQL有“架构”的意思,小弟才疏学浅,这里只是抛砖引玉。
1.自定义BookDataBaseHelper(extends SQLiteOpenHelper )类
package com.qdf.test.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.qdf.test.database.BookDbSchema.BookTable;
public class BookDataBaseHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "book.db";
public static final int VERSION = 1;
private SQLiteDatabase mDB;
public BookDataBaseHelper(Context context) {
//修改了一下构造方法需要的参数,直接向父类方法中传入了值
super(context, DATABASE_NAME, null, VERSION);
}
/**
* 单例模式获取数据库对象
*/
public SQLiteDatabase getDbInstance() {
if (mDB== null) {
mDB= this.getWritableDatabase();
}
return mDB;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
//建表,书名类型为varchar(20),价格为浮点型
sqLiteDatabase.execSQL("create table " + BookTable.NAME + "(" + BookTable.Cols.BOOK_NAME + " varchar(20) not null," + BookTable.Cols.PRICE + " float not null)");
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
//在这里做升级操作
}
}
首先大家可能看到构造方法和我之前说的不一样,参数只剩下了Context context,因为我直接向父类方法传入了context以外的值,这样别的类创建BookDataBaseHelper 对象的时候就只需要传入context就行了,当然你也可以保持原有的构造方法,只要在创建对象时传入相应参数就行。
第二个要介绍下之前说过的从SQLiteOpenHelper继承的两个方法onCreate()与onUpgrade(),让我们来重写它们。先说onCreate(),我们一般用它来初始化数据库,它是什么时候执行的呢,当我们要操作数据库时,必须先创建我们自定义的帮助类(BookDataBaseHelper)的实例(注意根据所选构造方法传参)。然而这时,onCreate()方法还未执行,因为我们还需要创建一个SQLiteDatabase的实例,并通过我们的帮助类实例来获取
SQLiteDatabase的实例,也就是:
SQLiteDatabase mSqLiteDatabase=mBookDataBaseHelper.getReadableDatabase()
实际上,有两个方法可以获取SQLiteDatabase 实例,一个是上面的 getReadableDatabase(),一个是getWritableDatabase(),这两个方法被调用时,当数据已经创建时则读取数据库,若数据库不存在则创建,然后返回SQLiteDatabase 对象(可对数据库进行读写),但是当磁盘满了无法再写入数据时,getReadableDatabase()返回的对象会以只读的形式打开数据库,getWritableDatabase()则会异常(GG)。当我们通过这个方法获取到SQLiteDatabase 对象时,帮助类的onCreate()方法就执行了,这时候数据库文件以及其内的表都被创建了(数据库名就是我们在构造方法中传入的字符串)。其路径为/data/data/package_name/databases。
根据我上面的onCreate()方法中的内容,可以看到我创建了一个books表,该方法的参数sqLiteDatabase应该就是在我们获取SQLiteDatabase 对象时得到的。至于onUpgrade()方法,当我们想升级版本时,你只需改一下静态常量VERSION的数字,比如2,这时候我们再运行程序的时候,onUpgrade()方法也就执行了,你可以完成你想要的操作,且其参数中有两个整型值,int oldVersion, int newVersion,记录了旧版本号和新版本号。(另外细心的你通过放置Log可以发现你第二次运行代码时,onCreate()就没执行了,即使你写了新代码,感觉突然悟出为什么要有onUpgrade()了)
第三个要提一下一个从同学(宣传宣传他的简书:Raymond_qi_kang的简书)那知道的一个获取SQLiteDatabase 实例的方法,就是上面代码中的getDbInstance(),返回值属性为SQLiteDatabase 。这样做的话,之前我们说的获取SQLiteDatabase 实例可以写成:
mSqLiteDatabase = mBookDataBaseHelper.getDbInstance();
这样就不用每次获取实例时再getReadableDatabase()或者getWritableDatabase()了。
2.来!增删改查!
说起来小弟刚知道对数据库增删改查是交CRUD操作,好酷炫。事先要说的是,对SQLite数据库增删改查,SQLiteDatabase 拥有自己的方法来辅助我们方便的操作,但是也许会有对SQL比较熟练的朋友更习惯用SQL语句来操作,可喜的是Android是支持我们使用SQL语句的,比如之前的帮助类中onCreate()方法中创建表时我们就是使用的SQL语句,当你想要使用SQL语句操作时只需像上面一样使用execSQL(执行SQL):
sqLiteDatabase.execSQL(SQL语句); //SQL语句为字符串
我们接下来要将的CRUD操作主要讲讲利用Android提供的辅助方法,利用SQLiteDatabase 对象来操作。(虽然我也更喜欢直接使用SQL语句)
我创建了一个Activity,布局中有四个按钮,分别是ADD、DELETE、QUERY、UPDATE,为它们添加了点击事件监听器,非常简单就不贴出来了,分段讲解,最后再奉上自己的一个小DEMO。
⑴增
说是增加数据其实就是往现有的数据表中插入一条或多条记录。先来看看这个方法的参数。
insert(String table, String nullColumnHack, ContentValues values)
第一个参数为我们要插入数据的表名,第二个是给未添加数据的可为空的字段自动赋值为NULL,我们一般填null。第三个参数是这个方法的点睛之比,之后的update也会用到,这是个ContentValues类型的参数。
ContentValues的作用是通过重载它的put()方法来存储键值对,我们只要与表的字段相对应的添加键值对就好(小弟语言组织能力太好,见谅),代码如下:
点击ADD按钮时进行的操作:
//插入一条数据
ContentValues contentValues = new ContentValues();
contentValues.put(BookTable.Cols.BOOK_NAME, "《朝花夕拾》");
contentValues.put(BookTable.Cols.PRICE, 20.8);
mSqLiteDatabase.insert(BookTable.NAME, null, contentValues);
contentValues.clear();//若还要接着插入第二条数据真,则要清空contentValues
//接着插入数据
contentValues.put(BookTable.Cols.BOOK_NAME, "《水浒传》");
contentValues.put(BookTable.Cols.PRICE, 59.9);
mSqLiteDatabase.insert(BookTable.NAME, null, contentValues);
//SQL语句操作
mSqLiteDatabase.execSQL("insert into " +
BookTable.NAME + "(" +
BookTable.Cols.BOOK_NAME + "," +
BookTable.Cols.PRICE + ")" +
" values('《三重门》',22.5)");
这段代码首先实例花了ContentValues,然后我们开始用put传入键值对,put中的KEY直接引用了我们之前创建的BookDbSchema类中内部类BookTable的静态常量,是不是让人看了一目了然?同时还展示了用SQL语句操作。记住连续插入数据时要记得在第一次插入后clear哦。最终结果(点开先前说的路径中的.db文件选择相应数据表可查看):
插入成功!
⑵.删
删除则是使用SQLiteDatabase 的delete方法,先让我们看它的参数:
delete(String table, String whereClause, String[] whereArgs)
它的第一个参数也是表名,第二、第三个参数是条件,如果为null的话,则会默认删除所有行的数据。小弟我的理解呢whereClause是条件的一个大致框架,他表示我们将删除某条数据,而这条数据中的某字段“>”、“<”,“>=”,”<=”或“=”某个值,而这个值则在whereArgs这个数组中,为什么是数组呢,这是为了应对有多条件的情况:
DELETE按钮操作:
String whereClause_delete = BookTable.Cols.BOOK_NAME + "=? and " + BookTable.Cols.PRICE + "=?";
String whereArgs_delete[] = {"《三重门》", "22.5"};
mSqLiteDatabase.delete(BookTable.NAME, whereClause_delete, whereArgs_delete);
//使用SQL操作
mSqLiteDatabase.execSQL("delete from " + BookTable.NAME + " where " +
BookTable.Cols.BOOK_NAME + "='《水浒传》' and " +
BookTable.Cols.PRICE + "=59.9");
//
mSqLiteDatabase.execSQL("delete from " + BookTable.NAME + " where " + BookTable.Cols.BOOK_NAME + "=?", new String[]{"《朝花夕拾》"});
可以看到,我设置了两个条件就是书名为《水浒传》并且价格为59.9才删除,而whereClause 中的?为占位符,其值为whereArgs数组中的元素,因为我设置了两个条件,所以数组中有两个元素分别为两个?的值。
提到这个”?“,还要说说execSQL 的重载方法:
execSQL(String sql, Object[] bindArgs)
实际使用是这样:
mSqLiteDatabase.execSQL("delete from " + BookTable.NAME + " where " + BookTable.Cols.BOOK_NAME + "=?", new String[]{"《朝花夕拾》"});
⑶ 改
改在SQL中就是更新-Update,同样需要用到ContentValues,来为你要更新的行重新传值,
UPDATE按钮操作:
ContentValues contentValues_update = new ContentValues();
contentValues_update.put(BookTable.Cols.PRICE, 29.9);
String whereClause_update = BookTable.Cols.BOOK_NAME + "=?";
String whereArgs_update[] = {"《朝花夕拾》"};
mSqLiteDatabase.update(BookTable.NAME, contentValues_update, whereClause_update, whereArgs_update);
//SQL操作
mSqLiteDatabase.execSQL("update " + BookTable.NAME +
" set " + BookTable.Cols.PRICE + " =99.9 where " +
BookTable.Cols.BOOK_NAME + "='《水浒传》'");
大家可以看出,我更改了《朝花夕拾》和《水浒传》的价格分别改成了29.9和99.9。
⑷查
首先我们看看SQLiteDatabase 提供的query方法的四种可传参数选择:
boolean distinct, String table, String[] columns,String selection, String[] selectionArgs, String groupBy,String having, String orderBy, String limit
boolean distinct, String table, String[] columns,String selection, String[] selectionArgs, String groupBy,String having, String orderBy, String limit, CancellationSignal cancellationSignal
String table, String[] columns, String selection,String[] selectionArgs, String groupBy, String having,String orderBy
String table, String[] columns, String selection,String[] selectionArgs, String groupBy, String having,String orderBy, String limit
相信熟悉SQL对其中一些参数名应该不会陌生,简单提一提:
- boolean distinct:查询结果是否消除重复值 true/false
- String[] columns:要查询的列,数组内元素为字段名
- String selection:where的约束条件
- String[] selectionArgs:where中占位符的值
- String groupBy:对查询结果进行分组
- String having:分组过滤条件,必须跟groupBy一起用
- String orderBy:根据传入参数(字段)进行排序,例如:BookTable.Cols.PRICE + ” DESC”/”ASC”——根据书的价格(降序/升序)排序
- String limit:限制查询结果返回数目,且可以指定第几条到第几条记录
CancellationSignal cancellationSignal:进程中取消操作的信号, 如果操作被取消, 当查询命令执行时会抛出 OperationCanceledException 异常(小弟暂时不会用)
要注意的是这个query方法最后返回给我们的是一个Cursor对象,比如我们要查询整个Books表并且按书的价格降序,那么代码就是:
Cursor cursor = mSqLiteDatabase.query(BookTable.NAME, null, null, null, null, null, BookTable.Cols.PRICE + " DESC", null);
while (cursor.moveToNext()) {
String book_name = cursor.getString(cursor.getColumnIndexOrThrow(BookTable.Cols.BOOK_NAME));
Float price = cursor.getFloat(cursor.getColumnIndexOrThrow(BookTable.Cols.PRICE));
Log.i("query_result", "书名: " + book_name + " 价格: " + price);
}
cursor.close();//游标使用完后要记得close
看看Log输出的结果,
while(cursor.moveToNext())会让游标从-1的position开始不停的往表底移动直到超过表底的position,没移动一次就会扫描一条数据,因为我两个字段的类型分别为字符串和浮点数,所以cursor分别使用了getString()与getFloat()方法来获取字段数据(getFloat()也可以替换为getString()将其转换为字符串),而我们写在内部的cursor.getColumnIndexOrThrow(”字段名”),是其从第一个字段到最后一个字段来寻找我们在括号内指定的字段,返回的是个整型值,如果不存在将抛出IllegalArgumentException 异常,结合在一起才让我们找到了某条数据的某个字段的值。
最后在这里预留一个小弟我还没弄的特明白的知识点:事务,等小弟弄清楚后在来补上。
学习总结:还有很多相关知识大家有兴趣可以看看源码,相信熟悉SQL的人会很轻松的使用。小弟只是抛砖引玉,这里只是简单记录了各方法的使用,实际使用中还需要们灵活的将各方法封装。