android中原始数据库创建与框架的对比,建表三范式的理解

范式的理解
数据库表的建立都应该遵守三个范式,

  1. 第一范式
    对于表的属性是不可再分的例如学生表:
    学生的属性有,姓名,年龄,学号,班级,这就复合第一范式,如果如果表存在班级一班级二放在第二排的话就不属于第一范式,由于dbms语法规定傻瓜也不会建立一个不属于第一范式的表。
  2. 第二范式
    对于表的属性非主属性完全依赖于主属性,注意:完全依赖。
    学生属性有,姓名,年龄,学号,班级,这就复合第二范式,如果学生属性添加课程,学分的话,因为存在学分依赖与课程,学分不依赖学号,所以不满足完全依赖主属性,所以第二张表不属于第二范式。
  3. 第三范式
    对于表的属性不存在递归依赖关系,学生属性有。学号,姓名,班级,班主任,这个属于第二范式却不属于第三范式,学号可以得出班级,班级可以得出班主任,学号也可以得出班主任,存在递归依赖

  4. 总结
    数据库表都满足三范式,那么无疑所建的的数据库中的数据冗余度会很小,而且简洁干净。
    传统的建表方式
    其实为了方便我们对数据库表进行管理,Android本身就提供了一个帮助类:SQLiteOpenHelper。这个类集创建和升级数据库于一身,并且自动管理了数据库版本,算是一个非常好用的工具。
    由源码public abstract class SQLiteOpenHelper可以知道这个帮助类SQLiteOpenHelper是一个抽象类
    这意味着如果我们想要使用它的话,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是onCreate()和onUpgrade(),我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
    代码如下:

public class MySqlLiteHelper extends SQLiteOpenHelper{
    public MySqlLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

其中,当数据库创建的时候会调用onCreate()方法,在这里去执行建表操作就可以了。比如说我们想新建一张news表,其中有title,content,publishdate,commentcount这几列,分别代表着新闻标题、新闻内容、发布时间和评论数,那么代码就可以这样写:

public class MySqlLiteHelper extends SQLiteOpenHelper{
    public static final String CREATE_NEWS = "create table news ("
            + "id integer primary key autoincrement, "
            + "title text, "
            + "content text, "
            + "publishdate integer,"
            + "commentcount integer)";
    public MySqlLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_NEWS);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

可以看到,我们把建表语句定义成了一个常量,然后在onCreate()方法中去执行了这条建表语句,news表也就创建成功了。这条建表语句虽然简单,但是里面还是包含了一些小的细节,我来解释一下。首先,根据数据库的范式要求,任何一张表都应该是有主键的,所以这里我们添加了一个自增长的id列,并把它设为主键。然后title列和content列都是字符串类型的,commentcount列是整型的,这都很好理解,但是publishdate列该怎么设计呢?由于SQLite中并不支持存储日期这种数据类型,因此我们需要将日期先转换成UTC时间(自1970年1月1号零点)的毫秒数,然后再存储到数据库中,因此publishdate列也应该是整型的。

现在,我们只需要获取到SQLiteDatabase的实例,数据库表就会自动创建了,如下所示:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MySqlLiteHelper dbHelper = new MySqlLiteHelper(this, "demo.db", null, 1);
        SQLiteDatabase db = dbHelper.getWritableDatabase();
    }
}

这是android原始数据库操作api感觉也不是很复杂,那么框架是不是更简单,我们现在来分析一下LitePal的基本用法

不管用什么框架首先配置是一定要有的下面是我们来快读配置一下方便开发。

快速配置

  1. 引入Jar包或源码

首先我们需要将LitePal的jar包引入到项目当中,可以点击这里查看LitePal的最新版本,选择你需要的下载即可。下载好了jar包之后,把它复制到项目的libs目录中就算是引入成功了,如下图所示:

android中原始数据库创建与框架的对比,建表三范式的理解_第1张图片

如果你不想用jar包的话,也可以把LitePal的源码下载下来,然后作为一个library库导入到Eclipse当中,再让我们的项目去引用这个library库就可以了。

  1. 配置litepal.xml

接着在项目的assets目录下面新建一个litepal.xml文件,并将以下代码拷贝进去:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="demo1" ></dbname>

    <version value="1.0" ></version>

    <list>
    </list>
</litepal>

配置文件相当简单,用于设定数据库的名字,用于设定数据库的版本号,用于设定所有的映射模型,我们稍后就会用到。

  1. 配置LitePalApplication

由于操作数据库时需要用到Context,而我们显然不希望在每个接口中都去传一遍这个参数,那样操作数据库就显得太繁琐了。因此,LitePal使用了一个方法来简化掉Context这个参数,只需要在AndroidManifest.xml中配置一下LitePalApplication,所有的数据库操作就都不用再传Context了,如下所示:

<application
        android:name="org.litepal.LitePalApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

当然,有些程序可能会有自己的Application,并在这里配置过了。比如说有一个MyApplication没有关系,这时只需要修改一下MyApplication的继承结构,让它不要直接继承Application类,而是继承LitePalApplication类,就可以使用一切都能正常工作了。
但是,有些程序可能会遇到一些更加极端的情况,比如说MyApplication需要继承另外一个AnotherApplication,并且这个AnotherApplication还是在jar包当中的,不能修改它的代码。这种情况应该算是比较少见了,但是如果你遇到了的话也不用急,仍然是有解释方案的。你可以把LitePal的源码下载下来,然后把src目录下的所有代码直接拷贝到你项目的src目录下面,接着打开LitePalApplication类,将它的继承结构改成继承自AnotherApplication,再让MyApplication继承自LitePalApplication,这样所有的Application就都可以在一起正常工作了。
仅仅三步,我们就将所有的配置工作全部完成了,并且这是一件一本万利的事情,自此以后,你就可以开心地体验LitePal提供的各种便利了,就让我们从建表开始吧。
开始建表
前面在介绍的时候已经说了,LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而我们使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。

但是我们为什么要使用对象关系映射模式呢?这主要是因为大多数的程序员都很擅长面向对象编程,但其中只有少部分的人才比较精通关系型数据库。而且数据库的SQL语言晦涩难懂,就算你很精通它,恐怕也不喜欢经常在代码中去写它吧?而对象关系映射模式则很好地解决了这个问题,它允许我们使用面向对象的方式来操作数据库,从而可以从晦涩难懂的SQL语言中解脱出来。

那么接下来我们就看一看LitePal中是如何建表的吧。根据对象关系映射模式的理念,每一张表都应该对应一个模型(Model),也就是说,如果我们想要建一张news表,就应该有一个对应的News模型类。新建一个News类,如下所示:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MySqlLiteHelper dbHelper = new MySqlLiteHelper(this, "demo.db", null, 1);
        SQLiteDatabase db = dbHelper.getWritableDatabase();//原始方法创建数据库
        SQLiteDatabase db1 = Connector.getDatabase();//使用框架创建数据库
    }
}

创建表使用原始方法要自己写语句,使用框架配置麻烦一点但是一劳永逸。
创建表很简单,表的升级呢?在做项目的时候难免我们在考虑数据库的时候总有想不到的数据,如果数据库不单单有新闻实体,还有评论实体呢?那么我们很快就会想到代码如下:

public class MySqlLiteHelper extends SQLiteOpenHelper{
    public static final String CREATE_NEWS = "create table news ("
            + "id integer primary key autoincrement, "
            + "title text, "
            + "content text, "
            + "publishdate integer,"
            + "commentcount integer)";
    public static final String CREATE_COMMENT = "create table comment ("
            + "id integer primary key autoincrement, "
            + "content text)";
    public MySqlLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_NEWS);
        db.execSQL(CREATE_COMMENT);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

这样存在一个问题,就是说如果数据库创建好了的话,应用已经安装过了的话,程序是不会执行onCteate()这个方法了,也就是说第二个表对于已经安装了该应用的用户来说不会被创建,那么我们就要在onUpgrade()方法中做文章了

@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
        case 1:
            db.execSQL(CREATE_COMMENT);
        default:
        }
创建表只是数据库操作中最基本的一步而已,我们在一开始创建的表结构,随着需求的变更,到了后期是极有可能需要修改的。因此,升级表的操作对于任何一个项目也是至关重要的,那么今天我们就一起来学习一下,在Android传统开发当中升级表的方式,以及使用LitePal来进行升级表操作的用法。如果你还没有看过前一篇文章,建议先去参考一下 Android数据库高手秘籍(二)——创建表和LitePal的基本用法 。
借助MySQLiteHelper已经创建好了news这张表,这也是demo.db这个数据库的第一个版本。然而,现在需求发生了变更,我们的软件除了能看新闻之外,还应该允许用户评论,所以这时就需要对数据库进行升级,添加一个comment表。

该怎么做呢?添加一个comment表的建表语句,然后在onCreate()方法中去执行它?没错,这样的话,两张表就会同时创建了,代码如下所示:
Java
public class MySQLiteHelper extends SQLiteOpenHelper {

public static final String CREATE_NEWS = "create table news ("
        + "id integer primary key autoincrement, "
        + "title text, "
        + "content text, "
        + "publishdate integer,"
        + "commentcount integer)";

public static final String CREATE_COMMENT = "create table comment ("
        + "id integer primary key autoincrement, "
        + "content text)";

public MySQLiteHelper(Context context, String name, CursorFactory factory,
        int version) {
    super(context, name, factory, version);
}

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_NEWS);
    db.execSQL(CREATE_COMMENT);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}

}

public class MySQLiteHelper extends SQLiteOpenHelper {

public static final String CREATE_NEWS = "create table news ("
        + "id integer primary key autoincrement, "
        + "title text, "
        + "content text, "
        + "publishdate integer,"
        + "commentcount integer)";

public static final String CREATE_COMMENT = "create table comment ("
        + "id integer primary key autoincrement, "
        + "content text)";

public MySQLiteHelper(Context context, String name, CursorFactory factory,
        int version) {
    super(context, name, factory, version);
}

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_NEWS);
    db.execSQL(CREATE_COMMENT);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}

}

这对于第一次安装我们软件的用户来说是完全可以正常工作的,但是如果有的用户已经安装过上一版的软件,那么很遗憾,comment表是创建不出来的,因为之前数据库就已经创建过了,onCreate()方法是不会重新执行的。

对于这种情况我们就要用升级的方式来解决了,看到MySQLiteHelper构造方法中的第四个参数了吗,这个就是数据库版本号的标识,每当版本号增加的时候就会调用onUpgrade()方法,我们只需要在这里处理升级表的操作就行了。比较简单粗暴的方式是将数据库中现有的所有表都删除掉,然后重新创建,代码如下所示:
Java
public class MySQLiteHelper extends SQLiteOpenHelper {

......

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_NEWS);
    db.execSQL(CREATE_COMMENT);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("drop table if exists news");
    onCreate(db);
}

}
public class MySQLiteHelper extends SQLiteOpenHelper {

......

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_NEWS);
    db.execSQL(CREATE_COMMENT);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("drop table if exists news");
    onCreate(db);
}

}

可以看到,当数据库升级的时候,我们先把news表删除掉,然后重新执行了一次onCreate()方法,这样就保证数据库中的表都是最新的了。

但是,如果news表中本来已经有数据了,使用这种方式升级的话,就会导致表中的数据全部丢失,所以这并不是一种值得推荐的升级方法。那么更好的升级方法是什么样的呢?这就稍微有些复杂了,需要在onUpgrade()方法中根据版本号加入具体的升级逻辑,我们来试试来吧。比如之前的数据库版本号是1,那么在onUpgrade()方法中就可以这样写:
Java
public class MySQLiteHelper extends SQLiteOpenHelper {

......

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_NEWS);
    db.execSQL(CREATE_COMMENT);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    switch (oldVersion) {
    case 1:
        db.execSQL(CREATE_COMMENT);
    default:
    }
}

}
public class MySQLiteHelper extends SQLiteOpenHelper {

......

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_NEWS);
    db.execSQL(CREATE_COMMENT);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    switch (oldVersion) {
    case 1:
        db.execSQL(CREATE_COMMENT);
    default:
    }
}

}

可以看到,这里在onUpgrade()方法中加入了一个switch判断,如果oldVersion等于1,就再创建一个comment表。现在只需要调用如下代码,表就可以得到创建或升级了:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 2);
SQLiteDatabase db = dbHelper.getWritableDatabase();

这样数据就升级了。
然后我们来分析框架是怎么做到的
OK,现在软件的第二版本也发布出去了,可是就在发布不久之后,突然发现comment表中少了一个字段,我们并没有记录评论发布的时间。没办法,只好在第三版中修复这个问题了,那我们该怎么样去添加这个字段呢?主要需要修改comment表的建表语句,以及onUpgrade()方法中的逻辑,代码如下所示:

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
        case 1:
            db.execSQL(CREATE_COMMENT);
            break;
        case 2:
            db.execSQL("alter table comment add column publishdate integer");
            break;
        default:
        }

现在我们已经学习了新增表和新增列这两种升级方式,那么如果是某张表中的某一列已经没有用了,我想把这一列删除掉该怎么写呢?很遗憾,SQLite并不支持删除列的功能,对于这情况,多数软件采取的作法是无视它,反正以后也用不到它了,留着也占不了什么空间,所以针对于这种需求,确实没什么简单的解决办法。

这大概就是传统开发当中升级数据库表的方式了,虽说能写出这样的代码表示你已经对数据库的升级操作理解的比较清楚了,但随着版本越来越多,onUpgrade()方法中的逻辑也会变得愈发复杂,稍微一不留神,也许就会产生错误。因此,如果能让代码自动控制升级逻辑,而不是由人工来管理,那就是再好不过了,那么下面我们就来学习一下怎样使用LitePal来进行升级表的操作。
使用框架来升级表就很简单了,如果要添加一个表只需要添加一个类,然后把该类添加到配置文件中去代码如下:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="demo" ></dbname>

    <version value="2" ></version>

    <list>
        <mapping class="com.example.databasetest.model.News"></mapping>
        <mapping class="com.example.databasetest.model.Comment"></mapping>
    </list>
</litepal>
public class Comment {

    private int id;

    private String content;

    private Date publishDate;

    // 自动生成get、set方法 
    ...
}

非常简单不需要自己控制数据库升级,通过这两种升级方式的对比,相信你已经充分体会到了使用LitePal进行升级表操作所带来的便利了吧。我们不需要去编写任何与升级相关的逻辑,也不需要关心程序是从哪个版本升级过来的,唯一要做的就是确定好最新的Model结构是什么样的,然后将litepal.xml中的版本号加1,所有的升级逻辑就都会自动完成了。LitePal确实将数据库表的升级操作变得极度简单,使很多程序员可以从维护数据库表升级的困扰中解脱出来。

然而,LitePal却明显做到了更好。前面我们提到过关于删除列的问题,最终的结论是无法解决,因为SQLite是不支持删除列的命令的。但是如果使用LitePal,这一问题就可以简单地解决掉,比如说publishdate这一列我们又不想要了,那么只需要在Comment类中把它删除掉,然后将版本号加1,下次操作数据库的时候这个列就会不见了。

那么有的朋友可能会问了,不是说SQLite不支持删除列的命令吗?那LitePal又是怎样做到的呢?其实LitePal并没有删除任何一列,它只是先将comment表重命名成一个临时表,然后根据最新的Comment类的结构生成一个新的comment表,再把临时表中除了publishdate之外的数据复制到新的表中,最后把临时表删掉。因此,看上去的效果好像是做到了删除列的功能。

这也是使用框架的好处,如果没有框架的帮助,我们显然不会为了删除一个列而大废周章地去写这么多的代码,而使用框架的话,具体的实现逻辑我们已经不用再关心,只需要控制好模型类的数据结构就可以了。

另外,如果你想删除某一张表的话,操作也很简单,在litepal.xml中的映射列表中将相应的类删除,表自然也就不存在了。
表与表之间的关系,怎么表达框架很好的处理了这个问题,我们自己写sql语句要考虑很多问题,并不是那么好写,用框架的话就很方便了
关系代码如下

    public class News {  
        ...  
        private Introduction introduction;  //一对一

        private List<Comment> commentList = new ArrayList<Comment>(); //一对多 

        // 自动生成get、set方法 
    }  
        public class Comment {  
        ...  
        private News news; //一对一 

        // 自动生成get、set方法 
    }  
        public class News {  
        ...  
        private Introduction introduction;  

        private List<Comment> commentList = new ArrayList<Comment>();  

        private List<Category> categoryList = new ArrayList<Category>();  

        // 自动生成get、set方法 
    }  
        public class Category {  
        ...  
        private List<News> newsList = new ArrayList<News>();  //多对多

        // 自动生成get、set方法 
    }  

最后最重要的环节就是标的增删改查英文CRUD,最原始的代码就是利用Sql语句进行拼接来对数据库进行操作,这样虽然可以提升我们的基础知识,增加我们对数据库原始语句熟悉性,但是这样开发效率不高,所以android就提供了自己的一套api供代码如下:

    SQLiteDatabase db = dbHelper.getWritableDatabase();  
    ContentValues values = new ContentValues();  
    values.put("title", "这是一条新闻标题");  
    values.put("content", "这是一条新闻内容");  
    values.put("publishdate", System.currentTimeMillis());  
    long id = db.insert("news", null, values);  

使用框架更方便
这样一看就知道了这个弊端,每次只能插入一条数据,如果碰到批量插入的话就必须要用到循环。
使用LitePal存储数据
LitePal中与存储相关的API其实并不多,但用法还是颇为丰富的,而且比起传统的insert()方法,使用LitePal来存储数据可以简单到让你惊叹的地步,那么今天我们就来完整地学习一下LitePal存储数据的所有用法。

在前面几篇文章当中,我们在项目里已经建好了News、Comment、Introduction、Category这几个实体类,通过这些实体类,LitePal就可以把相应的表自动创建出来。现在来观察这几个实体类,我们发现这几个类都是没有继承结构的。没错,因为LitePal进行表管理操作时不需要这些实体类有任何的继承结构,当时为了简单起见就没有写。但是进行CRUD操作时就不行了,LitePal要求所有的实体类都要继承自DataSupport这个类,因此这里我们就要把继承结构给加上才行。修改News类的代码,如下所示:

public class News extends DataSupport {

        private int id;
    private Introduction introduction;

    private List<Comment> commentList = new ArrayList<Comment>();

    private List<Category> categoryList = new ArrayList<Category>();

    private String title;

        private String content;

        private Date publishDate;

        private int commentCount;

    public Introduction getIntroduction() {
        return introduction;
    }

    public void setIntroduction(Introduction introduction) {
        this.introduction = introduction;
    }

    public List<Comment> getCommentList() {
        return commentList;
    }

    public void setCommentList(List<Comment> commentList) {
        this.commentList = commentList;
    }

    public List<Category> getCategoryList() {
        return categoryList;
    }

    public void setCategoryList(List<Category> categoryList) {
        this.categoryList = categoryList;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getPublishDate() {
        return publishDate;
    }

    public void setPublishDate(Date publishDate) {
        this.publishDate = publishDate;
    }

    public int getCommentCount() {
        return commentCount;
    }

    public void setCommentCount(int commentCount) {
        this.commentCount = commentCount;
    }
}

那存储的代码就简单了直接对类进行操作

    News news = new News(); 
    news.setTitle("这是一条新闻标题"); 
    news.setContent("这是一条新闻内容"); 
    news.setPublishDate(new Date()); 
    news.save(); 

可以看出,save()方法返回的是一个布尔值,用于表示存储成功还是失败,但同时也说明这个方法是不会抛出异常的。有些朋友希望如果存储失败的话就抛出异常,而不是返回一个false,那就可以使用saveThrows()方法来代替
另外,LitePal对集合数据的存储还专门提供了一个方法,比如说我们有一个News集合,那么应该怎样去存储这个集合中的每条News呢?传统情况下可以这样写:
通过一个循环来遍历出这个集合中的每一个News对象,然后逐个调用save()方法。这样的写法当然是可以的,但是效率会比较低,因为调用save()方法的时候除了会执行存储操作之外,还会去分析News类的关联关系,那么每次循环都去重新分析一遍关联关系显然是比较耗时的。因此,LitePal提供了一个saveAll()方法,专门用于存储集合数据的,用法如下所示:

    List<News> newsList;  
    ...  
    DataSupport.saveAll(newsList);  

saveAll()方法接收一个Collection集合参数,只要把待存储的集合数据传入即可。这个方法可以完成和上面一段代码完全一样的功能,但效率却会高得多,而且写法也更加简单。

好了,这样我们就把LitePal中提供的存储操作的用法全部都学完了,那么今天的文章就到这里,下一篇文章当中会开始讲解更新和删除操作的用法
表的修改和删除
SQLiteDatabase类中提供了一个insert()方法用于插入数据,那么类似地,它还提供了update()和delete()这两个方法,分别用于修改和删除数据。先来看一下update()方法的方法定义:public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
update()方法接收四个参数,第一个参数是表名,第二个参数是一个封装了待修改数据的ContentValues对象,第三和第四个参数用于指定修改哪些行,对应了SQL语句中的where部分。

那么比如说我们想把news表中id为2的记录的标题改成“今日iPhone6发布”,就可以这样写:

    SQLiteDatabase db = dbHelper.getWritableDatabase();  
    ContentValues values = new ContentValues();  
    values.put("title", "今日iPhone6发布");  
    db.update("news", values, "id = ?", new String[] {"2"}); 

接下来再看一下delete()方法的方法定义:public int delete(String table, String whereClause, String[] whereArgs)
delete()方法接收三个参数,第一个参数同样是表名,第二和第三个参数用于指定删除哪些行,对应了SQL语句中的where部分。

那么比如说我们想把news表中所有没有评论的新闻都删除掉,就可以这样写:

    SQLiteDatabase db = dbHelper.getWritableDatabase();  
    db.delete("news", "commentcount = ?", new String[] {"0"});  

数据查询

    public Cursor rawQuery(String sql, String[] selectionArgs)  

其中,rawQuery()方法接收两个参数,第一个参数接收的就是一个SQL字符串,第二个参数是用于替换SQL语句中占位符(?)的字符串数组。rawQuery()方法返回一个Cursor对象,所有查询到的数据都是封闭在这个对象当中的,我们只要一一取出就可以了。

当然这种用法其实并不是很常用,因为相信大多数人都还是不喜欢编写SQL语句的。所以,Android专门提供了一种封装好的API,使得我们不用编写SQL语句也能查询出数据,即SQLiteDatabase中的query()方法。query()提供了三个方法重载,其中参数最少的一个也有七个参数,我们来看下方法定义:

public Cursor query(String table, String[] columns, String selection,  
            String[] selectionArgs, String groupBy, String having,  
            String orderBy) 

其中第一参数是表名,表示我们希望从哪张表中查询数据。第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于去约束查询某一行或某几行的数据,不指定则默认是查询所有行的数据。第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。

这个方法是query()方法最少的一个方法重载了,另外还有两个方法重载分别是八个和九个参数。虽说这个方法在Android数据库表查询的时候非常常用,但重多的参数让我们在理解这个方法的时候可能会很费力,另外使用起来的时候也会相当的不爽。比如说,我们想查询news表中的所有数据,就应该要这样写:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

SQLiteDatabase db = dbHelper.getWritableDatabase();  
Cursor cursor = db.query("news", null, null, null, null, null, null);  

可以看到,将第一个表名参数指定成news,然后后面的六个参数我们都用不到,就全部指定成null。
那如果是我们想查询news表中所有评论数大于零的新闻该怎么写呢?代码如下所示:

SQLiteDatabase db = dbHelper.getWritableDatabase();  
Cursor cursor = db.query("news", null, "commentcount>?", new String[]{"0"}, null, null, null); 

由于第三和第四个参数是用于指定约束条件的,所以我们在第三个参数中指明了commentcount>?,然后在第四个参数中通过一个String数组来替换占位符,这样查到的结果就是news表中所有评论数大于零的新闻了。那么其它的几个参数呢?仍然用不到,所以还是只能传null。

然后我们可以看到,query()方法的返回值是一个Cursor对象,所有查询到的数据都是封装在这个对象中的,所以我们还需要将数据逐一从Cursor对象中取出,然后设置到News实体类当中,如下所示:

    List<News> newsList = new ArrayList<News>(); 
    if (cursor != null && cursor.moveToFirst()) {  
        do {  
            int id = cursor.getInt(cursor.getColumnIndex("id")); 
            String title = cursor.getString(cursor.getColumnIndex("title")); 
            String content = cursor.getString(cursor.getColumnIndex("content")); 
            Date publishDate = new Date(cursor.getLong(cursor.getColumnIndex("publishdate"))); 
            int commentCount = cursor.getInt(cursor.getColumnIndex("commentcount")); 
            News news = new News(); 
            news.setId(id); 
            news.setTitle(title); 
            news.setContent(content); 
            news.setPublishDate(publishDate); 
            news.setCommentCount(commentCount); 
            newsList.add(news); 
        } while (cursor.moveToNext()); 
    }  

这大概就是传统查询数据方式的用法了,总体来看,用法确实非常不友好,尤其是query()方法冗长的参数列表,即使我们用不到那些参数,也必须要传入许多个null。另外,查询到的数据还都只是封装到了一个Cursor对象中,我们还需要将数据一一取出然后再set到实体类对象当中。麻烦吗?可能你觉得不麻烦,因为你已经习惯了这种用法。但是习惯总是可以改变的,也许当你体验了LitePal中查询API给我们带来的便利之后,就会有了新的看法了,那么下面我们就一起来体验一下LitePal的查询艺术。
使用LitePal查询数据
LitePal在查询方面提供了非常丰富的API,功能多种多样,基本上已经能够满足我们平时所有的查询需求了。不仅如此,LitePal在查询API的设计方面也是非常用心,摒弃了原生query()方法中繁琐的参数列表,而是改用了一种更为灵巧的方式——连缀查询。除此之外,LitePal查询的结果也不再返回Cursor对象,然后再由开发者自己去逐个取出,而是直接返回封装好的对象。这些改变都使得查询数据变得更加简单,也更加合理,那么下面我们就来完整地学习一下LitePal中查询数据的所有用法。
简单查询

比如说现在我们想实现一个最简单的功能,查询news表中id为1的这条记录,使用LitePal就可以这样写:News news = DataSupport.find(News.class, 1);
News firstNews = DataSupport.findFirst(News.class); //查询第一条数据方法
News lastNews = DataSupport.findLast(News.class); //查询最后一条数据方法
但是LitePal给我们提供了一个更简便的方法——findAll()。这个方法的用法和find()方法是非常类似的,只不过它可以指定多个id查询多条数据方法

List<News> newsList = DataSupport.findAll(News.class, 1, 3, 5, 7);

findAll()方法也是接收数组参数的,所以说同样的功能你也可以这样写List<News> allNews = DataSupport.findAll(News.class);
当然了,LitePal给我们提供的查询功能还远远不只这些,好戏还在后头。相信大家现在也已经发现了,我们目前的查询功能都是基于id来进行查询的,并不能随意地指定查询条件。那么怎样才能指定查询条件呢?让我们回想一下传统情况应该怎么做,query()方法中接收七个参数,其中第三和第四个参数就是用于指定查询条件的,然后其它几个参数都填null就可以了。但是呢,前面我们已经痛批过了这种写法,因为冗长的参数列表太过繁琐,那么LitePal又是怎么解决这个问题的呢?我们现在就来学习一下。

为了避免冗长的参数列表,LitePal采用了一种非常巧妙的解决方案,叫作连缀查询,这种查询很灵活,可以根据我们实际的查询需求来动态配置查询参数。 那这里举个简单的例子,比如我们想查询news表中所有评论数大于零的新闻,就可以这样写:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

List<News> newsList = DataSupport.where("commentcount > ?", "0").find(News.class);  

可以看到,首先是调用了DataSupport的where()方法,在这里指定了查询条件。where()方法接收任意个字符串参数,其中第一个参数用于进行条件约束,从第二个参数开始,都是用于替换第一个参数中的占位符的。那这个where()方法就对应了一条SQL语句中的where部分。

接着我们在where()方法之后直接连缀了一个find()方法,然后在这里指定一个泛型类,表示用于查询哪张表。那么上面的一段代码,查询出的结果和如下SQL语句是相同的:
但是这样会将news表中所有的列都查询出来,也许你并不需要那么多的数据,而是只要title和content这两列数据。那么也很简单,我们只要再增加一个连缀就行了,如下所示:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0").find(News.class);  

可以看到,这里我们新增了一个select()方法,这个方法接收任意个字符串参数,每个参数要求对应一个列名,这样就只会把相应列的数据查询出来了,因此select()方法对应了一条SQL语句中的select部分。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:
很好玩吧?不过这还不算完呢,我们还可以继续连缀更多的东西。比如说,我希望将查询出的新闻按照发布的时间倒序排列,即最新发布的新闻放在最前面,那就可以这样写:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").find(News.class);  

order()方法中接收一个字符串参数,用于指定查询出的结果按照哪一列进行排序,asc表示正序排序,desc表示倒序排序,因此order()方法对应了一条SQL语句中的order by部分。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:
[sql] view plain copy
在CODE上查看代码片派生到我的代码片

select title,content from users where commentcount > 0 order by publishdate desc;  

然后呢,也许你并不希望将所有条件匹配的结果一次性全部查询出来,因为这样数据量可能会有点太大了,而是希望只查询出前10条数据,那么使用连缀同样可以轻松解决这个问题,代码如下所示:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").limit(10).find(News.class);  

这里我们又连缀了一个limit()方法,这个方法接收一个整型参数,用于指定查询前几条数据,这里指定成10,意思就是查询所有匹配结果中的前10条数据。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:
[sql] view plain copy
在CODE上查看代码片派生到我的代码片

select title,content from users where commentcount > 0 order by publishdate desc limit 10;  

刚才我们查询到的是所有匹配条件的前10条新闻,那么现在我想对新闻进行分页展示,翻到第二页时,展示第11到第20条新闻,这又该怎么实现呢?没关系,在LitePal的帮助下,这些功能都是十分简单的,只需要再连缀一个偏移量就可以了,如下所示:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").limit(10).offset(10)  
        .find(News.class);  

可以看到,这里我们又添加了一个offset()方法,用于指定查询结果的偏移量,这里指定成10,就表示偏移十个位置,那么原来是查询前10条新闻的,偏移了十个位置之后,就变成了查询第11到第20条新闻了,如果偏移量是20,那就表示查询第21到第30条新闻,以此类推。因此,limit()方法和order()方法共同对应了一条SQL语句中的limit部分。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:
[sql] view plain copy
在CODE上查看代码片派生到我的代码片

select title,content from users where commentcount > 0 order by publishdate desc limit 10,10;  

这大概就是LitePal中连缀查询的所有用法了。看出区别了吧?这种查询的好处就在于,我们可以随意地组合各种查询参数,需要用到的时候就把它们连缀到一起,不需要用到的时候不用指定就可以了。对比一下query()方法中那冗长的参数列表,即使我们用不到那些参数,也必须要传null,是不是明显感觉LitePal中的查询更加人性化?
不过,上述我们的所有用法中,都只能是查询到指定表中的数据而已,关联表中数据是无法查到的,因为LitePal默认的模式就是懒查询,当然这也是推荐的查询方式。那么,如果你真的非常想要一次性将关联表中的数据也一起查询出来,当然也是可以的,LitePal中也支持激进查询的方式,下面我们就来一起看一下。

不知道你有没有发现,刚才我们所学的每一个类型的find()方法,都对应了一个带有isEager参数的方法重载,这个参数相信大家一看就明白是什么意思了,设置成true就表示激进查询,这样就会把关联表中的数据一起查询出来了。

比如说,我们想要查询news表中id为1的新闻,并且把这条新闻所对应的评论也一起查询出来,就可以这样写:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

News news = DataSupport.find(News.class, 1, true);  
List<Comment> commentList = news.getCommentList();  

可以看到,这里并没有什么复杂的用法,也就是在find()方法的最后多加了一个true参数,就表示使用激进查询了。这会将和news表关联的所有表中的数据也一起查出来,那么comment表和news表是多对一的关联,所以使用激进查询一条新闻的时候,那么该新闻所对应的评论也就一起被查询出来了。

激进查询的用法非常简单,就只有这么多,其它find()方法也都是同样的用法,就不再重复介绍了。但是这种查询方式LitePal并不推荐,因为如果一旦关联表中的数据很多,查询速度可能就会非常慢。而且激进查询只能查询出指定表的关联表数据,但是没法继续迭代查询关联表的关联表数据。因此,这里我建议大家还是使用默认的懒加载更加合适,至于如何查询出关联表中的数据,其实只需要在模型类中做一点小修改就可以了。修改News类中的代码,如下所示:
[java] view plain copy
在CODE上查看代码片派生到我的代码片

public class News extends DataSupport{  

    ...  

    public List<Comment> getComments() {  
        return DataSupport.where("news_id = ?", String.valueOf(id)).find(Comment.class);  
    }  

}  

可以看到,我们在News类中添加了一个getComments()方法,而这个方法的内部就是使用了一句连缀查询,查出了当前这条新闻对应的所有评论。改成这种写法之后,我们就可以将关联表数据的查询延迟,当我们需要去获取新闻所对应的评论时,再去调用News的getComments()方法,这时才会去查询关联数据。这种写法会比激进查询更加高效也更加合理。

你可能感兴趣的:(android,数据库)