Android数据库操作(二)——建表、升级表、表关联

前言

本文参考转发摘自:郭霖博客http://blog.csdn.net/guolin_blog?viewmode=contents 强烈建议进入原博客查看学习

1、建表

  • 操作数据库的第一步当然是创建表了,传统创建表的方法相信大多数人都知道,那么今天我除了会展示传统的建表方法之外,还会讲解LitePal这个框架的基本用法,并使用它来完成同样的建表操作,让大家体会到使用框架来操作数据库的魅力。
  • 那么先来简单介绍一下吧,LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发时最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表、増删改查的操作。并且LitePal很“轻”,jar包只有100k不到,而且近乎零配置,这一点和hibernate这类的框架有很大区别。目前LitePal的源码已经托管到了GitHub上,地址是https://github.com/LitePalFramework/LitePal

1.1 传统的建表方式

  • 为了方便我们对数据库表进行管理,Android本身就提供了一个帮助类: SQLiteOpenHelper。这个类集创建和升级数据库于一身,并且自动管理了数据库版本,算是一个非常好用的工具。

  • 那我们现在就来试试 SQLiteOpenHelper 的用法吧。首先你要知道 SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是 onCreate()onUpgrade() ,我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去 实现创建、升级数据库的逻辑

  • 新建一个MySQLiteHelper类并让它 继承SQLiteOpenHelper

public class MySQLiteHelper extends SQLiteOpenHelper {

    public MySQLiteHelper(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 MySQLiteHelper extends SQLiteOpenHelper {

    //建news表语句
    public static final String NEWS_TABLE = "create table news("
            + "id integer primary key autoincrement,"
            + "title text,"
            + "content text,"
            + "publishdata integer,"
            + "commentcount integer)";

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

    //实现创建的逻辑
    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建名为 “news” 的表
        db.execSQL(NEWS_TABLE);
    }

    //实现升级数据库的逻辑
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

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

  • 接着,我们只需要获取到SQLiteDatabase的实例,数据库表就会自动创建了,如下所示:
SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
// "demo.db" 就是数据库名
        
 SQLiteDatabase db = dbHelper.getWritableDatabase();
  • 感觉很简单很方便是吗?那你就太容易满足了,下面我们就来学习一下LitePal的基本用法,看一看使用这个框架是如何实现同样的功能的。

1.2 使用LitePal建表

  • 虽说LitePal宣称是近乎零配置,但也只是“近乎”而已,它还是需要进行一些简单配置才可以使用的,那么我们第一步就先快速学习一下LitePal的配置方法。

1.2.1 快速配置

1. 引入Jar包或源码

  • 首先我们需要将LitePal的jar包引入到项目当中,可以点击这里查看LitePal的最新版本,选择你需要的下载即可。下载好了jar包之后,把它复制到项目的libs目录中就算是引入成功了,如下图所示:
    引入jar包
  • 如果不想用jar包的话,也可以把LitePal的源码下载下来,然后作为一个library库导入到Eclipse当中,再让我们的项目去引用这个library库就可以了。

2. 配置litepal.xml

  • 接着在项目的 assets目录 (assets在main下)下面新建一个litepal.xml文件,并将以下代码拷贝进去:
<?xml version="1.0" encoding="utf-8"?>  
<litepal>  
    <dbname value="demo" ></dbname>  
  
    <version value="1" ></version>  
  
    <list>  
    </list>  
</litepal>  
  • 配置文件相当简单,<dbname>用于设定数据库的名字,<version>用于设定数据库的版本号,<list>用于设定所有的映射模型,我们稍后就会用到。

3. 配置LitePalApplication

  • 由于操作数据库时需要用到 Context,而我们显然不希望在每个接口中都去传一遍这个参数,那样操作数据库就显得太繁琐了。因此,LitePal使用了一个方法来简化掉Context这个参数,只需要在 AndroidManifest.xml 中配置一下 LitePalApplication,所有的数据库操作就都不用再传Context了,如下所示:
<manifest>  
    <application  
        android:name="org.litepal.LitePalApplication"  
        ...  
    >  
    ...  
    </application>  
</manifest>  
  • 当然,有些程序可能会有自己的Application,并在这里配置过了。比如说有一个MyApplication,如下所示:
<manifest>  
    <application  
        android:name="com.example.MyApplication"  
        ...  
    >  
    ...  
    </application>  
</manifest>  
  • 没有关系,这时只需要修改一下MyApplication的继承结构,让它不要直接继承Application类,而是继承LitePalApplication类,就可以使用一切都能正常工作了,代码如下所示:
public class MyApplication extends LitePalApplication {  
    ...  
}  
  • 但是,有些程序可能会遇到一些更加极端的情况,比如说MyApplication需要继承另外一个AnotherApplication,并且这个AnotherApplication还是在jar包当中的,不能修改它的代码。这种情况应该算是比较少见了,但是如果你遇到了的话也不用急,仍然是有解释方案的。你可以把LitePal的源码下载下来,然后把src目录下的所有代码直接拷贝到你项目的src目录下面,接着打开LitePalApplication类,将它的继承结构改成继承自AnotherApplication,再让MyApplication继承自LitePalApplication,这样所有的Application就都可以在一起正常工作了。
  • 仅仅三步,我们就将所有的配置工作全部完成了,并且这是一件一本万利的事情,自此以后,你就可以开心地体验LitePal提供的各种便利了,就让我们从建表开始吧。

1.2.2 LitePal建表

  • 前面在介绍的时候已经说了,LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而我们使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。

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

  • 那么接下来我们就看一看LitePal中是如何建表的吧。根据对象关系映射模式的理念,每一张表都应该对应一个 模型(Model),也就是说,如果我们想要建一张news表,就应该有一个对应的News模型类。新建一个 News类,如下所示:
package com.example.databasetest.model;  
public class News {  
}  
  • 然后,表中的每一列其实就是对应了模型类中的一个字段,比如news表中有id、title、content、publishdate、commentcount这几个列,那么在News类中就也应该有这几个字段,代码如下所示:
public class News {  
      
    private int id;  
      
    private String title;  
      
    private String content;  
      
    private Date publishDate;  
      
    private int commentCount;  
      
    // 自动生成get、set方法  
    ...  
}  

其中id这个字段可写可不写,因为即使不写这个字段,LitePal也会在表中自动生成一个id列,毕竟每张表都一定要有主键的嘛。

  • 这里要特别说明一下,LitePal的映射规则是非常轻量级的,不像一些其它的数据库框架,需要为每个模型类单独配置一个映射关系的XML,LitePal的所有映射都是自动完成的。根据LitePal的数据类型支持,可以进行对象关系映射的数据类型一共有8种,int、short、long、float、double、boolean、String和Date。只要是声明成这8种数据类型的字段都会被自动映射到数据库表中,并不需要进行任何额外的配置。

  • 现在模型类已经建好了,我们还差最后一步,就是将它配置到映射列表当中。编辑assets目录下的litepal.xml文件,在<list>标签中加入News模型类的声明:

<?xml version="1.0" encoding="utf-8"?>  
<litepal>  
    <dbname value="demo" ></dbname>  
  
    <version value="1" ></version>  
  
    <list>  
        <mapping class="com.example.databasetest.model.News"></mapping>  
    </list>  
</litepal>  

注意这里一定要填入News类的完整类名。

  • OK,这样所有的工作就都已经完成了,现在只要你对数据库有任何的操作,news表就会被自动创建出来。比如说LitePal提供了一个便捷的方法来获取到SQLiteDatabase的实例,如下所示:
 //LitePal 获取实例
 SQLiteDatabase db = Connector.getDatabase();
  • 调用一下上述代码,news表就应该已经创建成功了。我们使用SQLite命令来查看一下,打开demo.db数据库,输入.table命令,结果如下图所示:


    20140907222357271.png
  • 可以看到,news表已经存在了。另外两张android_metadata和table_schema表是自动生成的,我们不用理。接下来我们还可以再查询一下news表的建表语句,如下图所示:


    20140907222630369.png
  • 这就是LitePal根据News类中的字段自动帮我们生成的建表语句,由此也说明,建表操作已经成功完成了。

2、升级表

  • 创建表只是数据库操作中最基本的一步而已,我们在一开始创建的表结构,随着需求的变更,到了后期是极有可能需要修改的。因此,升级表的操作对于任何一个项目也是至关重要的,那么今天我们就一起来学习一下,在Android传统开发当中升级表的方式,以及使用LitePal来进行升级表操作的用法。

2.1 传统的升级表方式

  • 上一节文章中我们借助MySQLiteHelper已经创建好了news这张表,这也是demo.db这个数据库的第一个版本。然而,现在需求发生了变更,我们的软件除了能看新闻之外,还应该允许用户评论,所以这时就需要对数据库进行升级,添加一个comment表。

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

public class MySQLiteHelper extends SQLiteOpenHelper {

    //建news表语句
    public static final String NEWS_TABLE = "create table news("
            + "id integer primary key autoincrement,"
            + "title text,"
            + "content text,"
            + "publishdata integer,"
            + "commentcount integer)";

    //建comment表语句
    public static final String COMMENT_TABLE = "create table comment("
            + "id integer primary key autoincrement,"
            + "content text)";

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

    //实现创建的逻辑
    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建名为 “news” 的表
        db.execSQL(NEWS_TABLE);

        //创建名为 “comment” 的表
        db.execSQL(COMMENT_TABLE);
    }

    //实现升级数据库的逻辑
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

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

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

public class MySQLiteHelper extends SQLiteOpenHelper {

    .....

    //实现创建的逻辑
    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建名为 “news” 的表
        db.execSQL(NEWS_TABLE);

        //创建名为 “comment” 的表
        db.execSQL(COMMENT_TABLE);
    }

    //实现升级数据库的逻辑
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        //删除 “news” 表
        db.execSQL("drop table if exists news");
        
        //调用onCreate重新建表
        onCreate(db);
    }
}

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

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

    ......

    //实现创建的逻辑
    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建名为 “news” 的表
        db.execSQL(NEWS_TABLE);

        //创建名为 “comment” 的表
        db.execSQL(COMMENT_TABLE);
    }

    //实现升级数据库的逻辑
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                db.execSQL(COMMENT_TABLE);
            default:
        }
    }
}
  • 可以看到,这里在 onUpgrade() 方法中加入了一个switch判断,如果oldVersion等于1,就再创建一个comment表。现在只需要调用如下代码,表就可以得到创建或升级了:
SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 2);
        
 SQLiteDatabase db = dbHelper.getWritableDatabase();

这里我们将版本号加1,如果用户是从旧版本升级过来的,就会新增一个comment表,而如果用户是直接安装的新版本,就会在onCreate()方法中把两个表一起创建了。

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

    ......

    //建comment表语句
    public static final String COMMENT_TABLE = "create table comment("
            + "id integer primary key autoincrement,"
            + "content text,"
            + "publishdate integer)";   //增加字段

    ......

    //实现升级数据库的逻辑
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                db.execSQL(COMMENT_TABLE);
                //case中别用break
            case 2:
                //增加字段
                db.execSQL("alter table comment add column publishdate integer");
                //case中别用break
            default:
            break;//这这里break,防止从1~3的情况()
        }
    }
}
  • 可以看到,在建表语句当中我们新增了publishdate这一列,这样当执行onCreate()方法去创建表的时候,comment表中就会有这一列了。那么如果是从旧版本升级过来的呢?也没有问题,我们在onUpgrade()方法中已经把升级逻辑都处理好了,当oldVersion等于2的时候,会执行alter语句来添加publishdate这一列。现在调用以下代码来创建或升级数据库:
SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 3);

  SQLiteDatabase db = dbHelper.getWritableDatabase();

将数据库版本号设置成3,这样就可以保证数据库中的表又是最新的了。

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

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

2.2 使用LitePal升级表

  • 通过上文的学习,我们已经知道LitePal是一款ORM模式的框架了,已经熟悉创建表流程的你,相信对于升级表也一定会轻车熟路的。那么为了模仿传统升级表方式中的需求,现在我们也需要创建一张comment表。第一步该怎么办呢?相信你也许早就已经猜到了,那当然是先创建一个Comment类了,如下所示:
package com.example.databasetest.model;  
public class Comment {  
      
    private int id;  
      
    private String content;  
      
    // 自动生成get、set方法   
    ...  
}  

OK,Comment类中有id和content这两个字段,也就意味着comment表中会有id和content这两列。

  • 接着修改litepal.xml中的配置,在映射列表中新增Comment类,并将版本号加1,如下所示:
<?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>  
  • 没错,就是这么简单,仅仅两步,升级的操作就已经完成了,现在我们只需要操作一下数据库,comment表就会自动生成了,如下所示:
//LitePal 获取实例
 SQLiteDatabase db = Connector.getDatabase();
  • 那么我们还是通过.table命令来查看一下结果,如下图所示:


    20140914155958796.png
  • OK,comment表已经出来了,那么再通过pragma命令来查看一下它的表结构吧:


    20140914160140852.png

没有问题,comment表中目前有id和content这两列,和Comment模型类中的字段是保持一致的。

  • 那么现在又来了新的需求,需要在 comment表中添加一个publishdate列,该怎么办呢?不用怀疑,跟着你的直觉走,相信你已经猜到应该在Comment类中添加这样一个字段了吧,如下所示:
public class Comment {  
      
    private int id;  
      
    private String content;  
      
    private Date publishDate;  
      
    // 自动生成get、set方法   
    ...  
}  
  • 然后呢?剩下的操作已经非常简单了,只需要在litepal.xml中对版本号加1就行了,如下所示:
<litepal>  
    <dbname value="demo" ></dbname>  
  
    <version value="3" ></version>  
    ...  
</litepal>  
  • 这样当我们下一次操作数据库的时候,publishdate列就应该会自动添加到comment表中。调用Connector.getDatabase()方法,然后重新查询comment表结构,如下所示:


    20140914161913495.png

可以看到,publishdate这一列确实已经成功添加到comment表中了。

2.3 两种方式总结

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

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

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

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

  • 另外,如果你想删除某一张表的话,操作也很简单,在litepal.xml中的映射列表中将相应的类删除,表自然也就不存在了。其它的一些升级操作也都是类似的,相信你已经能举一反三,这里就不再赘述了。

3、建立表关联

3.1 关联关系的基础知识

  • 喜欢把所有的代码都写在一个类里的程序员肯定是个新手。没错,任何一个像样的程序都不可能仅仅只有一个类的,同样地,任何一个像样的数据库也不可能仅仅只有一张表。我们都知道,在面向对象的编程语言中,多个类之间可以相互关联引用,共同完成某项功能。那么在数据库当中,多个表之间可以相互关联吗?当然可以!只不过表与表之间的关联关系要比对象之间的关联关系复杂一些,也更加难懂,但是作为数据库的基本功,还是应该了解清楚的,那么我们就先来学习一下数据库表关联的基础知识。

  • 表与表之间的关联关系一共有三种类型,一对一、多对一、和多对多,下面我们分别对这三种类型展开进行讨论。

一对一

  • 表示两个表中的数据必须是一一对应的关系。这种场景其实并不是很常见,我们还是通过例子来直观地体会一下,例子仍然是在之前文章的基础上展开的。

  • 现在我们已经创建好了news这张表,里面主要记录了新闻的标题和内容,那么除了标题和内容之外,有些新闻还可能带有一些导语和摘要,我们把这两个字段放在一张introduction表中,作为新闻的简介。那么很显然,news表和introduction表就是一对一的关系了,因为一条新闻只能对应一个简介,一个简介也只能属于一条新闻。它们之间的对应关系大概如下图描述的一样:


    一对一

可以看到,News1对应了Introduction2,News2对应了Introduction3,News3对应了Introduction1,但不管怎么样,它们都是一对一的关系。

  • 那么这种一对一的关系,在编程语言中该怎么体现出来呢?相信熟悉面向对象设计的你,一定很轻松就能想出来吧,只需要在News类中持有一个Introduction类的引用,然后在Introduction类中也持有一个News类的引用,这样它们之间自然就是一对一的关系了。

  • 没错,对象之间的一对一关系非常简单易懂,那么难点就在于,如何在数据库表中建立这样的一对一关系了。由于数据库并不像面向对象的语言一样支持相互引用,如果想让两张表之间建立一对一的关系,一般就只能通过外键的方式来实现了。因此,一对一关系的表结构就可以这样设计:


    一对一表结构
  • 请注意,introduction表中有一个news_id列,这是一个外键列,里面应该存放一个具体的新闻id,这样一条introduction就能对应一条news,也就实现一对一的关系了,如下图所示:


  • 由此我们就能够看出,id为1的introduction对应着id为2的news,id为2的introduction对应着id为3的news,id为3的introduction对应着id为1的news。需要注意的是,一对一的关系并没有强制要求外键必须加在哪一张表上,你可以在introduction表中加一个news_id作为外键,也可以在news表中加一个introduction_id作为外键,不管使用哪一种,都可以表示出它们是一对一的关联关系。

多对一

  • 表示一张表中的数据可以对应另一张表中的多条数据。这种场景比起一对一关系就要常见太多了,在我们平时的开发工作中多对一关系真的是比比皆是。比如说现在我们的数据库中有一个news表,还有一个comment表,它们两个之间就是典型的多对一关系,一条新闻可以有很多条评论,但是一条评论只能是属于一条新闻的。它们的关系如下图所示:


    多对一
  • 而这种多对一的关系在编程语言中是非常容易体现出来的,比如Java中就有专门集合类,如List、Set等,使用它们的话就能轻松简单地在对象之间建立多对一的关系,我们稍后就会看到。那么,这里的难点仍然是在数据库表中如何建立这样的多对一关系。现在说难点其实已经不难了,因为前面我们已经学会了一对一关系的建立方法,而多对一也是类似的。没错,数据库表中多对一的关系仍然是通过外键来建立的,只不过一对一的时候外键加在哪一张表上都可以,但多对一的时候关键必须要加在多方的表中。因此,多对一关系的表结构就可以这样设计:

    表结构

  • 在comment表中有一个news_id列,这是一个外键列,里面应该存放一个具体的新闻id,并且允许多条comment都存放同一个新闻id,这样一条评论就只能对应一条新闻,但一条新闻却可以有多条评论,也就实现多对一的关系了,如下图所示:


  • 由此我们就可以看出,id为1、2、3的三条评论是属于第一条新闻的,而id为4、5的两条评论是属于第二条新闻的。

多对多

  • 表示两张关联表中的数据都可以对应另一张表中的多条数据。这种场景也不算是很常见,但比一对一关系要稍微更加常用一些。举个例子,我们都知道新闻网站是会将新闻进行种类划分的,这样用户就可以选择自己喜欢的那一类新闻进行浏览,比如说网易新闻中就会有头条、科技、娱乐、手机等等种类。每个种类下面当然都会有许多条新闻,而一条新闻也可能是属于多个种类的,比如iPhone6发布的新闻既可以属于手机种类,也可以属于科技种类,甚至还可以上头条。因此,新闻和种类之间就是一种多对多的关系,如下图所示:


    多对多
  • 可以看到,News1是属于Category1的,而News2和News3都是既属于Category1也属于Category2,如此复杂的关联关系该如何表示呢?在面向对象的编程语言中一切都是那么的简单,只需要在News类中使用集合类声明拥有多个Category,然后在Category类中也使用集合类声明拥有多个News就可以了,我们稍后就会看到。而难点仍然是留在了数据库上,两张表之间如何建立多对多的关联关系呢,还是用外键吗?肯定不行了,多对多的情况只能是借助中间表来完成了。也就是说,我们需要多建立一张表,这张表没什么其它作用,就是为了存放news表和category表之间的关联关系的,如下图所示:


    表结构
  • 注意这里我们建立一张名为category_news的中间表,中间表的命名并没有什么强制性的约束,但一个良好的命名规范可以让你一眼就明白这张表是用来做什么的。中间表里面只有两列,而且也只需要有两列,分别是news表的外键和category表的外键,在这里存放新闻和种类相应的id,就可以让它们之间建立关联关系了,如下图所示:


  • 由此我们就可以看出,第一条新闻是属于第一个种类的,而第二和第三条新闻,则既属于第一个种类,也属于第二个种类。反过来也可以这样看,第一个种类下面有第一、第二、第三这三条新闻,而第二个种类下面只有第二、第三这两条新闻。不管怎么看,多对多的关系都是成立的。

  • 好了,三种关联关系都讲完了,那我们来简单总结一下吧。虽说上面介绍了花了很大的篇幅讲解数据库的表关联知识,但其实最后的结论是非常简单的,大家可以当成口诀一样背下来。即一对一关联的实现方式是用外键,多对一关联的实现方式也是用外键,多对多关联的实现方式是用中间表。记下了这个口诀,在很多数据库设计的时候,你都可以发挥得更加游刃有余。

3.2 使用LitePal建立表关联

  • 虽说口诀就是这个样子,但牵扯到表关联的时候毕竟增加了建表的难度,建表语句会更加复杂,你也需要格外地小心以防止出现什么错误。因此,使用LitePal来自动建立表关联又是一个非常不错的选择,我们不需要关心什么外键、中间表等实现的细节,只需要在对象中声明好它们相互之间的引用关系,LitePal就会自动在数据库表之间建立好相应的关联关系了,下面我们就来尝试一下吧。

  • 首先确定一下一共涉及到了哪些实体类,News和Comment,这两个类我们在前两篇文章中就已经建好了,然后还需要有Introduction和Category这两个类,新建Introduction类,代码如下所示:

public class Introduction {  
      
    private int id;  
      
    private String guide;  
      
    private String digest;  
      
    // 自动生成get、set方法
    ......  
}  
  • 接着新建Category类,代码如下所示:
public class Category {  
      
    private int id;  
      
    private String name;  
      
    // 自动生成get、set方法 
    ...... 
}  
  • 现在四个类都已经建好了,但目前它们都还是各自独立的,互相之间没有任何联系,那么我们现在就开始用极为简单易懂的方式来给它们建立关联吧。

a、一对一关系:

首先,News和Introduction是一对一的关系,那就可以在News类中添加如下引用:

public class News {  
    ...  
    //News 和 Intorduction 是一对一的关系
    private Introduction introduction;  
      
    // 自动生成get、set方法  
}  
  • 就是这么简单,在News类中可以得到一个对应的Introduction的实例,那么它们之间就是 一对一关系 了。

b、多对一关系

  • 接着 Comment和News是多对一的关系,因此 News中应该包含多个Comment,而Comment中应该只有一个News,所以就可以这样写:
public class News {  
    ......

    //News 和 Intorduction 是一对一的关系
    private Intorduction intorduction;

    //News 和 Comment 是一对多的关系
    private List<Comment> commentList=new ArrayList<>();
      
    // 自动生成get、set方法  
    ......
}  
  • 先使用 一个泛型为Comment的List集合来表示News中包含多个Comment,然后修改Comment类的代码,如下所示:
public class Comment {  
    ......

    //Comment 和 News 是多对一的关系
    private News news;  
      
    // 自动生成get、set方法   
    ......
}  
  • 在Comment类中声明了一个News的实例,这样就清楚地表示出了News中可以包含多个Comment,而Comment中只能有一个News,也就是多对一的关系了。

c、多对多关系

  • 最后 News和Category是多对多的关系,相信聪明的你一定已经知道该怎么写了。News中可以包含多个Category,所以仍然应该使用List集合来表示:
public class News {  
    ......

    //News 和 Introduction是一对一关系
    private Introduction introduction;  
     
    //News 和 Comment是多对一关系
    private List<Comment> commentList = new ArrayList<Comment>();  
      
    //News 和 Category是多对多关系
    private List<Category> categoryList = new ArrayList<Category>();  
      
    // 自动生成get、set方法 
    ...... 
}  
  • 而Category中也可以包含多个News,因此Category类也应该使用相同的写法,如下所示:
public class Category {  
    ......

    //Category 和 News 是多对多关系  
    private List<News> newsList = new ArrayList<News>();  
      
    // 自动生成get、set方法
    ......  
} 
  • 这样就清楚地表达出它们之间是多对多的关联了。

  • 关联关系都声明好了之后,我们只需要将所有的实体类都添加到映射列表当中,并将数据库版本号加1就可以了。修改litepal.xml的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>  
<litepal>  
    <dbname value="demo" ></dbname>  
  
    <version value="4" ></version>  
  
    <list>  
        <mapping class="com.example.databasetest.model.News"></mapping>  
        <mapping class="com.example.databasetest.model.Comment"></mapping>  
        <mapping class="com.example.databasetest.model.Introduction"></mapping>  
        <mapping class="com.example.databasetest.model.Category"></mapping>  
    </list>  
</litepal>  
  • 基本上到这里就可以轻松地说结束了,现在只需要任意操作一下数据库,表之间的关联关系就将会自动建立,比如说调用一下Connector.getDatabase()方法。

  • 下面我们来验证一下吧,输入.table命令查看一下当前数据库中的表,如下所示:


  • OK,news、comment、category、introduction这几张表全都有了,除此之外还有一张category_news中间表。那我们要来一一检查一下了,先查看一下introduction表的结构吧,如下所示:


可以看到,多了一个news_id列,说明introduction表和news表之间的一对一关系已经建立好了。

  • 然后再检查一下comment表的结构,如下所示:


OK,comment表中也有一个news_id的列,那么comment表和news表之间的多对一关系也已经建立好了。

  • 最后检查一下category_news这张中间表的结构,如下所示:


一共只有两列,一列是news_id,一列是category_id,分别对应着两张表的外键,这样news表和category表的多对多关系也建立好了。

  • 借助LitePal的帮助,即使你并不熟悉数据库的表关联设计,只要你会面向对象编程,都可以轻松地将表与表之间的关联建立起来。创建表、升级表、表关联,这就是LitePal在数据库表管理方面给我们带来的巨大便利,相信大家都能体会到它的魅力所在了。

推荐阅读更多精彩内容