Google官方Android的Room持久化库示例Demo

是时候该放弃 GreenDao的使用了,该使用Room持久化库喽~~~

理由:GreenDao库已经很少维护更新了,greenDao现在在较新的开发环境使用中会警告:

WARNING: API 'variant.getJavaCompiler()' is obsolete and has been replaced with 'variant.getJavaCompileProvider()'.
It will be removed at the end of 2019.

百度的解决方法:

解决方法:
将gradle 从高版本还原到 3.2以下

 


 

文档地址:https://developer.android.com/topic/libraries/architecture/room.html

Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。

处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的用例是缓存相关数据。这样,当设备无法访问网络时,用户仍可在离线状态下浏览相应内容。设备之后重新连接到网络后,用户发起的所有内容更改都会同步到服务器。

由于 Room 负责为您处理这些问题,因此强烈建议您使用 Room(而不是 SQLite)。

注意:要在应用中使用 Room,请在应用的 build.gradle 文件中声明 Room 依赖项

dependencies {
      def room_version = "2.2.0-rc01"

      implementation "androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

      // optional - Kotlin Extensions and Coroutines support for Room
      implementation "androidx.room:room-ktx:$room_version"

      // optional - RxJava support for Room
      implementation "androidx.room:room-rxjava2:$room_version"

      // optional - Guava support for Room, including Optional and ListenableFuture
      implementation "androidx.room:room-guava:$room_version"

      // Test helpers
      testImplementation "androidx.room:room-testing:$room_version"
    }

 

Room 包含 3 个主要组件:

  • 数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。

    使用 @Database 注释的类应满足以下条件:

    • 是扩展 RoomDatabase 的抽象类。
    • 在注释中添加与数据库关联的实体列表。
    • 包含具有 0 个参数且返回使用 @Dao 注释的类的抽象方法。

    在运行时,您可以通过调用 Room.databaseBuilder()Room.inMemoryDatabaseBuilder() 获取 Database 的实例。

  • Entity:表示数据库中的表。

  • DAO:包含用于访问数据库的方法。

应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个 DAO 从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。最后,应用使用实体来获取和设置与数据库中的表列相对应的值。

Room 不同组件之间的关系如图 所示:

创建实体和Dao:

package com.loaderman.roomdemo.bean;

import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;


//Entity: 表示数据库内的表.
@Entity(tableName = "user", indices = {@Index(value = {"name"}, unique = true)})//有些时候, 数据库中的某些域或几组域必须是唯一的. 你可以通过将注解@Index的unique属性设置为true, 强制完成唯一的属性.
public class User {

    @PrimaryKey(autoGenerate = true)//主键是否自动增长,默认为false
    private int id;
    @NonNull//  表示参数,成员变量或者方法返回值从不为null
    @ColumnInfo(name = "name")//指定数据库表的列名。
    private String name;
    //演示版本2,升级数据库版本用, 演示版本1注释掉即可
    private String phone;
    private int age;
    public User() {
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Ignore
    public User(String name, int age) {

        this.name = name;
        this.age = age;
    }

    @Ignore
    public User(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    // getter  setter方法必须写
    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", phone='" + phone + '\'' +
                ", age=" + age +
                '}';
    }
}
package com.loaderman.roomdemo.bean;

import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.PrimaryKey;
//@ForeignKey注解定义它和实体User的关系
/*
* 外键非常强大, 因为它允许你指定做什么操作, 在引用实体更新的时候. 比如, 你可以告诉SQLite为用户删除所有的书,
* 在相应的User实例被删除时, 而该User被Book通过在@ForeignKey注解里面声明onDelete = CASCADE而关联.
* 备注: SQLite将@Insert(onConflict = REPLACE)作为REMOVE和REPLACE的集合来操作, 而非单独的UPDATE操作. 这个取代冲突值的方法能够影响你的外键约束.
 * */
@Entity(foreignKeys = @ForeignKey(entity = User.class,
        parentColumns = "id",
        childColumns = "user_id"))
public class Book {
    @PrimaryKey
    public int bookId;

    public String title;

    @ColumnInfo(name = "user_id")
    public int userId;
}
package com.loaderman.roomdemo.dao;

import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;

import com.loaderman.roomdemo.bean.Book;
import com.loaderman.roomdemo.bean.User;

import java.util.List;

import io.reactivex.Flowable;

//DAO: 包含用于访问数据库的方法.
@Dao
public interface UserDao {
    // OnConflictStrategy.REPLACE表示如果已经有数据,那么就覆盖掉
    //数据的判断通过主键进行匹配,也就是uid,非整个user对象
    //返回Long数据表示,插入条目的主键值(id)


    @Query("SELECT * FROM user")
    List<User> getAllUsers();

    // LiveData是可以被观察到的数据持有类。它里面缓存或持有了最新的数据。当数据改变时会通知它的观察者。
    // LiveData是可以感知生命周期的。UI组件只是观察相关数据,不会停止或恢复观察。
    // LiveData自动管理所有这些,因为它在观察时意识到相关的生命周期状态变化。
    @Query("SELECT * FROM user")
    LiveData<List<User>> getAllUser();

    @Query("SELECT * FROM user WHERE id=:id")
    User getUser(int id);

    @Query("SELECT * FROM user WHERE name=:name")
    User getUser(String name);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    List<Long> insert(User... users);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    Long insert(User user);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    List<Long> insert(List<User> userLists);

    @Update
    int update(User... users);

    @Update()
    int updateAll(User... user);

    @Update()
    int updateAll(List<User> user);

    @Delete
    int delete(User user);

    @Delete
    int deleteAll(List<User> users);

    @Delete
    int deleteAll(User... users);

    //返回RxJava2中的Publisher和Flowable.
    @Query("SELECT * from user where name = :name LIMIT 1")
    Flowable<User> getUserByName(String name);


    //多表查询
    @Query("SELECT * FROM book "
            + "INNER JOIN user ON user.id = book.user_id "
            + "WHERE   user.id = :userName")
    List<Book> findBooksByUserName(String userName);

}

AppDatabase.java

package com.loaderman.roomdemo.dao;

import android.content.Context;

import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;

import com.loaderman.roomdemo.Converters;
import com.loaderman.roomdemo.bean.Book;
import com.loaderman.roomdemo.bean.User;
//注解指定了database的表映射实体数据以及版本等信息 推荐使用单例模式
@Database(entities = { User.class, Book.class }, version = 2,exportSchema = true)
//添加@TypeConverters注解到AppDatabase类上, 之后Room就能够在AppDatabase中定义的每一个实体和DAO上使用这个转换器.
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {

    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE user ADD COLUMN phone TEXT ");
        }
    };

    public abstract UserDao getUserDao();
}

潜逃对象的创建示例:

package com.loaderman.roomdemo.bean;

import androidx.room.ColumnInfo;
import androidx.room.Embedded;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
/*
 创建嵌套对象有些时候, 在数据库逻辑中, 你想将一个实体或者POJO表示为一个紧密联系的整体,
 即使这个对象包含几个域. 在这些情况下, 你能够使用@Embedded注解来表示一个对象,
 而你想将这个对象分解为表内的子域. 然后你可以查询这些嵌套域, 就像你查询其它的独立列一样.
 这个表表示UserInfo对象包含如下几列: id, firstName, street, state, city和post_code.
 备注: 嵌套的域同样可以包含其它的嵌套域.
 如果实体拥有多个相同类型的嵌套域, 你可以通过设置prefix属性保留每一列唯一. 然后Room给嵌套对象的每一个列名的起始处添加prefix设置的给定值
*/
@Entity
public class UserInfo {
    @PrimaryKey
    public int id;

    public String firstName;

    @Embedded
    public Address address;
}

 class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code")
    public int postCode;
}

类型转换器

package com.loaderman.roomdemo;

import androidx.room.TypeConverter;

import java.sql.Date;
//类型转换器
public class Converters {
    @TypeConverter
    public static Date revertDate(long value) {
        return new Date(value);
    }

    @TypeConverter
    public static long converterDate(Date value) {
        return value.getTime();
    }
}

数据库迁移

你添加和更改App功能时,你需要修改实体类来反映这些更改。当用户更新到你的应用最新版本时,你不想要他们丢失所有存在的数据,尤其是你无法从远端服务器恢复数据时。

Room允许你编写Migration类来保留用户数据。每个Migration类指明一个startVersionendVersion。在运行时,Room运行每个Migration类的migrate()方法,使用正确的顺序来迁移数据库到最新版本。

警告:如果你没有提供需要的迁移类,Room将会重建数据库,也就意味着你会丢掉数据库中的所有数据。
警告:为了使迁移逻辑正常运行,请使用完整查询,而不是引用代表查询的常量。
示例代码:
  public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE user ADD COLUMN phone TEXT ");
        }
    };

配置数据库:

package com.loaderman.roomdemo;

import android.app.Application;
import androidx.room.Room;
import com.loaderman.roomdemo.dao.AppDatabase;
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initDb();
    }

    private static AppDatabase db;

    private void initDb() {
        db = Room.databaseBuilder(getApplicationContext(),
                AppDatabase.class, "UserDB")
                //添加数据库的变动迁移支持(当前状态从version1到version2的变动处理)
                //数据库升级1-->2
                .addMigrations(AppDatabase.MIGRATION_1_2)
                //下面注释表示允许主线程进行数据库操作,但是不推荐这样做。
                //他可能造成主线程lock以及anr
//                .allowMainThreadQueries()
                .build();

    }

    public static AppDatabase getAppDbHelper() {
        return db;
    }

}

记录数据库

在主module的build.gradle里面添加代码即可

    defaultConfig {
    ...
    javaCompileOptions {
        annotationProcessorOptions {
            //room的数据库概要、记录
            arguments = ["room.schemaLocation":
                                 "$projectDir/schemas".toString()]
        }
    }
}     
sourceSets {
    //数据库概要、记录存放位置
    androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}

效果:

 

 

测试:

 

package com.loaderman.roomdemo;

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import com.loaderman.roomdemo.bean.User;
import com.loaderman.roomdemo.dao.UserDao;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.Flowable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;

public class MainActivity extends AppCompatActivity {
    UserDao userDao;
    private TextView tvMsg;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        userDao = MyApplication.getAppDbHelper().getUserDao();
        tvMsg = findViewById(R.id.tv_msg);
    }

    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_insert_one:
                System.out.println("---------btn_insert_one----------------");
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        long insert = userDao.insert(new User(33,"张三", 18));
                        System.out.println(insert+"loaderman");
                        System.out.println("-------------------------");
                        MainActivity.this.setMsg("插入的主键值:" + insert);
                    }
                }).start();;

                break;
            case R.id.btn_insert_multiple:

                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        List<User> list = new ArrayList<>();
                        list.add(new User(44,"李四", 42));
                        list.add(new User(55,"王五", 56));
                        List<Long> insertALl = userDao.insert(list);
                        MainActivity.this.setMsg("插入的主键值:" + insertALl.toString());
                    }
                }).start();;

                break;
            case R.id.btn_query_one:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        User user1 = userDao.getUser("张三");
                        if (user1==null){
                            MainActivity.this.setMsg("没有查询到" );
                        }else {
                            MainActivity.this.setMsg("查询单个:" + user1.toString());
                        }

                    }
                }).start();;

                break;
            case R.id.btn_query_multiple:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        List<User> allUsers = userDao.getAllUsers();
                        if (allUsers==null){
                            MainActivity.this.setMsg("没有查询到" );
                        }else {
                            MainActivity.this.setMsg("查询多个:" + allUsers.toString());
                        }

                    }
                }).start();;

                break;
            case R.id.btn_update_one:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        int update = userDao.update(new User(33,"张三", 25));

                        MainActivity.this.setMsg("更新行数:" + update);
                    }
                }).start();

                break;
            case R.id.btn_update_multiple:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        List<User> list3 = new ArrayList<>();
                        list3.add(new User("李四", 44));
                        list3.add(new User("王五", 55));
                        int updateAll = userDao.updateAll(list3);
                        MainActivity.this.setMsg("更新行数:" + updateAll);
                    }
                }).start();;

                break;
            case R.id.btn_delete_one:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        int delete = userDao.delete(new User(33,"张三", 18));

                        MainActivity.this.setMsg("删除行数:" + delete);

                    }
                }).start();;

                break;
            case R.id.btn_delete_multiple:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        List<User> list2 = new ArrayList<>();
                        list2.add(new User(44,"李四", 42));
                        list2.add(new User(55,"王五", 56));
                        int deleteAll = userDao.deleteAll(list2);
                        MainActivity.this.setMsg("删除行数:" + deleteAll);
                    }
                }).start();;

                break;
            case R.id.btn_rx_query:
                 userDao.getUserByName("张三").observeOn(Schedulers.newThread()).subscribe(new Consumer<User>() {
                     @Override
                     public void accept(User user)  {
                         //该回调是子线程。需要在主线程刷新UI
                         MainActivity.this.setMsg("RxJava查询:" + user.toString());
                     }

                 });

                break;
            case R.id.btn_live_query:
                userDao.getAllUser().observe(this, new Observer<List<User>>() {
                    @Override
                    public void onChanged(List<User> users) {
                        Toast.makeText(MainActivity.this,users.toString(),Toast.LENGTH_LONG).show();
                        tvMsg.setText(users.toString());
                    }
                });
                break;
        }
    }
    private void setMsg(final String msg) {

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(MainActivity.this,msg,Toast.LENGTH_LONG).show();
                tvMsg.setText(msg);
            }
        });
    }
}

 

 

 


多版本迁移

要是用户刚下载的 APP,想升级到版本最新版本呢?目前我们定义了migrations:version 1 到 2, version 2 到 3, version 3 到 4, 所以 Room 会一个接一个的触发所有 migration。实际开发过程中,一般不会那么频繁

posted on 2019-12-23 21:39  LoaderMan  阅读(2850)  评论(0编辑  收藏  举报

导航