我所了解的 DBFlow

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發(fā)布

為啥要寫數(shù)據(jù)庫呢欠母?公司的項目一直在進行著調(diào)整膘怕,整個項目的 module 已經(jīng)超過 20暇检,為了做到徹底解耦挣柬,我們的組的扛把子可謂是大刀闊斧钥顽,還多次向我表達了我們的 storage 模塊需要調(diào)整的感慨庇忌。先說說現(xiàn)狀舞箍,目前項目中使用的是 Ormlite,總體的感覺就是使用起來還是要寫很多代碼皆疹,看著之前的升級版本的邏輯頭大疏橄。由于構(gòu)建和升級的邏輯都在 storage 模塊,所以要添加和修改表的話就要一定會修改到這個模塊的代碼,說白了還是耦合捎迫。

所以需求就是兩點:

  1. 使用簡單晃酒,升級方便。
  2. 多模塊使用窄绒,讓各個 module 負責各自的表贝次。(這是不是就是一個 module 對應一個數(shù)據(jù)庫來著)

上面兩點是我學習了解數(shù)據(jù)庫框架的目的,所以 DBFlow 也只是學習和嘗試的框架之一彰导,其他的后續(xù)再說蛔翅。

簡介

簡單說明一下,本文使用的是 DBFlow 的 4.1.2 版螺戳,也是截止目前為止的最新版本搁宾,在 github 上也可以看到,目前 DBFlow 還提供了一系列拓展倔幼,包括對 kotlin 的支持盖腿,Rxjava 的支持,數(shù)據(jù)庫加密等损同。當然我是帶著目的來的翩腐,所以我還是會主要關注上面提出來的兩點需求,至于一些詳細的使用還是盡力吧膏燃。

配置

為了后面多 Module 使用方便茂卦,先在項目中創(chuàng)建 config.gradle 文件,當然這個不是必須的组哩。

// config.gradle
ext {
    compileSdkVersion = 26
    buildToolsVersion = "26.0.2"
    minSdkVersion = 15
    targetSdkVersion = 26
    versionCode = 1
    versionName = "1.0"
    dbflow_version = "4.1.2"
}

對項目的 build.gradle 文件做以下修改:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle" // 沒有創(chuàng)建 config.gradle 就不是必須

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" } // 添加
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

在 app Module 中的 build.gradle 中添加依賴

dependencies {
    // ...其他依賴
    annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:$dbflow_version"
    // gradle 3.0.0 可以使用 implementation等龙,否則用 compile
    implementation "com.github.Raizlabs.DBFlow:dbflow-core:$dbflow_version"
    implementation "com.github.Raizlabs.DBFlow:dbflow:$dbflow_version"
}

到這里已經(jīng)配置完畢了,下面我們開始愉快的創(chuàng)建數(shù)據(jù)庫了伶贰。

創(chuàng)建數(shù)據(jù)庫與表

新建 App 繼承 Application蛛砰,并在其中初始化:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        FlowManager.init(this);// 初始化
    }
}
新建數(shù)據(jù)庫
@Database(version = AppDatabase.VERSION)
public class AppDatabase {
    public static final int VERSION = 1;
}
新建表

新建 Product 類,并用注解 @Table 標注黍衙,指定它的數(shù)據(jù)庫為 AppDatabase泥畅;用 @PrimaryKey 標注 id 為主鍵,并且為自增長琅翻;用 @Column 標注 name 為表中的一列位仁。

@Table(database = AppDatabase.class)
public class Product extends BaseModel {
    @PrimaryKey(autoincrement = true)
    public long id;
    @Column
    public String name;
}

這里我們的表算是建完了,只需要點擊 AndroidStudio 的 Build -> Make Project 之后方椎,就可以在 app\build\generated\source\apt\debug 目錄下找到生成的類 Product_Table聂抢,在里面可以看到一些 SQL 語句。
注意:這里我們繼承的 BaseModel 是 DBFlow 給我們提供的辩尊,并不是我們自己項目中的涛浙,所以有人可能有疑問:我可不可以不繼承它?答案是可以的,差別僅僅增刪改查的操作上有所不同轿亮。

CRUD

DBFlow 對數(shù)據(jù)的增刪改查已經(jīng)做了封裝疮薇,使用起來比較簡單,也不很容易理解我注。

Insert

對于向數(shù)據(jù)庫插入數(shù)據(jù)的操作按咒,對于已經(jīng)繼承了 BaseModel 的 bean,我們可以直接 new 一個出來但骨,給相應的屬性賦值之后励七,直接調(diào)用 save() 方法,數(shù)據(jù)就保存完畢了奔缠,代碼如下掠抬。
另外,這里我們并沒有給 Product 的主鍵 id 賦值校哎,但是在保存完之后這個 id 就被賦值了两波。

Product product = new Product();
product.name = "P" + (System.currentTimeMillis() % 10000);
product.save();
// 執(zhí)行到這里之后 id 已經(jīng)被賦值
Query

數(shù)據(jù)的查詢,這里舉了一個簡單但是平時使用較為頻繁的例子闷哆。從前文我們可以知道 Product_Table 是 apt 給我們生成的腰奋,使用起來也很靈活易懂,當然其他的更為復雜的建議大家可以查看一下官方文檔抱怔。

List<Product> products = SQLite.select()
    .from(Product.class)
    .where(Product_Table.name.isNotNull(), Product_Table.id.greaterThanOrEq(5L))// 這里的條件也可以多個
    .orderBy(Product_Table.id, true)// 按照 id 升序
    .limit(3)// 限制 3 條
    .queryList();// 返回的 list 不為 null劣坊,但是可能為 empty
Update

更新和刪除可以為先查詢后操作,只要查到對應的數(shù)據(jù)屈留,在 bean 上做修改局冰,然后調(diào)用 update() 方法,數(shù)據(jù)庫就能修改完成灌危。還有另一中更接近 sql 語法的方式锐想。

// 第一種 先查后改
Product product = SQLite.select()
        .from(Product.class)
        .querySingle();// 區(qū)別與 queryList()
if (product != null) {
    L.d("Update: " + product.name + " update to P0000");
    product.name = "P0000";
    product.update();
}
// 第二種
SQLite.update(Product.class)
        .set(Product_Table.name.eq("PXXXX"))
        .where(Product_Table.name.eq("P0000"))
        .execute();
Delete

與更新操作類似。

// 第一種 先查后刪
Product product = SQLite.select()
        .from(Product.class)
        .querySingle();
if (product != null) {
    product.delete();
    L.d("Delete: " + product.name);
}
// 第二種
SQLite.delete(Product.class)
        .where(Product_Table.name.eq("PXXXX"))
        .execute();
對于未繼承 BaseModel bean 的 CRUD

對于沒有繼承 BaseModel 的 bean乍狐,我們可以用以下方式進行數(shù)據(jù)操作,事實上前面的 save() 等方法最終也是通過這樣處理的固逗。當然定義表的時候的注解不可或缺浅蚪。
這里,考慮到更新和刪除的第二種方法烫罩,嘗試了一下惜傲,果然還可以寫出類似的插入方法。

Product product = new Product();
product.name = "P" + (System.currentTimeMillis() % 10000);
FlowManager.getModelAdapter(Product.class).insert(product);
// 又一種插入方法
SQLite.insert(Product.class)
        .columnValues(Product_Table.name.eq("P" + (System.currentTimeMillis() % 10000)))
        .execute();

版本升級

因新建表升級

好了現(xiàn)在我們已經(jīng)清楚了 DBFlow 的基本使用了贝攒,但是一張表不能滿足我們的需求暗撂堋!我還想創(chuàng)建一張 Category 表,那么趕緊再創(chuàng)建個類加下注解吧哈踱。

@Table(database = AppDatabase.class)
public class Category extends BaseModel {
    @PrimaryKey(autoincrement = true)
    public long id;
    @Column
    public String name;
}

簡單歸簡單荒适,但是還是試一下增刪改查功能吧!

Category category = new Category();
category.name = "food";
category.save();

運行开镣,之后就會發(fā)現(xiàn)了崩潰信息:

 android.database.sqlite.SQLiteException: no such table: Category (code 1): , 
 while compiling: INSERT INTO `Category`(`name`) VALUES (?)

這里我們遺漏了數(shù)據(jù)庫版本的升級刀诬,對于增加表格來說,DBFlow 版本升級其實很簡單邪财,我們只要找到我們的數(shù)據(jù)庫類陕壹,并且把他的版本號加 1。

@Database(version = AppDatabase.VERSION)
public class AppDatabase {
    public static final int VERSION = 2;
}

再重新運行下树埠,我們就能在不影響 Product 表的前提下糠馆,成功新建了 Category 表了。

因修改表結(jié)構(gòu)升級

DBFlow 的表結(jié)構(gòu)修改是通過 Migration 進行的怎憋,通過對它的實現(xiàn)又碌,來進行對表的操作。

public interface Migration {

    /**
     * Called before we migrate data. Instantiate migration data before releasing it in {@link #onPostMigrate()}
     * 在修改之前執(zhí)行盛霎。
     */
    void onPreMigrate();

    /**
     * Perform your migrations here
     * 執(zhí)行數(shù)據(jù)庫操作
     * @param database The database to operate on 我們需要操作的數(shù)據(jù)庫
     */
    void migrate(@NonNull DatabaseWrapper database);

    /**
     * Called after the migration completes. Release migration data here.
     * 在修改之后執(zhí)行赠橙,釋放資源
     */
    void onPostMigrate();
}

雖然看到這里,還是不知道怎么使用它愤炸,不過不用擔心期揪,DBFlow 已經(jīng)有它的幾個現(xiàn)成的實現(xiàn)提供給我們進行使用。

  1. AlterTableMigration 用于重命名表规个,增加列
  2. IndexMigration/IndexPropertyMigration 用于索引創(chuàng)建和刪除
  3. UpdateTableMigration 升級數(shù)據(jù)庫的時候更新數(shù)據(jù)

下面我們就舉一個相對常見的列子來看一下如何進行表結(jié)構(gòu)的修改凤薛。
首先我們先修改一下之前創(chuàng)建的 Product 表:

@Table(database = AppDatabase.class)
public class Product extends BaseModel {
    @PrimaryKey(autoincrement = true)
    public long id;
    @Column
    public String name;
    @Column(defaultValue = "10000")// 設置默認 100 塊錢,即使忘了錄入價格诞仓,我們賣出去也不吃虧(注意該屬性坑爹)
    public long price;// 分
    @Column
    public String manufacturer;

}

這里我給加上了兩列 price 和 manufactuer缤苫,然后希望價格默認為 100 元,接著寫 Migration墅拭。

@Migration(version = 3, database = AppDatabase.class)
public static class Migration3 extends AlterTableMigration<Product>{

    public Migration3(Class<Product> table) {
        super(table);
    }

    @Override
    public void onPreMigrate() {
        addColumn(SQLiteType.INTEGER, "price");
        addColumn(SQLiteType.TEXT, "manufacturer");
    }
}
  1. 因為是添加表的列活玲,所以繼承 AlterTableMigration;
  2. 修改 AppDatabase 的版本為 3谍婉,因為我們之前添加過 Category 表 version 為 2舒憾;
  3. 添加注解,注解中 version 為現(xiàn)在的版本號 3穗熬,database 為我們的 AppDatabase 表镀迂;
  4. 重寫 onPreMigrate() 方法添加 addColumn() 就是我們在 Product 中新加的字段;

有疑問唤蔗!如果一次升級我們不止改了一處探遵,還有涉及到其他的修改咋辦窟赏?
看到 @Migration 注解中有個 priority 這里我們姑且多建幾個 Migration 用 priority 區(qū)分優(yōu)先級來試試吧!

@Migration(version = 3, priority = 2, database = AppDatabase.class)
public static class Migration3 extends AlterTableMigration<Product>{
    // 省略...
}
// 新建兩個打上 log
@Migration(version = 3, priority = 0, database = AppDatabase.class)
public static class Migration3Zero extends BaseMigration {
    @Override
    public void onPreMigrate() {
        L.d("Migration3Zero onPreMigrate: ");
    }
    @Override
    public void migrate(DatabaseWrapper database) {
        L.d("Migration3Zero migrate: ");
    }
    @Override
    public void onPostMigrate() {
        L.d("Migration3Zero onPostMigrate: ");
    }
}

@Migration(version = 3, priority = 1, database = AppDatabase.class)
public static class Migration3One extends BaseMigration {
    // 省略...
}

輸出結(jié)果為:

12-07 15:00:19.406 25213-25213/com.dthfish.dbflowdemo D/DBLog: Migration3Zero onPreMigrate:
12-07 15:00:19.406 25213-25213/com.dthfish.dbflowdemo D/DBLog: Migration3Zero migrate:
12-07 15:00:19.406 25213-25213/com.dthfish.dbflowdemo D/DBLog: Migration3Zero onPostMigrate:
12-07 15:00:19.406 25213-25213/com.dthfish.dbflowdemo D/DBLog: Migration3One onPreMigrate:
12-07 15:00:19.406 25213-25213/com.dthfish.dbflowdemo D/DBLog: Migration3One migrate:
12-07 15:00:19.406 25213-25213/com.dthfish.dbflowdemo D/DBLog: Migration3One onPostMigrate:

可以看到 priority 小的執(zhí)行順序優(yōu)先箱季,如果想要指定兩個Migration 的 priority 相同的同學涯穷,就不要找不痛快了,因為沒法保證執(zhí)行順序规哪。
接下來看一下我們更新的表格內(nèi)容:

  1. 查詢之前的數(shù)據(jù)求豫,已經(jīng)成功的添加了兩個屬性,但是 price = 0诉稍;

12-07 15:00:19.426 25213-25213/com.dthfish.dbflowdemo D/DBLog:
Query: [
Product{id=6, name='P6396', price=0, manufacturer='null'},
Product{id=7, name='P9297', price=0, manufacturer='null'},
Product{id=8, name='P8988', price=0, manufacturer='null'},
Product{id=9, name='P7232', price=0, manufacturer='null'},
Product{id=10, name='P7147', price=0, manufacturer='null'},
Product{id=11, name='P7634', price=0, manufacturer='null'}
]

  1. 新插入一條數(shù)據(jù)后蝠嘉,查詢——然并軟,說好的 defaultValue 呢杯巨!你還我的 100 塊蚤告!

12-07 15:06:47.666 25213-25213/com.dthfish.dbflowdemo D/DBLog:
Query: [
Product{id=6, name='P6396', price=0, manufacturer='null'},
Product{id=7, name='P9297', price=0, manufacturer='null'},
Product{id=8, name='P8988', price=0, manufacturer='null'},
Product{id=9, name='P7232', price=0, manufacturer='null'},
Product{id=10, name='P7147', price=0, manufacturer='null'},
Product{id=11, name='P7634', price=0, manufacturer='null'},
Product{id=12, name='P1172', price=0, manufacturer='null'}
]

關于失效的的 defaultValue

好吧關于 defaultValue 失效我是始料未及的,在 github 上的 Issues 中查看了一下服爷,雖然有類似的問題但是還是沒有找到正確的處理方法(希望不是我英文水平的問題)杜恰,如果有同學知道正確的方法請務必聯(lián)系我!以免我誤人子弟仍源!
但是我嘗試出了我自己的方法心褐。我的數(shù)據(jù)庫要升級版本 4 啦!

@Table(database = AppDatabase.class)
public class Product extends BaseModel {
    @PrimaryKey(autoincrement = true)
    public long id;
    @Column
    public String name;
    @Column//(defaultValue = "10000")// 設置默認 100 塊錢笼踩,即使忘了錄入價格逗爹,我們賣出去也不吃虧
    public long price = 10000L;// 分
    @Column
    public String manufacturer;
}

@Migration(version = 4, database = AppDatabase.class)
public static class Migration4 extends UpdateTableMigration<Product> {
    public Migration4(@NonNull Class<Product> table) {
        super(table);
    }
    @Override
    public void onPreMigrate() {
        where(Product_Table.price.eq(0L));
        set(Product_Table.price.eq(10000L));
    }
}
  1. 去掉了 defaultValue 直接給 price 一個默認值;
  2. 對版本 3 的補救措施嚎于,寫了 Migration4掘而,在 onPreMigrate 方法中對歷史數(shù)據(jù)進行了處理。
刪掉表格的一列

見 github Issue #467

This is a SQLite question, and short answer is...not easily. You have to (this is with SQLite too):

  1. Create new table without column with temporary name
  2. Copy over data to it
  3. Drop old table
  4. Recreate new table with column left out

As I said, not easy.

  1. 創(chuàng)建一個沒有那一列的臨時的表
  2. 把數(shù)據(jù)復制進去
  3. 刪掉舊的表
  4. 重新建個表

令我不禁想起了郭神的 LitePal于购,對刪除列做的良心處理袍睡。

多 module 使用

其實說了這么多,我最關心的還是多 module 的使用肋僧,畢竟我最初的目的還是這個斑胜。接下來的過程有些曲折,我會把過程中出現(xiàn)的錯誤以及處理方法都記錄下來嫌吠。

創(chuàng)建多個 Module 添加依賴

這里除去 app伪窖,我又創(chuàng)建了 base,special居兆,ship 三個 module,下面看一下他們的依賴配置竹伸。

// base build.gradle
dependencies {
    // 其他...
    annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:$dbflow_version"
    api "com.github.Raizlabs.DBFlow:dbflow-core:$dbflow_version"
    api "com.github.Raizlabs.DBFlow:dbflow:$dbflow_version"
}
// ship build.gradle
dependencies {
    // 其他...
    annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:$dbflow_version"
    api project(':base')
}
// special build.gradle
dependencies {
    // 其他...
    annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:$dbflow_version"
    api project(':base')
}
// app build.gradle
dependencies {
    // 其他...
    annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:$dbflow_version"
    implementation project(':ship')
    implementation project(':special')
}

這里不用在意 api 和 implementation泥栖,它們是 com.android.tools.build:gradle:3.0.0 才有的指令簇宽,統(tǒng)統(tǒng)可以改成為 compile。到這里我重新跑了一下程序吧享,發(fā)現(xiàn)通過了魏割,那還等什么趕緊試一下。

在 ship module 中新建數(shù)據(jù)庫和表

我沒有把 AppDataBase 移到 base module 中钢颂,因為我希望各個 module 維護自己的表的時候不要修改到底層的 module钞它,所以在 ship 中創(chuàng)建自己的 ShipDataBase。

@Database(version = ShipDatabase.VERSION)
public class ShipDatabase {
    public static final int VERSION = 1;
}

@Table(database = ShipDatabase.class)
public class ShipProduct extends BaseModel{
    @PrimaryKey(autoincrement = true)
    public long id;
    @Column
    public String name;
}
開始痛苦的解決問題

和前文一樣殊鞭,添加插入和查詢方法遭垛,build 報錯:

Error:Error converting bytecode to dex:
Cause: com.android.dex.DexException: Multiple dex files define Lcom/raizlabs/android/dbflow/config/GeneratedDatabaseHolder;

What a f**k! 好吧強大的 google 指引我到 github Issue #266
里面信息一堆操灿,但是我隱約找到了我要的答案:

What I learned is you can pass arguments to the processor. For example, the following addition to your build.gradle file >will pass the target module name to DBFlow-Compiler:

apt {
  arguments {
      targetModuleName 'DBFlow'
  }
}

This will result in the database holder being named DBFlowGeneratedDatabaseHolder. It will be initialized as discussed >in previous comments.

另外還有人提出 apt 已經(jīng)不維護了:

I use
apt { arguments { targetModuleName 'DBFlow' } }
this solution in android studio 2.3.3
and that solves my problem but today I update my android studio to version 3.0 and we don't have apt or we cannot use it so any solution for this version of the android studio
I also ask the question in StackOverflow https://stackoverflow.com/questions/46998943/how-to-set-prefix-to-generateddatabaseholder-java-class-in-dbflow-in-android-stu

就是說锯仪,在使用 apt(咋配置就不提了,大家可以查一下) 的情況下趾盐,可以在 Ship 的 module 的 build.gradle 中添加:

apply plugin: 'com.android.library'
android {
    // 省略...
}
apt {
    arguments{
        targetModuleName 'Ship'
    }
}
dependencies {
    // 省略...
}

那么就可以解決問題庶喜,通過 build 的話,最終生成的類名會是 ShipGeneratedDatabaseHolder救鲤,與 app module 中的GeneratedDatabaseHolder 區(qū)別久窟。
但是現(xiàn)在的問題是 apt 已經(jīng)不維護了,我們是否還能通過啥方法進行處理呢本缠?
Android注解使用之注解編譯android-apt如何切換到annotationProcessor
按照上文斥扛,我又修改了 Ship 的 build.gradle 文件:

android {
    // 省略...
    defaultConfig {
        // 省略...
        jackOptions {
            enabled true
        }
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ targetModuleName : 'Ship' ]
            }
        }

    }
    // 省略...
}

編譯,報錯:


報錯1.png

好吧再改搓茬!AndroidStudio 告訴了我們解決方案:

android {
    // 省略...
    defaultConfig {
        // 省略...
        android.compileOptions.sourceCompatibility 1.8
        android.compileOptions.targetCompatibility 1.8
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ targetModuleName : 'Ship' ]
            }
        }

    }
    // 省略...
}

這次終于成功了跑起來了犹赖,我們試試插入吧!報錯:(我已經(jīng)習慣了)

com.raizlabs.android.dbflow.structure.InvalidDBConfiguration:
Model object: com.dthfish.ship.database.ShipProduct is not registered with a Database. Did you forget an annotation?

還好卷仑,通過錯誤我們看到說我們沒有在 Database 中注冊峻村,可是事實上我們已經(jīng)加了 @Table(database = ShipDatabase.class) 注解。實際上锡凝,是我們新生成的 ShipGeneratedDatabaseHolder 需要注冊粘昨。

FlowConfig flowConfig = new FlowConfig.Builder(this)
        .addDatabaseHolder(ShipGeneratedDatabaseHolder.class)
        .build();
FlowManager.init(flowConfig);

到這里,多 module 的使用已經(jīng)介紹完啦窜锯,我先去 Stackoverflow 上替外國友人解答下這個問題张肾!

外鍵

終于到這里了,原來解決完上邊的多 module 使用的問題其實已經(jīng)感覺篇幅有點長了锚扎,但是考慮到外鍵的重要性覺得還是有必要講一下的吞瞪。

升級數(shù)據(jù)庫:給表添加外鍵

事實上一上來就會有疑問:更新數(shù)據(jù)庫的時候添加普通的一列和添加外鍵一樣嗎?
對于我們的 bean 來說添加僅僅是添加了一個成員變量驾孔,但是更新數(shù)據(jù)庫就不一樣了芍秆,因為數(shù)據(jù)庫里面這些自定義的數(shù)據(jù)結(jié)構(gòu)是通過外鍵關聯(lián)的惯疙。好吧我們又回到了數(shù)據(jù)庫版本更新的問題,但是現(xiàn)在這個情況都不包含在之前提過的里面妖啥。下邊看一下我最終嘗試過后的結(jié)構(gòu):

@Table(database = AppDatabase.class)
public class Product extends BaseModel {
    @PrimaryKey(autoincrement = true)
    public long id;
    @Column
    public String name;
    @Column//(defaultValue = "10000")// 設置默認 100 塊錢霉颠,即使忘了錄入價格,我們賣出去也不吃虧
    public long price = 10000L;// 分
    @Column
    public String manufacturer;
    @ForeignKey(stubbedRelationship = true, saveForeignKeyModel = true)
    public Category category;
}

@Migration(version = 5, database = AppDatabase.class)
public static class Migration5 extends AlterTableMigration<Product> {
    public Migration5(Class<Product> table) {
        super(table);
    }
    @Override
    public void onPreMigrate() {
        addForeignKeyColumn(SQLiteType.INTEGER, "category_id", FlowManager.getTableName(Category.class) +"(`id`) ");
    }
}
  1. 在 Product 類中添加 category 字段荆虱,畢竟我們的產(chǎn)品是按品類劃分的
  2. 給 category 字段添加 @ForeignKey 注解蒿偎,注解里面的參數(shù),暫時先放一下怀读,一會說
  3. 編寫 Migration5诉位,修改版本號

雖然列了簡單的三步但是還是有值得思考的地方:onPreMigrate() 方法中的代碼該怎么寫?這個 category_id 哪里來的愿吹?
事實上 addForeignKeyColumn() 方法是在我寫版本升級的時候在 AlterTableMigration 類中發(fā)現(xiàn)的:

/**
 * Add a column to the DB. This does not necessarily need to be reflected in the {@link TModel},
 * but it is recommended.
 *
 * @param sqLiteType      The type of column that pertains to an {@link SQLiteType}//添加的字段的類型
 * @param columnName      The name of the column to add. Use the "$Table" class for the specified table.//添加的列名
 * @param referenceClause The clause of the references that this foreign key points to.//外鍵的指向
 * @return This instance
 */
public AlterTableMigration<TModel> addForeignKeyColumn(SQLiteType sqLiteType, String columnName, String referenceClause) {
    // 省略...
}

雖然注釋中有說各個參數(shù)的含義不从,但是事實上看了還是不知道怎么填寫,SQLiteType 中只有一些基本的類型犁跪,最終給我提示的還是 apt 生成的 Product_Table 類椿息。里面有這么一段:

@Override
public final String getCreationQuery() {
    return "CREATE TABLE IF NOT EXISTS `Product`(`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `price` INTEGER, `manufacturer` TEXT, `category_id` INTEGER"+ ", FOREIGN KEY(`category_id`) REFERENCES " + com.raizlabs.android.dbflow.config.FlowManager.getTableName(com.dthfish.dbflowdemo.database.Category.class) + "(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION" + ");";
}

結(jié)果就是把里面的內(nèi)容截取出來當參數(shù)了,運行結(jié)果也令人滿意坷衍。ps:如果這里有誤請及時聯(lián)系我寝优,給我留言啊,我也是摸索出來的枫耳。

一對一外鍵
public void foreignKeyInsert(View view) {
    // 為了保持每次的 Category 為同一個乏矾,其實是之前代碼設計的錯誤,沒有把 name 定為唯一
    Category category = SQLite.select()
            .from(Category.class)
            .where(Category_Table.name.eq("meat"))
            .querySingle();
    if (category == null) {
        category = new Category();
        category.name = "meat";
    }
    Product product = new Product();
    product.name = "P" + (System.currentTimeMillis() % 10000);
    product.category = category;
    product.save();
    L.d("Insert: " + product.toString());
    mLastInsertId = product.id;
}
private long mLastInsertId;
public void foreignKeyQuery(View view) {
    // 查詢上次保存的 Product
    List<Product> products = SQLite.select()
            .from(Product.class)
            .where(Product_Table.id.eq(mLastInsertId))
            .queryList();
    L.d("Query: " + products.toString());
}

兩個方法分別執(zhí)行迁杨,得到:

12-08 15:30:28.179 7615-7615/com.dthfish.dbflowdemo D/DBLog: 
Insert: Product{id=6, name='P8158', price=10000, manufacturer='null', category=Category{id=1, name='meat'}}
12-08 15:30:30.419 7615-7615/com.dthfish.dbflowdemo D/DBLog: 
Query: [Product{id=6, name='P8158', price=10000, manufacturer='null', category=Category{id=1, name='null'}}]

我們發(fā)現(xiàn) query 操作查出來的 category 字段钻心,只有 id,name 卻等于 null;修改如下:

private long mLastInsertId = 6L;// 6 為上邊 log 打印出來的 id
public void foreignKeyQuery(View view) {
    List<Product> products = SQLite.select()
            .from(Product.class)
            .where(Product_Table.id.eq(mLastInsertId))
            .queryList();
    for (Product product : products) {
        product.category.load();
    }
    L.d("Query: " + products.toString());
}

結(jié)果:

12-08 16:17:04.369 10814-10814/com.dthfish.dbflowdemo D/DBLog: 
Query: [Product{id=6, name='P8158', price=10000, manufacturer='null', category=Category{id=1, name='meat'}}]

這里我們多執(zhí)行了一步操作铅协,調(diào)用了 Category 的 load() 方法捷沸,然后就查到了 id 為 1 的 Category 所有的信息了。

@ForeignKey(stubbedRelationship = true, saveForeignKeyModel = true, deleteForeignKeyModel = false)
public Category category;

解釋:

  1. 我們標注 @ForeignKey 的時候聲明了 stubbedRelationship = true狐史,這樣我們查詢的時候會僅僅查出 category 的主鍵痒给,當 load() 的時候才進一步查詢;
  2. 上面的例子當我們保存 Product 的時候骏全,事實上也沒有顯性的調(diào)用 category.save()苍柏,最終查詢的時候發(fā)現(xiàn)已經(jīng)插入了 name = "meat" 的 Category,這是因為 saveForeignKeyModel = true姜贡。有興趣的同學可以自己試一下 deleteForeignKeyModel = true;
一對多
@Table(database = AppDatabase.class)
public class Product extends BaseModel {
    @PrimaryKey(autoincrement = true)
    public long id;
    @Column
    public String name;
    @Column//(defaultValue = "10000")// 設置默認 100 塊錢试吁,即使忘了錄入價格,我們賣出去也不吃虧
    public long price = 10000L;// 分
    @Column
    public String manufacturer;
    @ForeignKey(stubbedRelationship = true, saveForeignKeyModel = true, deleteForeignKeyModel = false)
    public Category category;
    public List<Product> present;
    public List<Product> getPresent() {
        if (present == null || present.isEmpty()) {
            present = SQLite.select()
                    .from(Product.class)
                    .where(Product_Table.name.like("PX%"))
                    .queryList();
        }
        return present;
    }
}

簡單的帶過一下楼咳,就是添加一個 List熄捍,通過 get 方法去查詢律秃,僅此而已,甚至不需要升級版本號治唤。

事務

ContentProvider

剩下的幾個主題就不再寫了,有興趣的同學可以自己看下官方文檔糙申。

最后

說實話在寫這篇文章之前我自己寫過另一個 Demo宾添,但是那個就沒有這篇文章舉得例子這樣簡單明了。當然 DBFlow 的還有很多細節(jié)我沒有講到柜裸,包括但不限于加密缕陕,Rxjava這些的支持。正因為我自己在學習 DBFlow 的過程中也去找了相關文章疙挺,要么說的都比較簡單扛邑,要么版本不是最新的,最后還是自己看文檔進行嘗試铐然,總結(jié)一篇出來蔬崩。結(jié)果也是有很多意外收獲,總之在沒有開始寫的時候我知道篇幅會很長搀暑,但是實際上更長沥阳,所以最后有幾個想講的地方也就懶得完善了,哈哈自点!

如果有喜歡這篇文章的同學桐罕,請務必給我一個贊呀!這是對廣大寫博客的同學的最大的肯定桂敛!
BDFlow github 地址
Demo 地址
原文地址

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末功炮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子术唬,更是在濱河造成了極大的恐慌薪伏,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碴开,死亡現(xiàn)場離奇詭異毅该,居然都是意外死亡,警方通過查閱死者的電腦和手機潦牛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門眶掌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人巴碗,你說我怎么就攤上這事朴爬。” “怎么了橡淆?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵召噩,是天一觀的道長母赵。 經(jīng)常有香客問我,道長具滴,這世上最難降的妖魔是什么凹嘲? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮构韵,結(jié)果婚禮上周蹭,老公的妹妹穿的比我還像新娘。我一直安慰自己疲恢,他們只是感情好凶朗,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著显拳,像睡著了一般棚愤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杂数,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天宛畦,我揣著相機與錄音,去河邊找鬼耍休。 笑死刃永,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的羊精。 我是一名探鬼主播斯够,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼喧锦!你這毒婦竟也來了读规?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤燃少,失蹤者是張志新(化名)和其女友劉穎束亏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阵具,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡碍遍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了阳液。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怕敬。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖帘皿,靈堂內(nèi)的尸體忽然破棺而出东跪,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布虽填,位于F島的核電站丁恭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏斋日。R本人自食惡果不足惜牲览,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望恶守。 院中可真熱鬧竭恬,春花似錦、人聲如沸熬的。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽押框。三九已至,卻和暖如春理逊,著一層夾襖步出監(jiān)牢的瞬間橡伞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工晋被, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兑徘,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓羡洛,卻偏偏與公主長得像挂脑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子欲侮,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344