Android数据库进阶-从SQLite到ORMLite封装

144
作者 CameloeAnthony
2016.11.02 10:41 字数 2778

项目代码:
CameloeAnthony/Ant
博客原地址:Android数据库进阶-从SQLite到ORMLite封装

前言

几乎每一个android项目中,都必不可少的会使用数据库的操作。在此之前我曾写过一篇文章Rxjava+数据库?来用用SqlBrite和SqlDelight吧! ,SqlBrite是对 Android 系统的 SQLiteOpenHelper 的封装,对SQL操作引入了响应式语义 (Rx)(用来在 RxJava 中使用)。在那之后确实也使用过一段时间的SqlBrite,不过可能是本人能力原因 ,在我的业务开发中,SqlBrite使用起来也并没有多么的方便 ,反而对整体的封装起到了一定的阻碍。所以后来也就继续回归使用ORMLite做数据库操作。下面文章还是从基础到封装再到实例讲讲我的项目中的ORMLite是怎么使用的吧。


ORMLite的引入

1 从SQLite到ORM

SQLite是在世界上使用的最多的数据库引擎,并且还是开源的。它实现了无配置,无服务要求的事务数据库引擎。SQLite可以在Mac OS-X, iOS, Android, Linux, 和 Windows上使用。android中使用的正是SQLite。在Android开发中,使用SQLite作为基础部分,想必大家对继承 SQLiteOpenHelper 创建数据库,调用 SQLiteDatabase 的 execSQL() 方法执行 INSERT, UPDATE, DELETE 等语句来更新表的数据,不管你如何执行查询都会返回一个Android 的 SQLite 数据库游标......这一系列概念并不陌生啊 。想必很多人都和我一样,并不想写任何SQL语句,因为一不小心就写错了,而且各种重复的SQL语句写着真的心烦,大大的影响了开发的效率。

我们当然希望不需要再去和复杂的SQL语句打交道,在面向对象的编程中只要像平时操作对象一样操作它就可以了。这就引入了ORM。

ORM是对象关系映射(Object Relational Mapping)的缩写,对象和关系数据是业务实体的两种表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据,ORM实现了对象和关系数据库之间的转换

2 java中ORM的原理

要实现JavaBean的属性到数据库表的字段的映射,任何ORM框架不外乎是读某个配置文件把JavaBean的属性和数据库表的字段自动关联起来,当从数据库Query时,自动把字段的值塞进JavaBean的对应属性里,当做INSERT或UPDATE时,自动把 JavaBean的属性值绑定到SQL语句中。

3 从ORM到ORMLite

ORM框架广泛引用于各种语言中,对于java开发者比较熟悉的有HibernateOrmlite等,Ormlite作为一个Java ORM。支持JDBC连接,Spring以及Android平台。除此之外Android中使用的ORM框架还有GreendaoActiveAndroidSugarORMRealm等。后续项目会考虑使用 Realm,到时候再进行讲解。


ORMLite基础

ORMLite provides a lightweight Object Relational Mapping between Java classes and SQL databases. There are certainly more mature ORMs which provide this functionality including Hibernate and iBatis. However, the author wanted a simple yet powerful wrapper around the JDBC functions, and Hibernate and iBatis are significantly more complicated with many dependencies.

ORMLite 提供了一个轻量级的java对象和数据库的对象关系操作,相比于Hibernate 和iBatis 等成熟的ORM框架的繁重,ORMLite旨在提供一个简单而有效的解决方案。

当然和之前的所有文章一样, 基础部分都回归官方文档。这里会对android使用中的重点的基础部分进行提及并对官网的例子做出改动。官方文档中关于android的使用部分,请点击这里

首先看看封装之前的ORMLite在我项目中的使用步骤:

1 下载ORMLite的jar包

首先去http://ormlite.com/releases/ 下载jar包,对于Android目前版本为:ormlite-android-5.0.jar 和 ormlite-core-5.0.jar ;在我项目中添加的是之前的4.49的jar包。


2 创建实体类,这里利用新闻信息实体NewsItem类
@DatabaseTable(tableName = "tb_news_item")
public class NewsItem implements Serializable{
    @DatabaseField(generatedId = true, columnName = "i_id")
    private int i_id;

    @DatabaseField(columnName = "channelId")
    @SerializedName(value = "channelId")
    private int channelId;

    @DatabaseField(columnName = "id")
    @SerializedName(value = "id", alternate = {"docid", "docId"})
    private int id;


    @DatabaseField(columnName = "title")
    @SerializedName(value = "MetaDataTitle", alternate = {"title", "name"})
    private String title;


    @DatabaseField(columnName = "content")
    @SerializedName(value = "content")
    private String content;

    @DatabaseField(columnName = "type")
    @SerializedName(value = "type", alternate = {"t", "docType"})
    private int type;


    @DatabaseField(columnName = "img", dataType = DataType.SERIALIZABLE)
    @SerializedName(value = "image", alternate = {"ic", "images", "picture", "pic", "img"})
    private ArrayList<String> images;

    private String icon;

    @DatabaseField(columnName = "url")
    @SerializedName(value = "url", alternate = {"link", "docURL","channelUrl"})
    private String url;

    @DatabaseField(columnName = "date")
    @SerializedName(value = "date", alternate = {"PubDate", "time"})
    private String date;

    @DatabaseField(columnName = "source")
    @SerializedName(value = "source")
    private String source;

    @DatabaseField(columnName = "media")
    @SerializedName(value = "media")
    private String media;


    @DatabaseField(columnName = "relPhotos")
    @SerializedName(value = "RelPhotos")
    private String relPhotos;

    @DatabaseField(columnName = "isTopic")
    private boolean isTopic = false;

    @DatabaseField(columnName = "isStar")
    private boolean isStar = false;

    @DatabaseField(columnName = "channelItems", dataType = DataType.SERIALIZABLE)
    @SerializedName(value = "channelItems")
    private ArrayList<NewsItem> newsItems;


    @DatabaseField(columnName = "parentChannelType")
    private int parentChannelType;


//......       get set 方法
}

除了通过@SerializedName(value = "xxx")支持Gson序列化。

这里有几个需要注意的地方:

  • 1 通过@DatabaseTable(tableName = "tb_news_item")指定了表名为tb_news_item.
  • 2 通过@DatabaseField(generatedId = true, columnName = "i_id")指定id字段为自动生成,并且名为i_id。
  • 3 对于ArrayList<String> 序列化对象的的支持,需要使用dataType = DataType.SERIALIZABLE .可以通过DataType控制数据库表中字段类型。

3 继承OrmLiteSqliteOpenHelper类

我们需要通过继承OrmLiteSqliteOpenHelper类来编写自己的数据库帮助类,
需要实现的方法为onCreate(SQLiteDatabase sqliteDatabase, ConnectionSource connectionSource) 以及onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion)。分别对应着数据库第一次创建 以及版本更新的时候调用的方法。
比如针对上面的NewsItem类,

/**
 * Database helper class used to manage the creation and upgrading of your database. This class also usually provides
 * the DAOs used by the other classes.
 */
public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
    private Context mContext;

    // the DAO object we use to access the NewsItem table
    private Dao<NewsItem, Integer> simpleDao = null;

    // name of the database file for your application -- change to something appropriate for your app
    private static final String DATABASE_NAME = "dbtest.db";
    // any time you make changes to your database objects, you may have to increase the database version
    private static final int DATABASE_VERSION = 1;


    /**
     * 
     * Returns the Database Access Object (DAO) for our NewsItem class. It will create it or just give the cached
     * value.
     */
    public Dao<NewsItem, Integer> getNewsItemDao() throws SQLException {
        if (simpleDao == null) {
            simpleDao = getDao(NewsItem.class);
        }
        return simpleDao;
    }



    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);

        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
       try {
            TableUtils.createTable(connectionSource, NewsItem.class);
       } catch (SQLException e) {
           e.printStackTrace();
       }
    }

    @Override
    public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource,
                          int oldVersion, int newVersion) {
       try {
          TableUtils.dropTable(connectionSource, NewsItem.class, true);
       } catch (SQLException e) {
           e.printStackTrace();
       }                          
    }

    /**
     * 释放资源
     */
    @Override
    public void close() {
        super.close();

//        instance = null;
        mContext = null;
    }

}

可以看到的是NewsItem就是我们的实体类,通过TableUtils.createTable(connectionSource, NewsItem.class);onCreate方法中完成了对象表的创建。


继承OrmLiteSqliteOpenHelper类,其实是间接继承了SQLiteOpenHelper
4 提取DAO类并封装其中的方法


可以通过上面的这张图看到的是当前DatabaseHelper类除了完成onCreate中创建以及onUpgrade中更新以外,也提供了DAO
java的设计模式中这会经常遇到,我们需要将数据库的操作独立到数据库连接类(也就是Data Access Objects 简称DAO )。也就是说每个DAO类提供增删查改等操作。每个实体对象比如说上面的NewsItem.class 都对应着一个DAO类。OrmLiteSqliteOpenHelper 类正为我们提供了getDao方法,也就有了上面图片中的操作。

public class NewsItemDaoOld {
    private MyApplication myApplication;


    public NewsItemDaoOld(MyApplication myApplication) {
        this.myApplication = myApplication;

    }

    public void add(NewsItem t) {
        try {
            myApplication.dbHelper.getNewsItemDao().create(t);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


    public void delete(NewsItem t) {
        try {
            myApplication.dbHelper.getNewsItemDao().delete(t);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void update(NewsItem t) {
        try {
            myApplication.dbHelper.getNewsItemDao().update(t);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public List<NewsItem> all() {
        try {
            return myApplication.dbHelper.getNewsItemDao().queryForAll();
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    public List<NewsItem> queryByColumn(String columnName, Object columnValue) {
        try {
            QueryBuilder builder =  myApplication.dbHelper.getNewsItemDao().queryBuilder();
            builder.where().eq(columnName, columnValue);
            return builder.query();
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }
}

为了区分后面封装后的NewsItemDao.class ,我这里命名为NewsItemDaoOld .class

这里可以看到在构造方法中得到了通过Application的继承类MyApplication 。然后获得DatabaseHelper 的实例,也就能够通过DatabaseHelper 中的getNewsItemDao()方法得到其中的NewsItemDaoOld()的实例。这里也可以发现ORMLite中各种方法的便利,增删改查都在NewsItemDaoOld .class中完成。

5 利用DAO类完成增删改查

到这里就可以在任何类中使用NewsItemDaoOld .class进行数据库的操作了。你同时也会发现操作数据库的代码变得异常简洁。导出数据库数据,通过SQLiteExpert查看数据,可以看到,新闻数据成功添加。


ORMLite还提供了一些基类ORMLiteBaseActivity,ORMLiteBaseService之类的,便于数据库操作的,这里不做考虑,毕竟项目中很大可能自己也需要继承自己的BaseActvity之类的。


ORMLite封装

封装之前我们先来总结需求和问题

上面的代码总的来说,封装到我的代码和业务逻辑中,有几个需要改进的地方。

1 对于NewsItemDao我们需要在DatabaseHelper中获取,在自己封装的NewsItemDaoOld 中进行数据操作。那么当一个app的表多了之后,我希望提供统一的增删改查操作,也就需要一个BaseDao完成一些基本的操作。以后的类统一命名为xxDao,并且继承自BaseDao。并且将DatabaseHelper中的getDAO获取各种数据的DAO的操作移到BaseDao中。

2 对表进行统一的管理。

3 将DataBaseHelper添加到DataManager中,按照以前的思路,将DataManager作为唯一的数据入口。


4 结合Dagger2进行全局的DataBaseHelper对象的管理
对于数据库操作需要有一个关注点,就是我们需要确保整个app中不同页面的数据库链接和操作应当都是一个 ,也就是说,不能让不同的线程同时操作数据库,这样肯定会存在数据库的紊乱和异常。对于官网提供的例子,建议通过继承OrmLiteBaseActivity作为Activity的基类来使用OpenHelperManager(将会在第一次链接数据库的时候创建,每次操作数据库的时候使用,在释放的时候进行关闭)。然后通过getHelper()类来获取OpenHelperManager进行操作。


ORMLite提供的基类

当然也可以在自己的BaseActivity中封装下面的操作。

private DatabaseHelper databaseHelper = null;

@Override
protected void onDestroy() {
    super.onDestroy();
    if (databaseHelper != null) {
        OpenHelperManager.releaseHelper();
        databaseHelper = null;
    }
}

private DBHelper getHelper() {
    if (databaseHelper == null) {
        databaseHelper =
            OpenHelperManager.getHelper(this, DatabaseHelper.class);
    }
    return databaseHelper;
}

然后这里由于我引入了Dagger2提供的全局单例对象我也就采取了自己的做法。我们全局使用的是同一个DatabaseHelper对象,也就避免了上方的操作。对Dagger2不理解的童鞋,请查看我之前的文章Google官方MVP+Dagger2架构详解

解决这四个问题,那么一起来看看我的思路吧:

public class BaseDao<T> {
    protected Class<T> clazz;
    protected Dao<T, Integer> daoOpe;


    /**
     * get dao class through {@link com.anthony.app.common.data.database.DatabaseHelper}
     *
     * @param mApplication using this to get instance of  DatabaseHelper
     */
    public BaseDao(MyApplication mApplication) {
        Class clazz = getClass();

        while (clazz != Object.class) {
            Type t = clazz.getGenericSuperclass();
            if (t instanceof ParameterizedType) {
                Type[] args = ((ParameterizedType) t).getActualTypeArguments();
                if (args[0] instanceof Class) {
                    this.clazz = (Class<T>) args[0];
                    break;
                }
            }
            clazz = clazz.getSuperclass();
        }

        try {
            if (mApplication.dbHelper == null) {
                throw new RuntimeException("No DbHelper Found!");
            }
            daoOpe = mApplication.dbHelper.getDao(this.clazz);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void add(T t) {
        try {
            daoOpe.create(t);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void delete(T t) {
        try {
            daoOpe.delete(t);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void update(T t) {
        try {
            daoOpe.update(t);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public List<T> all() {
        try {
            return daoOpe.queryForAll();
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    public List<T> queryByColumn(String columnName, Object columnValue) {
        try {
            QueryBuilder builder = daoOpe.queryBuilder();
            builder.where().eq(columnName, columnValue);
            return builder.query();
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

}
针对问题1

BaseDao 的构造方法中得到了MyApplication 实例,也就能够得到DatabaseHelper对象。我们也能通过反射获取DAO子类的泛型,从而能在当前的BaseDao中通过daoOpe = mApplication.dbHelper.getDao(this.clazz);获取到ORMLite中的Dao对象。从而可以进行增删改查。

public class NewsItemDao extends BaseDao<NewsItem> {

    public NewsItemDao(MyApplication mApplication) {
        super(mApplication);
    }

    public List<NewsItem> queryLatest(int channelId, long limit) {
        try {
            QueryBuilder builder = daoOpe.queryBuilder();
            builder.limit(limit)
                    .where()
                    .eq("channel_id", channelId)
                    .and()
                    .eq("isTopic", false);
            return builder.query();
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    public List<NewsItem> queryTopic(int channelId) {
        try {
            QueryBuilder builder = daoOpe.queryBuilder();
            builder.where()
                    .eq("channel_id", channelId)
                    .and()
                    .eq("isTopic", true);
            return builder.query();
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

}

这里NewsItem的Dao类NewsItemDao继承了BaseDao,并且添加两个查询方法对数据库对象进行操作。

针对问题2

这里为了统一管理,我将所有的表的类名写到array.xml文件中,从而可以在DatabaseHelper中进行获取和统一创建。



针对上面的问题3 和4 结合在一起进行讲解

将DatabaseHelper封装到DataManager中,让DataManager作为数据的入口。


在ApplicationModule中提供几个DAO类的实例。也就是在全局中都是用的是这些Dao类的实例。也就解决了问题4中提及的数据库操作的问题。



同时在ApplicationComponent中进行实例的暴露。这样我们可以在任何进行了注入的类中使用这三个实例了 。这三个实例的初始化已经在上面这张图中进行了说明。


最后在Application的实现类中进行DatabaseHelper的实例对象的获取,大功告成。


项目效果和源码

项目代码:
CameloeAnthony/Ant

这里结合新闻列表,提供了一个导出数据库的操作,数据库可以在
Android - data - com.anthony.app - cache中找到对应的数据库表。利用SQLiteExpert进行数据库的查看。


这里可以看到我这里有三个表,但是目前并没有对channel表和offline_res表进行添加操作。tb_news_item是我们之前在实体类中定义的表名。里面有对应的数据。


参考资料