4.2 数据库SQLite

本节介绍Android的数据库存储方式—— SQLite的使用方法,包括如何建表和删表、变更表结构以及对表数据进行增加、删除、修改、查询等操作,然后通过SQLite结合“登录App”项目改进记住密码功能。

4.2.1 SQLite的基本用法

SQLite是一个小巧的嵌入式数据库,使用方便、开发简单,手机上最早由iOS运用,后来Android也采用了SQLite。SQLite的多数sql语法与Oracle一样,下面只列出不同的地方:

(1)建表时为避免重复操作,应加上IF NOT EXISTS关键词,例如CREATE TABLE IF NOT EXISTS table_name。

(2)删表时为避免重复操作,应加上IF EXISTS关键词,例如DROP TABLE IF EXISTS table_name。

(3)添加新列时使用ALTER TABLE table_name ADD COLUMN ...,注意比Oracle多了一个COLUMN关键字。

(4)在SQLite中,ALTER语句每次只能添加一列,如果要添加多列,就只能分多次添加。

(5)SQLite支持整型INTEGER、字符串VARCHAR、浮点数FLOAT,但不支持布尔类型。布尔类型数要使用整型保存,如果直接保存布尔数据,在入库时SQLite就会自动将其转为0或1,0表示false,1表示true。

(6)SQLite建表时需要一个唯一标识字段,字段名为_id。每建一张新表都要例行公事加上该字段定义,具体属性定义为_idINTEGER PRIMARY KEYAUTOINCREMENT NOT NULL。

(7)条件语句等号后面的字符串值要用单引号括起来,如果没用使用单引号括起来,在运行时就会报错。

SQLiteDatabase是SQLite的数据库管理类,我们可以在活动页面代码或任何能取到Context的地方获取数据库实例,参考代码如下:

            //创建数据库,如果已存在就打开
            SQLiteDatabase db = getApplicationContext().openOrCreateDatabase("test.db",
    Context.MODE_PRIVATE, null);
            //删除数据库
            getApplicationContext().deleteDatabase("test.db");

SQLiteDatabase提供了若干操作数据表的API,常用的方法有3类,列举如下:

1.管理类,用于数据库层面的操作。

● openDatabase:打开指定路径的数据库。

● isOpen:判断数据库是否已打开。

● close:关闭数据库。

● getVersion:获取数据库的版本号。

● setVersion:设置数据库的版本号。

2.事务类,用于事务层面的操作。

● beginTransaction:开始事务。

● setTransactionSuccessful:设置事务的成功标志。

● endTransaction:结束事务。执行本方法时,系统会判断是否已执行setTransactionSuccessful,如果之前已设置就提交,如果没有设置就回滚。

3.数据处理类,用于数据表层面的操作。

● execSQL:执行拼接好的SQL控制语句。一般用于建表、删表、变更表结构。

● delete:删除符合条件的记录。

● update:更新符合条件的记录。

● insert:插入一条记录。

● query:执行查询操作,返回结果集的游标。

● rawQuery:执行拼接好的SQL查询语句,返回结果集的游标。

4.2.2 SQLiteOpenHelper

SQLiteDatabase存在局限性,例如必须小心、不能重复地打开数据库,处理数据库的升级很不方便。Android提供了一个辅助工具—— SQLiteOpenHelper,用于指导我们进行SQLite的合理使用。

SQLiteOpenHelper的具体使用步骤如下:

步骤01 新建一个继承自SQLiteOpenHelper的数据库操作类,提示重写onCreate和onUpgrade两个方法。其中,onCreate方法只在第一次打开数据库时执行,在此可进行表结构创建的操作;onUpgrade方法在数据库版本升高时执行,因此我们可以在onUpgrade函数内部根据新旧版本号进行表结构变更处理。

步骤02 封装保证数据库安全的必要方法,包括获取单例对象、打开数据库连接、关闭数据库连接。

● 获取单例对象:确保App运行时数据库只被打开一次,避免重复打开引起错误。

● 打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调用SQLiteOpenHelper的getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。

● 关闭数据库连接:数据库操作完毕后,应当调用SQLiteDatabase对象的close方法关闭连接。

步骤03 提供对表记录进行增加、删除、修改、查询的操作方法。

可被SQLite直接使用的数据结构是ContentValues类,类似于映射Map,提供put和get方法用来存取键值对。区别之处在于ContentValues的键只能是字符串,查看ContentValues的源码会发现其内部保存键值对的数据结构就是HashMap“privateHashMap<String, Object>mValues; ”。ContentValues主要用于记录增加和更新操作,即SQLiteDatabase的insert和update方法。

对于查询操作来说,使用的是另一个游标类Cursor。调用SQLiteDatabase的query和rawQuery方法时,返回的都是Cursor对象,因此获取查询结果要根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为3类,说明如下:

1.游标控制类方法,用于指定游标的状态。

● close:关闭游标。

● isClosed:判断游标是否关闭。

● isFirst:判断游标是否在开头。

● isLast:判断游标是否在末尾。

2.游标移动类方法,把游标移动到指定位置。

● moveToFirst:移动游标到开头。

● moveToLast:移动游标到末尾。

● moveToNext:移动游标到下一条记录。

● moveToPrevious:移动游标到上一条记录。

● move:往后移动游标若干条记录。

● moveToPosition:移动游标到指定位置的记录。

3.获取记录类方法,可获取记录的数量、类型以及取值。

● getCount:获取结果记录的数量。

● getInt:获取指定字段的整型值。

● getFloat:获取指定字段的浮点数值。

● getString:获取指定字段的字符串值。

● getType:获取指定字段的字段类型。

鉴于数据库操作的特殊性,不方便单独演示某个功能,接下来从创建数据库开始介绍,完整演示一下数据库的读写操作。如图4-5和图4-6所示,在页面上分别录入两个用户的注册信息并保存到SQLite。从SQLite读取用户注册信息并展示在页面上,如图4-7所示。

图4-5 第一条注册信息保存到数据库

图4-6 第二条注册信息保存到数据库

图4-7 从SQLite中读取两条注册记录

下面是用户注册信息数据库的SQLiteOpenHelper操作类的完整代码:

        public class UserDBHelper extends SQLiteOpenHelper {
            private static final String TAG = "UserDBHelper";
            private static final String DB_NAME = "user.db";
            private static final int DB_VERSION=1;
            private static UserDBHelper mHelper=null;
            private SQLiteDatabase mDB=null;
            private static final String TABLE_NAME="user_info";


            private UserDBHelper(Context context){
                super(context, DB_NAME, null, DB_VERSION);
            }


            private UserDBHelper(Context context, int version){
                super(context, DB_NAME, null, version);
            }


            public static UserDBHelper getInstance(Context context, int version){
                if(version>0&&mHelper==null){
                    mHelper=new UserDBHelper(context, version);
                }else if(mHelper==null){
                    mHelper=new UserDBHelper(context);
                }
                return mHelper;
            }


            public SQLiteDatabase openReadLink(){
                if(mDB==null||mDB.isOpen()! =true){
                    mDB=mHelper.getReadableDatabase();
                }
                return mDB;
            }


            public SQLiteDatabase openWriteLink(){
                if(mDB==null||mDB.isOpen()! =true){
                    mDB=mHelper.getWritableDatabase();
                }
                return mDB;
            }


            public void closeLink(){
                if(mDB! =null&&mDB.isOpen()==true){
                    mDB.close();
                    mDB=null;
                }
            }


            public String getDBName(){
                if(mHelper! =null){
                    return mHelper.getDatabaseName();
                }else{
                    return DB_NAME;
                }
            }


            @Override
            public void onCreate(SQLiteDatabase db){
                Log.d(TAG, "onCreate");
                String drop_sql="DROP TABLE IF EXISTS"+TABLE_NAME+"; ";
                db.execSQL(drop_sql);
                String create_sql="CREATE TABLE IF NOT EXISTS"+TABLE_NAME+"("
                        +"_id INTEGER PRIMARY KEY  AUTOINCREMENT NOT NULL, "
                        +"name VARCHAR NOT NULL, "+"age INTEGER NOT NULL, "
                        +"height LONG NOT NULL, "+"weight FLOAT NOT NULL, "
                        +"married INTEGER NOT NULL, "+"update_time VARCHAR NOT NULL"
                        //演示数据库升级时要先注释下面这行代码
                        +", phone VARCHAR"+", password VARCHAR"+"); ";
                Log.d(TAG, "create_sql:"+create_sql);
                db.execSQL(create_sql);
            }


            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
                Log.d(TAG, "onUpgrade oldVersion="+oldVersion+", newVersion="+newVersion);
                if(newVersion>1){
                    //Android的ALTER命令不支持一次添加多列,只能分多次添加
                    String alter_sql="ALTER TABLE"+TABLE_NAME+"ADD COLUMN"+"phone
    VARCHAR; ";
                    Log.d(TAG, "alter_sql:"+alter_sql);
                    db.execSQL(alter_sql);
                    alter_sql="ALTER TABLE"+TABLE_NAME+"ADD COLUMN"+"password
    VARCHAR; ";
                    Log.d(TAG, "alter_sql:"+alter_sql);
                    db.execSQL(alter_sql);
                }
            }


            public int delete(String condition){
                int count=mDB.delete(TABLE_NAME, condition, null);
                return count;
            }


            public int deleteAll(){
                int count=mDB.delete(TABLE_NAME, "1=1", null);
                return count;
            }


            public long insert(UserInfo info){
                ArrayList<UserInfo>infoArray=new ArrayList<UserInfo>();
                infoArray.add(info);
                return insert(infoArray);
            }


            public long insert(ArrayList<UserInfo>infoArray){
                long result=-1;
                for(int i=0; i<infoArray.size(); i++){
                    UserInfo info=infoArray.get(i);
                    ArrayList<UserInfo>tempArray=new ArrayList<UserInfo>();
                    // 如果存在同名记录,就更新记录。注意条件语句的等号后面要用单引号括起来
                    if(info.name! =null&&info.name.length()>0){
                        String condition=String.format("name='%s'", info.name);
                        tempArray=query(condition);
                        if(tempArray.size()>0){
                            update(info, condition);
                            result=tempArray.get(0).rowid;
                            continue;
                        }
                    }
                    // 如果存在同样的手机号码,就更新记录
                    if(info.phone! =null&&info.phone.length()>0){
                        String condition=String.format("phone='%s'", info.phone);
                        tempArray=query(condition);
                        if(tempArray.size()>0){
                            update(info, condition);
                            result=tempArray.get(0).rowid;
                            continue;
                        }
                    }
                    // 如果不存在唯一性重复的记录,就插入新记录
                    ContentValues cv=new ContentValues();
                    cv.put("name", info.name);
                    cv.put("age", info.age);
                    cv.put("height", info.height);
                    cv.put("weight", info.weight);
                    cv.put("married", info.married);
                    cv.put("update_time", info.update_time);
                    cv.put("phone", info.phone);
                    cv.put("password", info.password);
                    result=mDB.insert(TABLE_NAME, "", cv);
                    // 添加成功后返回行号,失败则返回-1
                    if(result==-1){
                        return result;
                    }
                }
                return result;
            }


            public int update(UserInfo info, String condition){
                ContentValues cv=new ContentValues();
                cv.put("name", info.name);
                cv.put("age", info.age);
                cv.put("height", info.height);
                cv.put("weight", info.weight);
                cv.put("married", info.married);
                cv.put("update_time", info.update_time);
                cv.put("phone", info.phone);
                cv.put("password", info.password);
                int count=mDB.update(TABLE_NAME, cv, condition, null);
                return count;
            }


            public int update(UserInfo info){
                return update(info, "rowid="+info.rowid);
            }


            public ArrayList<UserInfo>query(String condition){
                String sql=String.format("select rowid, _id, name, age, height, weight, married, update_time, "+
                        "phone, password from %s where %s; ", TABLE_NAME, condition);
                Log.d(TAG, "query sql:"+sql);
                ArrayList<UserInfo>infoArray=new ArrayList<UserInfo>();
                Cursor cursor=mDB.rawQuery(sql, null);
                if(cursor.moveToFirst()){
                    for(; ; cursor.moveToNext()){
                        UserInfo info=new UserInfo();
                        info.rowid=cursor.getLong(0);
                        info.xuhao=cursor.getInt(1);
                        info.name = cursor.getString(2);
                        info.age = cursor.getInt(3);
                        info.height = cursor.getLong(4);
                        info.weight = cursor.getFloat(5);
                        //SQLite没有布尔型,用0表示false,用1表示true
                        info.married = (cursor.getInt(6)==0)? false:true;
                        info.update_time = cursor.getString(7);
                        info.phone = cursor.getString(8);
                        info.password = cursor.getString(9);
                        infoArray.add(info);
                        if (cursor.isLast() == true) {
                            break;
                        }
                    }
                }
                cursor.close();
                return infoArray;
            }


            public UserInfo queryByPhone(String phone) {
                UserInfo info = null;
                ArrayList<UserInfo> infoArray = query(String.format("phone='%s'", phone));
                if (infoArray.size() > 0) {
                    info = infoArray.get(0);
                }
                return info;
            }
        }

4.2.3 优化记住密码功能

在“4.1.2实现记住密码功能”中,我们利用共享参数实现了记住密码的功能,不过这个方法有局限,只能记住一个用户的登录信息,并且手机号码跟密码不存在从属关系,如果换个手机号码登录,前一个用户的登录信息就被覆盖了。真正意义上的记住密码功能是先输入手机号码,然后根据手机号匹配保存的密码,一个密码对应一个手机号码,从而实现具体手机号码的密码记忆功能。

现在我们运用SQLite技术分条存储不同用户的登录信息,并提供根据手机号码查找登录信息的方法,这样可以同时记住多个手机号码的密码。具体的改造主要有以下3点:

(1)声明一个UserDBHelper对象,然后在活动页面的onResume方法中打开数据库连接,在onPasue方法中关闭数据库连接,示例代码如下:

            @Override
            protected void onResume() {
                super.onResume();
                mHelper = UserDBHelper.getInstance(this, 2);
                mHelper.openWriteLink();
            }


            @Override
            protected void onPause() {
                super.onPause();
                mHelper.closeLink();
            }

(2)登录成功时,如果用户勾选了“记住密码”,就使用数据库保存手机号码与密码在内的登录信息。在loginSuccess函数中增加如下代码:

                if (bRemember) {
                    UserInfo info = new UserInfo();
                    info.phone = et_phone.getText().toString();
                    info.password = et_password.getText().toString();
                    info.update_time = DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss");
                    mHelper.insert(info);
                }

(3)再次打开登录页面,用户输入手机号完毕后点击密码输入框时,App到数据库中根据手机号查找登录记录,并将记录结果中的密码填入密码框。

看到这里,读者也许已经想到给密码框注册点击事件,然后在onClick方法中补充数据库读取操作。可是EditText比较特殊,点击后只是让其获得焦点,再次点击才会触发点击事件。也就是说,要连续点击两次EditText才会处理点击事件。Android有时就是这么调皮捣蛋,你让它往东,它偏偏往西。难不成叫用户将就一下点击两次?用户肯定觉得这个App古怪、难用,还是卸载好了……这里提供一个解决办法,先给密码框注册一个焦点变更监听器,比如下面这行代码:

                et_password.setOnFocusChangeListener(this);

这个焦点变更监听器要实现接口OnFocusChangeListener,对应的事件处理方法是onFocusChange,将数据库查询操作放在该方法中,详细代码如下:

            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                String phone = et_phone.getText().toString();
                if (v.getId() == R.id.et_password) {
                    if (phone.length() > 0 && hasFocus == true) {
                        UserInfo info = mHelper.queryByPhone(phone);
                        if (info ! = null) {
                            et_password.setText(info.password);
                        }
                    }
                }
            }

这样,就不再需要点击两次才处理点击事件了。

代码写完后,再来看登录页面的效果图,用户上次登录成功时已勾选“记住密码”,现在再次进入登录页面,用户输入手机号后光标还停留在手机框,如图4-8所示。接着点击密码框,光标随之跳到密码框,这时密码框自动填入了该手机号对应的密码串,如图4-9所示。如此便真正实现了记住密码功能。

图4-8 光标在手机号码框

图4-9 光标在密码输入框