从零开始搭建一个主流项目框架(四)—Kotlin+GreenDao3.2

个人博客:haichenyi.com。感谢关注

  本篇介绍android常用的数据库之一GreenDao的简单用法,增删改查。基于前面搭的框架,当然,你也可以选择不用。看懂用法之后,用起来很方便。GreenDao数据库升级到3.0版本之后api用起来更加方便了,便于让开发人员专注于业务逻辑。我需要额外说明的是,我把之前的项目框架转成了kotlin,不会kotlin语法的同学,可以去研究一下。

添加依赖

最终,我们要添加如下代码,效果图如下:

初始化数据库.png

第一步

  打开的你根目录下面的build.gradle文件,也就是项目下面的,并不是app目录下面的build.gradle。

// In your root build.gradle file:
buildscript {
    repositories {
        jcenter()
        ...//其他你自己的
        mavenCentral() // add repository
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        ...//其他你自己的
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2' // add plugin
    }
}

第二步

  打开你的项目下面的build.gradle文件,也就是你的app目录下面的,之前我们添加依赖的时候的那个文件

// In your app projects build.gradle file:
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' // apply plugin
 
dependencies {
    compile 'org.greenrobot:greendao:3.2.2' // add library
}

  完成上面两步,辣么,关于greendao的依赖我们就添加完成了

初始化

  我们首先得有一个bean类,这个bean类对应的就是数据库表的表结构。我这里想说明的是(敲黑板了),看到了很多网上说的什么主键id必须用Long类型,这种说法是不准确的,准确的说,你的主键字段名称,如果是“id”,辣么,你这个字段“id”,必须用Long类型,如果你换一个名称,比方说“myId”,辣么,你就不必用Long类型,这个问题,说大不大,说小,又困扰了我有一会。我这里新建用户表,就需要一个User的java bean类。如下:

package com.haichenyi.myproject.model.bean;

import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.NotNull;
import org.greenrobot.greendao.annotation.Property;
import org.greenrobot.greendao.annotation.Transient;

/**
 * Author: 海晨忆
 * Date: 2018/2/24
 * Desc: 用户表的bean类
 */
@Entity
public class User {
  @Id
  private String name;
  private int sex;
  @Property(nameInDb = "Height")
  private int height;
  private String weight;
  @NotNull
  private int age;
  @Transient
  private String character;
}

  这里我把几个常用的注解都写出来了,说一下这几个注解是什么意思

注解 意义
@Entity 用于标识这是一个需要Greendao帮我们生成代码的bean
@Id 标明主键,括号里可以指定是否自增
@Property 用于设置属性在数据库中的列名(默认不写就是保持一致)
@NotNull 非空
@Transient 标识这个字段是自定义的不会创建到数据库表里

简单的讲一下:

  1. @Entity:标识的bean类,我们在运行的时候,greendao会自动帮我们生成对应的表

  2. @Id:标识的字段就是这个表对应的主键

  3. @Property:标识的字段在表中对应的那一栏的名称是后面括号里面的,这个表height字段对应表中的Height,一般我们直接设置成默认就可以了

  4. @NotNull:标识的字段,这个字段在表中不能为空,不然就出错,所以,在添加数据的时候设置默认值

  5. @Transient:标识的字段,在生成表的时候不会生成对应的字段。这个什么时候用呢?这个,我一般用作标记flag,比方说,从数据库拿数据,又不想重新写一个bean类,就用这个bean类,RecyclerView,填充完数据,item点击的时候,状态发生变化,我们要有一个flag,就通过修改这个字段的值,页面做出相应的变化。

  写到这里,我们的bean类也有了,要怎么生成数据库呢?在生成数据库之前,我们先把项目重新clean一遍,再build一遍,看到你刚写的需要生成表的bean类变成了如下样子:

package com.haichenyi.myproject.model.bean;

import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.NotNull;
import org.greenrobot.greendao.annotation.Property;
import org.greenrobot.greendao.annotation.Transient;
import org.greenrobot.greendao.annotation.Generated;

/**
 * Author: 海晨忆
 * Date: 2018/2/24
 * Desc: 用户表的bean类
 */
@Entity
public class User {
  @Id
  private String name;
  private int sex;
  @Property(nameInDb = "Height")
  private int height;
  private String weight;
  @NotNull
  private int age;
  @Transient
  private String character;
  @Generated(hash = 717717955)
  public User(String name, int sex, int height, String weight, int age) {
      this.name = name;
      this.sex = sex;
      this.height = height;
      this.weight = weight;
      this.age = age;
  }
  @Generated(hash = 586692638)
  public User() {
  }
  public String getName() {
      return this.name;
  }
  public void setName(String name) {
      this.name = name;
  }
  public int getSex() {
      return this.sex;
  }
  public void setSex(int sex) {
      this.sex = sex;
  }
  public int getHeight() {
      return this.height;
  }
  public void setHeight(int height) {
      this.height = height;
  }
  public String getWeight() {
      return this.weight;
  }
  public void setWeight(String weight) {
      this.weight = weight;
  }
  public int getAge() {
      return this.age;
  }
  public void setAge(int age) {
      this.age = age;
  }
}

  如上,greendao通过注解的方式帮我们自动生成了set/get方法,还有构造方法,这就对了,我们不用关,之后我们再执行如下代码生成数据库和表:

DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(getApplicationContext(), "haichenyi.db", null);
DaoMaster daoMaster = new DaoMaster(devOpenHelper.getWritableDb());
DaoSession daoSession = daoMaster.newSession();

  通过 DaoMaster 的内部类 DevOpenHelper,你可以得到一个便利的 SQLiteOpenHelper 对象。可能你已经注意到了,你并不需要去编写「CREATE TABLE」这样的 SQL 语句,因为 greenDAO 已经帮你做了。注意:默认的 DaoMaster.DevOpenHelper会在数据库升级时,删除所有的表,意味着这将导致数据的丢失。所以,在正式的项目中,你还应该做一层封装,来实现数据库的安全升级。升级的问题,我们在后面讲,这里我们先把数据库和表先创建了。

  上面这个方式是java格式的,由于,我昨天写完框架之后,我把项目转成了kotlin代码,所以这里有点不一样,项目我后面会上传。这里我要说明的是(敲黑板)我用kotlin的时候,碰到了一个问题,当我使用greendao的时候,他提示我,无法引入用注解方式生成的类,dagger2也是一样的,我用java代码写就没有问题,我写这篇博客的时候,目前还没有找到解决的办法。

greendao的问题.png

  我用了另外一种方式,采用跟之前网络请求一样的设计模式——装饰者模式。我这里就不多做说明了。我贴出我的代码。

SqlHelper

package com.haichenyi.myproject.model.sql;

/**
 * Author: 海晨忆
 * Date: 2018/2/24
 * Desc:
 */
public interface SqlHelper {
  void onAdd();

  void onDelete();

  void onUpdate();

  void onSelect();
}

  这里定义增删改查4个方法,用于测试这4个功能

SqlImpl

package com.haichenyi.myproject.model.sql;

import com.haichenyi.myproject.base.MyApplication;
import com.haichenyi.myproject.model.bean.DaoMaster;
import com.haichenyi.myproject.model.bean.DaoSession;
import com.haichenyi.myproject.model.bean.UserDao;
import com.haichenyi.myproject.utils.ToastUtils;

/**
 * Author: 海晨忆
 * Date: 2018/2/24
 * Desc:
 */
public class SqlImpl implements SqlHelper {
  private final UserDao userDao;

  /**
   * 初始化Sql Dao.
   *
   * @param application {@link MyApplication}
   */
  public SqlImpl(MyApplication application) {
    SqlOpenHelper helper = new SqlOpenHelper(application, "haichenyi.db");
    DaoSession daoSession = new DaoMaster(helper.getWritableDb()).newSession();
    userDao = daoSession.getUserDao();
  }

  @Override
  public void onAdd() {
    ToastUtils.Companion.showTipMsg("增加数据");
  }

  @Override
  public void onDelete() {
    ToastUtils.Companion.showTipMsg("删除数据");
  }

  @Override
  public void onUpdate() {
    ToastUtils.Companion.showTipMsg("更新数据");
  }

  @Override
  public void onSelect() {
    ToastUtils.Companion.showTipMsg("查询数据");
  }
}

  功能实现类,看到他的构造方法里面,第二个参数就是我们的数据库名称,后面通过getWritableDb()获取的是可写的数据库,可写就肯定可读。然后就是接口的实现类了,这里就是具体的增删改查功能的实现类,我这里在对应的方法里面就写了Toast,增删改查具体怎么写后面再说

SqlOpenHelper

package com.haichenyi.myproject.model.sql;

import android.content.Context;

import com.haichenyi.myproject.model.bean.DaoMaster;
import com.haichenyi.myproject.model.bean.UserDao;

import org.greenrobot.greendao.database.Database;

/**
 * Author: 海晨忆
 * Date: 2018/2/24
 * Desc:
 */
public class SqlOpenHelper extends DaoMaster.OpenHelper {
  public SqlOpenHelper(Context context, String name) {
    super(context, name);
  }

  @SuppressWarnings("unchecked")
  @Override
  public void onUpgrade(Database db, int oldVersion, int newVersion) {
    super.onUpgrade(db, oldVersion, newVersion);

    MigrationHelper.migrate(db, new MigrationHelper.ReCreateAllTableListener() {
      @Override
      public void onCreateAllTables(Database db, boolean ifNotExists) {
        DaoMaster.createAllTables(db, ifNotExists);
      }

      @Override
      public void onDropAllTables(Database db, boolean ifExists) {
        DaoMaster.dropAllTables(db, ifExists);
      }
    }, UserDao.class);
  }
}

  这个类用于管理数据库的表对应的字段发生变化的时候,数据库需要进行的版本更新,连上下面那个类,都是用于版本数据库版本更新的,防止数据丢失。怎么写呢?看到最后面的 UserDao.class 了吗?这个就是我们需要更新的表,你哪个表需要更新,直接写在后面就可以了,这个是可以一次传多个表的,并不是一次只能传一个

MigrationHelper

package com.haichenyi.myproject.model.sql;

import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;

import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.database.Database;
import org.greenrobot.greendao.database.StandardDatabase;
import org.greenrobot.greendao.internal.DaoConfig;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Author: 海晨忆.
 * Date: 2018/2/24
 * Desc:
 */
public final class MigrationHelper {
  public static boolean DEBUG = false;
  private static String TAG = "MigrationHelper";
  private static final String SQLITE_MASTER = "sqlite_master";
  private static final String SQLITE_TEMP_MASTER = "sqlite_temp_master";

  private static WeakReference weakListener;

  public interface ReCreateAllTableListener {
    void onCreateAllTables(Database db, boolean ifNotExists);

    void onDropAllTables(Database db, boolean ifExists);
  }

  public static void migrate(SQLiteDatabase db, Class>... daoClasses) {
    printLog("【The Old Database Version】" + db.getVersion());
    Database database = new StandardDatabase(db);
    migrate(database, daoClasses);
  }

  public static void migrate(SQLiteDatabase db, ReCreateAllTableListener listener, Class>... daoClasses) {
    weakListener = new WeakReference<>(listener);
    migrate(db, daoClasses);
  }

  public static void migrate(Database database, ReCreateAllTableListener listener, Class>... daoClasses) {
    weakListener = new WeakReference<>(listener);
    migrate(database, daoClasses);
  }

  public static void migrate(Database database, Class>... daoClasses) {
    printLog("【Generate temp table】start");
    generateTempTables(database, daoClasses);
    printLog("【Generate temp table】complete");

    ReCreateAllTableListener listener = null;
    if (weakListener != null) {
      listener = weakListener.get();
    }

    if (listener != null) {
      listener.onDropAllTables(database, true);
      printLog("【Drop all table by listener】");
      listener.onCreateAllTables(database, false);
      printLog("【Create all table by listener】");
    } else {
      dropAllTables(database, true, daoClasses);
      createAllTables(database, false, daoClasses);
    }
    printLog("【Restore data】start");
    restoreData(database, daoClasses);
    printLog("【Restore data】complete");
  }

  private static void generateTempTables(Database db, Class>... daoClasses) {
    for (int i = 0; i < daoClasses.length; i++) {
      String tempTableName = null;

      DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
      String tableName = daoConfig.tablename;
      if (!isTableExists(db, false, tableName)) {
        printLog("【New Table】" + tableName);
        continue;
      }
      try {
        tempTableName = daoConfig.tablename.concat("_TEMP");
        StringBuilder dropTableStringBuilder = new StringBuilder();
        dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";");
        db.execSQL(dropTableStringBuilder.toString());

        StringBuilder insertTableStringBuilder = new StringBuilder();
        insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName);
        insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";");
        db.execSQL(insertTableStringBuilder.toString());
        printLog("【Table】" + tableName + "\n ---Columns-->" + getColumnsStr(daoConfig));
        printLog("【Generate temp table】" + tempTableName);
      } catch (SQLException e) {
        Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e);
      }
    }
  }

  private static boolean isTableExists(Database db, boolean isTemp, String tableName) {
    if (db == null || TextUtils.isEmpty(tableName)) {
      return false;
    }
    String dbName = isTemp ? SQLITE_TEMP_MASTER : SQLITE_MASTER;
    String sql = "SELECT COUNT(*) FROM " + dbName + " WHERE type = ? AND name = ?";
    Cursor cursor = null;
    int count = 0;
    try {
      cursor = db.rawQuery(sql, new String[]{"table", tableName});
      if (cursor == null || !cursor.moveToFirst()) {
        return false;
      }
      count = cursor.getInt(0);
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (cursor != null)
        cursor.close();
    }
    return count > 0;
  }


  private static String getColumnsStr(DaoConfig daoConfig) {
    if (daoConfig == null) {
      return "no columns";
    }
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < daoConfig.allColumns.length; i++) {
      builder.append(daoConfig.allColumns[i]);
      builder.append(",");
    }
    if (builder.length() > 0) {
      builder.deleteCharAt(builder.length() - 1);
    }
    return builder.toString();
  }


  private static void dropAllTables(Database db, boolean ifExists, @NonNull Class>... daoClasses) {
    reflectMethod(db, "dropTable", ifExists, daoClasses);
    printLog("【Drop all table by reflect】");
  }

  private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class>... daoClasses) {
    reflectMethod(db, "createTable", ifNotExists, daoClasses);
    printLog("【Create all table by reflect】");
  }

  /**
   * dao class already define the sql exec method, so just invoke it
   */
  private static void reflectMethod(Database db, String methodName, boolean isExists, @NonNull Class>... daoClasses) {
    if (daoClasses.length < 1) {
      return;
    }
    try {
      for (Class cls : daoClasses) {
        Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class);
        method.invoke(null, db, isExists);
      }
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
  }

  private static void restoreData(Database db, Class>... daoClasses) {
    for (int i = 0; i < daoClasses.length; i++) {
      DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
      String tableName = daoConfig.tablename;
      String tempTableName = daoConfig.tablename.concat("_TEMP");

      if (!isTableExists(db, true, tempTableName)) {
        continue;
      }

      try {
        // get all columns from tempTable, take careful to use the columns list
        List columns = getColumns(db, tempTableName);
        ArrayList properties = new ArrayList<>(columns.size());
        for (int j = 0; j < daoConfig.properties.length; j++) {
          String columnName = daoConfig.properties[j].columnName;
          if (columns.contains(columnName)) {
            properties.add("`" + columnName + "`");
          }
        }
        if (properties.size() > 0) {
          final String columnSQL = TextUtils.join(",", properties);

          StringBuilder insertTableStringBuilder = new StringBuilder();
          insertTableStringBuilder.append("REPLACE INTO ").append(tableName).append(" (");
          insertTableStringBuilder.append(columnSQL);
          insertTableStringBuilder.append(") SELECT ");
          insertTableStringBuilder.append(columnSQL);
          insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");
          db.execSQL(insertTableStringBuilder.toString());
          printLog("【Restore data】 to " + tableName);
        }
        StringBuilder dropTableStringBuilder = new StringBuilder();
        dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
        db.execSQL(dropTableStringBuilder.toString());
        printLog("【Drop temp table】" + tempTableName);
      } catch (SQLException e) {
        Log.e(TAG, "【Failed to restore data from temp table 】" + tempTableName, e);
      }
    }
  }

  private static List getColumns(Database db, String tableName) {
    List columns = null;
    Cursor cursor = null;
    try {
      cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 0", null);
      if (null != cursor && cursor.getColumnCount() > 0) {
        columns = Arrays.asList(cursor.getColumnNames());
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (cursor != null)
        cursor.close();
      if (null == columns)
        columns = new ArrayList<>();
    }
    return columns;
  }

  private static void printLog(String info) {
    if (DEBUG) {
      Log.d(TAG, info);
    }
  }
}

  这个类是工具类,拿过去用就好了,还有就是,应用怎么判断是否需要版本更新呢?打开你的app下面的build.grade,在根结点下面添加如下代码:

greendao {
    schemaVersion 1
}

每当你发布新版本的时候,把这个版本号+1即可。

  当然,我门这里依然是用的dagger生成的全局单例,所以,你还需要在你的AppModule下面添加如下代码:

  @Provides
  @Singleton
  SqlHelper provideSqlHelper() {
    return new SqlImpl(application);
  }

  记得把项目重新clean一遍,build一遍,重新跑项目的时候,找到你的数据库。data-data-你的应用包名-databases-haichenyi.db,这个就是我们的数据库。找个Sqlite可视化工具打开,你会看到如下结构。

表结构.png

  太多了,不写了,下一篇写增删改查。

项目链接

你可能感兴趣的:(从零开始搭建一个主流项目框架(四)—Kotlin+GreenDao3.2)