SQLite本地数据库的应用

说明
我们知道savedInstanceState、文件与SharedPreference都能够保存数据,但他们都无法满足应用持久化保存数据的需求,Android为此提供了长期存储地:即SQLite数据库。

概述

SQLite是一个轻量级的关系型数据库,运算速度快,占用资源少,很适合在移动设备上使用, 不仅支持标准SQL语法,还遵循ACID(数据库事务)原则,无需账号,使用起来非常方便!

SQLite是类似于MySQL和Postgresql的开源关系型数据库。不同于其他数据库的是, SQLite使用单个文件存储数据,使用SQLite库读取数据。

小结下特点:

SQlite通过文件保存数据库,一个文件就是一个数据库,数据库中又包含多个表格,表格里又有 多条记录,每个记录由多个字段构成,每个字段有对应的值,每个值我们可以指定类型,也可以不指定 类型(主键除外)

关于SQLite数据库支持存储的数据类型及相关的基本操作语句可以移步到android中的数据库操作或者SQLite在线文档

Android标准库包含SQLite库以及配套的一些Java辅助类。

使用SQLite本地数据库

Step 1 : 定义Schema

我们以上一篇RecyclerView的基本用法为例,将每一个View对象中的内容存入数据库。

创建数据库前,首先要清楚存储什么样的数据。 我们要保存的是一条条Info信息
记录,这需要定义如图所示的infos数据表。

这里写图片描述

SQL中一个重要的概念是schema:一种DB结构的正式声明,用于表示database的组成结构。schema是从创建DB的SQL语句中生成的。我们会发现创建一个伴随类(companion class)是很有益的,这个类称为合约类(contract class),它用一种系统化并且自动生成文档的方式,显示指定了schema样式。

Contract Clsss是一些常量的容器。它定义了例如URIs表名列名等。这个contract类允许在同一个包下与其他类使用同样的常量。 它让我们只需要在一个地方修改列名,然后这个列名就可以自动传递给整个code

组织contract类的一个好方法是在类的根层级定义一些全局变量,然后为每一个table来创建内部类

首先,我们来创建定义schema的Java类。创建时,新建一个databas,在包下新建类命名为InfoDbSchema,这样,就可以将InfoDbSchema.java文件放入专门的database包中,实现数据库操作相关代码的组织和归类。

在InfoDbSchema类中,再定义一个描述数据表的InfoTable内部类:

public class InfoDbScheme {
    public static final class InfoTable{
        public static final String NAME = "infos";
    }
}

InfoTable内部类唯一的用途就是定义描述数据表元素的String常量。首先要定义的是数据库表名(InfoTable.NAME)

接下来定义数据表字段:

public class InfoDbScheme {
    public static final class InfoTable{
        public static final String NAME = "infos";

        public static final class Col{
            public static final String UUID = "uuid";
            public static final String TITLE = "title";
            public static final String DATE = "date";
        }
    }
}

有了这些数据表元素,就可以在Java代码中安全地引用了。例如, InfoTable.Cols.TITLE就是指Info记录的title字段。此外,这种定义方式还给修改字段名称或新增表元素带来了方便。

step 2 : 使用SQL Helper创建初始数据库

定 义 完 数 据 库 schema , 就 可 以 创 建 数 据 库 了 。 openOrCreateDatabase(…) 和databaseList()方法是Android提供的Context底层方法,可以用来打开数据库文件并将其转换为SQLiteDatabase实例。

不过,实际开发时,建议总是遵循以下步骤。

  1. 确认目标数据库是否存在。

  2. 如果不存在,首先创建数据库,然后创建数据库表以及必需的初始化数据。

  3. 如果存在,打开并确认InfoDbSchema是否是最新版本。

  4. 如果是旧版本,就运行相关代码升级到最新版本。

    令人高兴的是, Android提供的SQLiteOpenHelper类可以帮我们处理这些。在数据库包中创建InfoBaseHelper类(InfoBaseHelper.java):

public class InfoBaseHelper extends SQLiteOpenHelper {
private static final int VERSION = 1;
private static final String DATABASE_NAME = "infoBase.db";
public InfoBaseHelper(Context context) {
super(context, DATABASE_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}

有了SQLiteOpenHelper类,打开SQLiteDatabase的繁杂工作都可以交给它处理。在InfoLab中用它创建infos数据库(InfoLab.java):

public class InfoLab {
    private static InfoLab sInfoLab;
    private Context mAppContext;
    private ArrayList<Info> mInfos;
    private SQLiteDatabase mDateBase;

    private InfoLab(Context appContext){
        mAppContext = appContext.getApplicationContext();
        mDateBase = new InfoBaseHelper(mAppContext).getWritableDatabase();
        mInfos = new ArrayList<Info>();

         /* for(int i = 0;i<100;i++){
            Info info = new Info();
            info.setmTtitle("Info #"+i);
            mInfos.add(info);
       }*/
    }
    ...
}

这里调用getWritableDatabase()方法时, CrimeBaseHelper要做如下工作。

  1. 打开/data/data/com.example.sqlitetest2/databases/crimeBase.db数据库;如果不存在,就先创建crimeBase.db数据库文件。

  2. 如果是首次创建数据库,就调用onCreate(SQLiteDatabase)方法,然后保存最新的版本号。

  3. 如果已创建过数据库,首先检查它的版本号。如果InfoOpenHelper中的版本号更高,就调用onUpgrade(SQLiteDatabase, int, int)方法升级。

最后,再做个总结: onCreate(SQLiteDatabase)方法负责创建初始数据库; onUpgrade(SQLiteDatabase, int, int)方法负责与升级相关的工作。

我 们 在onCreate(…)方法中创建数据库表,这需要导入InfoDbSchema类的InfoTable内部类。(InfoBaseHelper.java

 public void onCreate(SQLiteDatabase db) {

        db.execSQL("create table " + InfoTable.NAME + "(" +
                " _id integer primary key autoincrement, " +
                InfoTable.Col.UUID + ", " +
                InfoTable.Col.TITLE + ", " +
                InfoTable.Col.DATE  +
                ")"
        );
    }

现在我们就在手机本地文件中创建了一个本地数据库,数据库名字叫做infoBase.db,在数据库中还创建了一个数据库表,表的名字叫做infos,在表中我们还创建了几个字段,uuid、title还有date。

我们可以在手机目录/data/data/[your package name]下查看(前提是手机要root),你就可以看到下图这样的文件。

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

当然现在info表里我们还没有添加数据。

step 3 : 写入数据库

要使用SQLiteDatabase,数据库中首先要有数据。数据库写入操作有:向infos表中插入新记录以及在Info变更时更新原始记录。

我们修改InfoLab类,不用List来存储数据,改用mDateBase来存储数据,首先要删除掉InfoLab类中的ArrayList<Info>代码,增加一个添加数据的方法及更新数据的方法,改动完成如下:(InfoLab.java

public class InfoLab {
    private static InfoLab sInfoLab;
    private Context mAppContext;
   // private ArrayList<Info> mInfos;
    private SQLiteDatabase mDateBase;

    private InfoLab(Context appContext){
        mAppContext = appContext.getApplicationContext();
        mDateBase = new InfoBaseHelper(mAppContext).getWritableDatabase();
       // mInfos = new ArrayList<Info>();

         /* for(int i = 0;i<100;i++){
            Info info = new Info();
            info.setmTtitle("Info #"+i);
            mInfos.add(info);
       }*/
    }

    public static InfoLab get(Context c){
        if(sInfoLab==null){
            sInfoLab = new InfoLab(c.getApplicationContext());
        }
        return sInfoLab;
    }

    public ArrayList<Info> getInfos(){
      //  return mInfos;
        return new ArrayList<>();
    }

    public Info getInfo(UUID uuid){
//        for(Info i:mInfos){
//            if(i.getmId().equals(uuid)){
//                return i;
//            }
//        }
        return null;
    }

    public void addInfo(Info info){
      //  mInfos.add(info);

    }

 public void updateInfo(Info info){

    }
}

在InfoListFragment.java类中增加添加数据的按钮,修改info_list_activity.xml的代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".InfoListActivity">

    <android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/info_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <LinearLayout
        android:id="@+id/empty_crime_list"
        android:layout_width="wrap_content"
        android:layout_height="123dp"
        android:orientation="vertical"
        android:layout_gravity="center">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:padding="16dp"
            android:text="没有Info记录可以显示"/>

        <Button
            android:id="@+id/add_crime_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:padding="16dp"
            android:text="@string/new_crime"/>
    </LinearLayout>
</LinearLayout>

这里写图片描述

修改InfoListActivity.java代码如下:

public class InfoListActivity extends AppCompatActivity {
    ...
    private LinearLayout mLinearLayout;
    private Button addButton;
    ...
     protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.info_list_activity);

        mLinearLayout = (LinearLayout)this.findViewById(R.id.empty_crime_list);
        addButton = (Button)this.findViewById(R.id.add_crime_button);

        mInfoRecyclerView = (RecyclerView)this.findViewById(R.id.info_recycler_view);
        mInfoRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        updateUI();
    } 
    ...
     private void updateUI() {
        InfoLab infoLab = InfoLab.get(this);
        List<Info> infos = infoLab.getInfos();

        if(mAdapter==null){
            mAdapter = new InfoAdapter(infos);
            mInfoRecyclerView.setAdapter(mAdapter);
        }
        else{
            mAdapter.notifyDataSetChanged();
        }
        if(infos.size()>0){
            mLinearLayout.setVisibility(View.GONE);
        }
        else{
            mLinearLayout.setVisibility(View.VISIBLE);
            addButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.d("hehe","hehe");
                   Info info = new Info();
                    InfoLab.get(InfoListActivity.this).addInfo(info);
                    Intent intent = new Intent(InfoListActivity.this,InfoDetailActivity.class);
                    intent.putExtra(EXTRA_INFO_ID,info.getmId());
                    startActivity(intent);
                }
            });
        }
           mInfoRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST));
    }   

}

现在我们点击按钮,就会加载InfoDetailActivity.java页面。

接下来开始往数据库中写入数据:

使用 ContentValues

负责处理数据库写入和更新操作的辅助类是ContentValues。它是个键值存储类,类似于Java的HashMap和前面用过的Bundle。不同的是, ContentValues只能用于处理SQLite数据。

step 4 : 创建ContentValues( InfoLab.java )

public class InfoLab {
    ...
     public static ContentValues getContentValues(Info info){
        ContentValues values = new ContentValues();
        values.put(InfoTable.Col.UUID,info.getmId().toString());
        values.put(InfoTable.Col.TITLE,info.getmTtitle());
        values.put(InfoTable.Col.DATE,info.getmDate().toString());
        return values;
    }
}

step 5 : 插入和更新记录( InfoLab.java )

 public void addInfo(Info info){
      //  mInfos.add(info);
        ContentValues values = getContentValues(info);
        mDateBase.insert(InfoTable.NAME,null,values);
    }

insert(String, String, ContentValues)方法有两个重要的参数,还有一个很少用到。
传入的第一个参数是数据库表名,最后一个是要写入的数据。
第二个参数称为nullColumnHack。它有什么用途呢?
别急,举个例子你就明白了。假设你想调用insert(…)方法,但传入了ContentValues
空值。这时, SQLite不干了, insert(…)方法调用只能以失败告终。
然而,如果能以uuid值作为nullColumnHack传入的话, SQLite就可以忽略ContentValues空值,而且还会自动传入一个带uuid且值为null的ContentValues。结果, insert(…)方法得以成功调用并插入了一条新记录

  public void updateInfo(Info info){
        String uuidString = info.getmId().toString();
        ContentValues values = getContentValues(info);
        mDateBase.update(InfoTable.NAME, values,
                InfoTable.Col.UUID + " = ?",
                new String[] { uuidString });

    }

update(String, ContentValues, String, String[])更新方法类似于insert(…)方法,向其传入要更新的数据表名和为表记录准备的ContentValues。然而,与insert(…)方法不同的是,你要确定该更新哪些记录。具体的做法是:创建where子句(第三个参数) ,然后指定where子句中的参数值(String[]数组参数)。
问题来了,为什么不直接在where子句中放入uuidString呢?这可比使用?然后传入String[]简单多了!
事实上,很多时候, String本身会包含SQL代码。如果将它直接放入query语句中,这些代码
可能会改变query语句的含义,甚至会修改数据库资料。这实际就是SQL脚本注入, 危害相当严重。
使用?的话,就不用关心String包含什么,代码执行的效果肯定就是我们想要的。

step 6 : Info数据刷新( InfoDetailActivity.java )

public class InfoDetailActivity extends AppCompatActivity {
    ...
    public void onCreate(Bundle savedInstanceState) {
    ...
    }

    @Override
    protected void onResume() {
        super.onResume();
        InfoLab.get(this).updateInfo(mInfo);
    }
}

这样,点击按钮,你就可以往里面插入数据了,因为还没有完成会导致闪退,但是数据库中已经成功的添加了一条数据,打开数据库目录可以看到:

这里写图片描述

step 7 : 读取数据库

读取SQLite数据库中数据需要用到query(…)方法。这个方法有好几个重载版本。我们要用的版本如下:

public Cursor query(
String table,
String[] columns,
String where,
String[] whereArgs,
String groupBy,
String having,
String orderBy,
String limit)

参数table是要查询的数据表。参数columns指定要依次获取哪些字段的值。参数where和whereArgs的作用与update(…)方法中的一样。
新增一个便利方法调用query(…)方法查询InfoeTable中的记录( InfoLab.java )

private Cursor queryCrimes(String whereClause, String[] whereArgs) {
Cursor cursor = mDatabase.query(
InfoTable.NAME,
null, // Columns - null selects all columns
whereClause,
whereArgs,
null, // groupBy
null, // having
null // orderBy
);
return cursor;
}

step 8 : 使用 CursorWrapper

Cursor是个神奇的表数据处理工具,其任务就是封装数据表中的原始字段值。

创建InfoCursorWrapper类(InfoCursorWrapper.java

public class InfoCursorWrapper extends CursorWrapper {
    /**
     * Creates a cursor wrapper.
     *
     * @param cursor The underlying cursor to wrap.
     */
    public InfoCursorWrapper(Cursor cursor) {
        super(cursor);
    }

    ...
}

新增getCrime()方法(InfoCursorWrapper.java

public class InfoCursorWrapper extends CursorWrapper {
    /**
     * Creates a cursor wrapper.
     *
     * @param cursor The underlying cursor to wrap.
     */
    public InfoCursorWrapper(Cursor cursor) {
        super(cursor);
    }

    public Info getInfo() {
        String uuidString = getString(getColumnIndex(InfoTable.Col.UUID));
        String title = getString(getColumnIndex(InfoTable.Col.TITLE));
        long date = getLong(getColumnIndex(InfoTable.Col.DATE));

        Info info = new Info(UUID.fromString(uuidString));
        info.setmTtitle(title);
        info.setmDate(new Date(date));
        return info;
    }
}

step 9 : 使用cursor封装方法(InfoLab.java)

 private InfoCursorWrapper queryInfo(String whereClaues,String[] whereArgs){
        Cursor cursor = mDateBase.query(
                InfoTable.NAME,
                null, // Columns - null selects all columns
                whereClaues,
                whereArgs,
                null, // groupBy
                null, // having
                null // orderBy
                 );

        return new InfoCursorWrapper(cursor);
    }

step 10 :返回info列表(InfoLab.java)

 public ArrayList<Info> getInfos(){
      //  return mInfos;
        //return new ArrayList<>();

        ArrayList<Info> infos = new ArrayList<>();
       InfoCursorWrapper cursor = queryInfo(null, null);
        try {
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                infos.add(cursor.getInfo());
                cursor.moveToNext();
            }
        } finally {
            cursor.close();
        }
        return infos;
    }

要从cursor中取出数据,首先要调用moveToFirst()方法移动cursor指向第一个元素。读取行记录后,再调用moveToNext()方法,读取下一行记录,直到isAfterLast()告诉我们没有数据可取为止。

最后,别忘了调用Cursor的close()方法关闭它。

step11 :重写getInfo(UUID)方法(InfoLab.java)

 public Info getInfo(UUID uuid){
//        for(Info i:mInfos){
//            if(i.getmId().equals(uuid)){
//                return i;
//            }
//        }
       // return null;
        InfoCursorWrapper cursor = queryInfo(
                InfoTable.Col.UUID + " = ?",
                new String[] { uuid.toString() }
        );
        try {
            if (cursor.getCount() == 0) {
                return null;
            }
            cursor.moveToFirst();
            return cursor.getInfo();
        } finally {
            cursor.close();
        }
    }

上述代码的作用如下。

现在可以插入info记录了。也就是说,点击New Crime菜单项,实现将info添加到InfoLab的代码可以正常工作了。

数据库查询没有问题了。 InfoDetailActivity现在能够看见InfoLab中的所有Info了。

InfoLab.getInfo(UUID) 方 法 也 能 正 常 工 作 了 。 InfoDetailActivity 终于可以显示真正的Info对象了。

step 12 : 刷新模型层数据

添加setInfos(List<Info>)方法(InfoListActivity.java):

private class InfoAdapter extends RecyclerView.Adapter<InfoHolder> {
...

@Override
public int getItemCount() {
    return mInfos.size();
}

 public void setInfos(List<Info> infos){
            mInfos = infos;
        }

}

然后在updateUI()方法中调用setInfos(List<Info> infos)方法(InfoListActivity.java)

    private void updateUI() {
        InfoLab infoLab = InfoLab.get(this);
        List<Info> infos = infoLab.getInfos();

        if(mAdapter==null){
            mAdapter = new InfoAdapter(infos);
            mInfoRecyclerView.setAdapter(mAdapter);
        }
        else{
            mAdapter.setInfos(infos);
            mAdapter.notifyDataSetChanged();
        }
       ...
    }

现在,可以验证我们的成果了。运行应用,新增一项info记录,然后按回退键,
确认InfoListActivity中会出现刚才新增的记录。数据库中也添加了info记录。

源码在这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值