android 开发进程 0.37 room数据存储的使用

Posted 百密一疏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 开发进程 0.37 room数据存储的使用相关的知识,希望对你有一定的参考价值。

room数据存储简介

room 是jetpack库中的一个数据持久化库,底层还是使用的SQLite的实现方式。但使用方式更加简单,原生的SQLite方法较为繁琐,room使用的是实体类和数据库表映射的方式。更为简洁易懂。  

room数据库导入

在module的gradle中添加:

    def room_version = "2.3.0" // check latest version from docs
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"

注意kapt是在项目使用kotlin后必须添加的,且不是数据库相关代码使用kotlin才需要导入,只要项目导入了kotlin就需要引入kotlin的库,此外GitHub上还有与rxjava等其他可能用到的类库。

room数据库的使用

设置数据表

room数据库集与实体类和数据表的映射关系,首先看如何创建一个基本的数据表:

@Entity(tableName = "User")
public class User {
    @PrimaryKey((autoGenerate = true))
    @ColumnInfo(name = "id")
    private int id;
    @ColumnInfo(name = "name")
    private String name;
    @ColumnInfo(name = "password")
    private String password;
}

在上面的代码中我们省去了构造函数和获取类变量的方法。可以看到room使用了注解来简化操作,@Entity 注解表示此类为一个数据库的表,同时其中的tableName设置表名,不显示处理表名即为类名。变量注解@Primarykey设置主键和是否自增,@ColumnInfo是必须的,设置变量为数据库的列名。

设置数据库

设置数据库代码如下:其中UserDataDao是我们准备封装的数据库方法。

@Database(entities = {User.class}, version = 1)
public abstract class UserDataBase extends RoomDatabase {
    private static UserDataBase instance;
    public static UserDataBase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context.getApplicationContext(), UserDataBase.class,"userdatabase")
                    .allowMainThreadQueries()
                    .addMigrations(MISGRATION_1_2)
                    .fallbackToDestructiveMigration()
                    .build();
        }
        return instance;
    }
    public abstract UserDataDao userDataDao();

注解@Database中的entities关联了数据库表和数据库,后面的version表明数据库的版本,这在数据库的更新中会用到。使用一个静态方法创建数据库,allowMainThreadQueries方法设置可以在主线程中读写数据库,addMigrations是添加数据库升级策略,fallbackToDestructiveMigration允许数据库升级失败后重建而不是直接崩溃。

数据库访问Dao

@Dao
public interface UserDataDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insert(User...users);
    @Update
    void updata(User...users);
    @Delete
    void delete(User...users);
    @Query("Select * from User")
    List<User> selectAll();
}

采用已经设置好的注解,OnConflictStrategy设置冲突时解决策略,即当插入一行时如果此行已经存在即覆盖。room会自动生成一个impl的类,其中实现了具体的SQL执行过程。

使用方式

    userDataBase = UserDataBase.getInstance(this);
    userDataDao=userDataBase.userDataDao();
    User user=new User(1, "11", "111");
    userDataDao.insert(user);
    List<User> userList= userDataDao.selectAll();
    tvMain.setText(userList.toString());

room数据库的升级

数据库的升级是较危险的,很容易造成崩溃,客户端数据库的升级和服务器数据库升级场景也有所不同,设想一个场景,某一app升级版本,新版本中数据库表中字段出现增加,而又不想抛弃原有本机保存的数据。注意版本变化和不同的迁移策略,如1迁移到2 2迁移到3,当前版本如为3可能还需要添加1到3的方法,防止出现错误,下面是几种迁移例子:

    static Migration MISGRATION_1_2=new Migration(1,2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            //迁移范例1 创建新表
              database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                + "`name` TEXT, PRIMARY KEY(`id`))");
                   database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER");

            //迁移范例2 添加字段
                 database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER");

            //迁移范例3 遍历修改字段
            Cursor cursor= database.query("select * from User");
            do {
                while (cursor.moveToNext()) {
                    Log.e("TAG", "migrate: "+cursor.getString(cursor.getColumnIndex("password")) );
                    String sql="insert into User (password) values (?)";
                    SupportSQLiteStatement statement= database.compileStatement(sql);
                    statement.bindString(1, "22222");
                    statement.execute();
                }
            }while (cursor.moveToFirst());
        }
    };

其他

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

gradle此设置可以导出数据库相关信息,包括建表语句查询语句等。

吾生也有涯,而知也無涯。以有涯隨無涯,殆已

Android开发中多进程共享数据

# 背景 最近在工作中遇到一个需求,需要在接收到推送的时候将推送获得的数据存起来,以供app启动时使用。我们会认为这不是So easy吗?只要把数据存到SharedPreferences中,然后让app打开同一个SharedPreferences读取数据就可以了。但是在实际的测试中,我们发现推送进程存入的数据,并不能在app进程中获得。所以这是为什么呢,也许聪明的读者从我们上面的陈述中已经发现了原因,因为我们有两个进程,推送进程负责将推送数据存入,而app进程负责读取,但是正是由于是两个进程,如果它们同时存在,它们各自在内存中保持了自己的SP对象和数据,在推送进程中的存入并不能在app进程体现出来,并且可能会被app进程刷掉更改的数据。那么我们怎么做才能让这两边共享数据呢?请看下面陈述。

多进程支持的SharedPreferences(不推荐)

我们原来的做法是使用SharedPreferences, 自然而然想到,SharedPreferences 在MODE_PRIVATE MODE_PUBLIC 之外其实还可以设置多进程的Flag ———— MODE_MULTI_PROCESS

SharedPreferences myPrefs = context.getSharedPreferences(MY_FILE_NAME, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);

一旦我们设置了这个Flag,每次调用Context.getSharedPreferences 的时候系统会重新从SP文件中读入数据,因此我们在使用的时候每次读取和存入都要使用Context.getSharedPreferences 重新获取SP实例。即使是这样,由于SP本质上并不是多进程安全的,所以还是无法保证数据的同步,因此该方法我们并没有使用,我们也不推荐使用。

Tray

如果SP不是多进程安全的,那么是否有多进程安全的,又有SP功能的第三方项目呢。答案是有的,Tray——一个多进程安全的SharedPreferences,我们可以在Github上找到它,如果是AndroidStudio,可以直接使用Gradle引入,可谓是十分方便,如下是使用的代码,十分简单,没有apply commit,看起来比SP还要简单。

 // create a preference accessor. This is for global app preferences.
final AppPreferences appPreferences = new AppPreferences(getContext()); // this Preference comes for free from the library
// save a key value pair
appPreferences.put("key", "lorem ipsum");

// read the value for your key. the second parameter is a fallback (optional otherwise throws)
final String value = appPreferences.getString("key", "default");
Log.v(TAG, "value: " + value); // value: lorem ipsum

// read a key that isn‘t saved. returns the default (or throws without default)
final String defaultValue = appPreferences.getString("key2", "default");
Log.v(TAG, "value: " + defaultValue); // value: default

但是最终我们并没有选择使用它,主要的原因是它需要minSdk 为15,而我们是支持sdk14的,所以只能果断放弃了。

ContentProvider

既然Tray不支持sdk15以下的,那么我们是否可以使用Tray的原理自己实现一个呢?在阅读Tray的源码时我们发现其实它是在ContentProvider的基础上做的,而ContentProvider是Android官方支持的多进程安全的。以下是使用ContentProvider的一个例子。

public class ArticlesProvider extends ContentProvider {  
    private static final String LOG_TAG = "shy.luo.providers.articles.ArticlesProvider";  
  
    private static final String DB_NAME = "Articles.db";  
    private static final String DB_TABLE = "ArticlesTable";  
    private static final int DB_VERSION = 1;  
  
    private static final String DB_CREATE = "create table " + DB_TABLE +  
                            " (" + Articles.ID + " integer primary key autoincrement, " +  
                            Articles.TITLE + " text not null, " +  
                            Articles.ABSTRACT + " text not null, " +  
                            Articles.URL + " text not null);";  
  
    private static final UriMatcher uriMatcher;  
    static {  
            uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);  
            uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM);  
            uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID);  
            uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS);  
    }  
  
    private static final HashMap<String, String> articleProjectionMap;  
    static {  
            articleProjectionMap = new HashMap<String, String>();  
            articleProjectionMap.put(Articles.ID, Articles.ID);  
            articleProjectionMap.put(Articles.TITLE, Articles.TITLE);  
            articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT);  
            articleProjectionMap.put(Articles.URL, Articles.URL);  
    }  
  
    private DBHelper dbHelper = null;  
    private ContentResolver resolver = null;  
  
    @Override  
    public boolean onCreate() {  
            Context context = getContext();  
            resolver = context.getContentResolver();  
            dbHelper = new DBHelper(context, DB_NAME, null, DB_VERSION);  
  
            Log.i(LOG_TAG, "Articles Provider Create");  
  
            return true;  
    }  
  
    @Override  
    public String getType(Uri uri) {  
            switch (uriMatcher.match(uri)) {  
            case Articles.ITEM:  
                    return Articles.CONTENT_TYPE;  
            case Articles.ITEM_ID:  
            case Articles.ITEM_POS:  
                    return Articles.CONTENT_ITEM_TYPE;  
            default:  
                    throw new IllegalArgumentException("Error Uri: " + uri);  
            }  
    }  
  
    @Override  
    public Uri insert(Uri uri, ContentValues values) {  
            if(uriMatcher.match(uri) != Articles.ITEM) {  
                    throw new IllegalArgumentException("Error Uri: " + uri);  
            }  
  
            SQLiteDatabase db = dbHelper.getWritableDatabase();  
  
            long id = db.insert(DB_TABLE, Articles.ID, values);  
            if(id < 0) {  
                    throw new SQLiteException("Unable to insert " + values + " for " + uri);  
            }  
  
            Uri newUri = ContentUris.withAppendedId(uri, id);  
            resolver.notifyChange(newUri, null);  
  
            return newUri;  
    }  
  
    @Override  
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {  
            SQLiteDatabase db = dbHelper.getWritableDatabase();  
            int count = 0;  
  
            switch(uriMatcher.match(uri)) {  
            case Articles.ITEM: {  
                    count = db.update(DB_TABLE, values, selection, selectionArgs);  
                    break;  
            }  
            case Articles.ITEM_ID: {  
                    String id = uri.getPathSegments().get(1);  
                    count = db.update(DB_TABLE, values, Articles.ID + "=" + id  
                                    + (!TextUtils.isEmpty(selection) ? " and (" + selection + ‘)‘ : ""), selectionArgs);  
                    break;  
            }  
            default:  
                    throw new IllegalArgumentException("Error Uri: " + uri);  
            }  
  
            resolver.notifyChange(uri, null);  
  
            return count;  
    }  
  
    @Override  
    public int delete(Uri uri, String selection, String[] selectionArgs) {  
            SQLiteDatabase db = dbHelper.getWritableDatabase();  
            int count = 0;  
  
            switch(uriMatcher.match(uri)) {  
            case Articles.ITEM: {  
                    count = db.delete(DB_TABLE, selection, selectionArgs);  
                    break;  
            }  
            case Articles.ITEM_ID: {  
                    String id = uri.getPathSegments().get(1);  
                    count = db.delete(DB_TABLE, Articles.ID + "=" + id  
                                    + (!TextUtils.isEmpty(selection) ? " and (" + selection + ‘)‘ : ""), selectionArgs);  
                    break;  
            }  
            default:  
                    throw new IllegalArgumentException("Error Uri: " + uri);  
            }  
  
            resolver.notifyChange(uri, null);  
  
            return count;  
    }  
  
    @Override  
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {  
            Log.i(LOG_TAG, "ArticlesProvider.query: " + uri);  
  
            SQLiteDatabase db = dbHelper.getReadableDatabase();  
  
            SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();  
            String limit = null;  
  
            switch (uriMatcher.match(uri)) {  
            case Articles.ITEM: {  
                    sqlBuilder.setTables(DB_TABLE);  
                    sqlBuilder.setProjectionMap(articleProjectionMap);  
                    break;  
            }  
            case Articles.ITEM_ID: {  
                    String id = uri.getPathSegments().get(1);  
                    sqlBuilder.setTables(DB_TABLE);  
                    sqlBuilder.setProjectionMap(articleProjectionMap);  
                    sqlBuilder.appendWhere(Articles.ID + "=" + id);  
                    break;  
            }  
            case Articles.ITEM_POS: {  
                    String pos = uri.getPathSegments().get(1);  
                    sqlBuilder.setTables(DB_TABLE);  
                    sqlBuilder.setProjectionMap(articleProjectionMap);  
                    limit = pos + ", 1";  
                    break;  
            }  
            default:  
                    throw new IllegalArgumentException("Error Uri: " + uri);  
            }  
  
            Cursor cursor = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, TextUtils.isEmpty(sortOrder) ? Articles.DEFAULT_SORT_ORDER : sortOrder, limit);  
            cursor.setNotificationUri(resolver, uri);  
  
            return cursor;  
    }  

    @Override  
    public Bundle call(String method, String request, Bundle args) {  
            Log.i(LOG_TAG, "ArticlesProvider.call: " + method);  
  
            if(method.equals(Articles.METHOD_GET_ITEM_COUNT)) {  
                    return getItemCount();  
            }  
  
            throw new IllegalArgumentException("Error method call: " + method);  
    }  
  
    private Bundle getItemCount() {  
            Log.i(LOG_TAG, "ArticlesProvider.getItemCount");  
  
            SQLiteDatabase db = dbHelper.getReadableDatabase();  
            Cursor cursor = db.rawQuery("select count(*) from " + DB_TABLE, null);  
  
            int count = 0;  
            if (cursor.moveToFirst()) {  
                    count = cursor.getInt(0);  
            }  
  
            Bundle bundle = new Bundle();  
            bundle.putInt(Articles.KEY_ITEM_COUNT, count);  
  
            cursor.close();  
            db.close();  
  
            return bundle;  
    }  
  
    private static class DBHelper extends SQLiteOpenHelper {  
            public DBHelper(Context context, String name, CursorFactory factory, int version) {  
                    super(context, name, factory, version);  
            }  
  
            @Override  
            public void onCreate(SQLiteDatabase db) {  
                    db.execSQL(DB_CREATE);  
            }  
  
            @Override  
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
                    db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);  
                    onCreate(db);  
            }  
    }  
}   我们需要创建一个类继承自ContentProvider,并重载以下方法。 - onCreate(),用来执行一些初始化的工作。 - query(Uri, String[], String, String[], String),用来返回数据给调用者。 - insert(Uri, ContentValues),用来插入新的数据。 - update(Uri, ContentValues, String, String[]),用来更新已有的数据。 - delete(Uri, String, String[]),用来删除数据。 - getType(Uri),用来返回数据的MIME类型。

具体使用参考 Android应用程序组件Content Provider应用实例这篇博客,这里不再赘述。 在以上对ContentProvider的使用过程中,我们发现过程比较繁琐,如果对于比较复杂的需求可能还比较使用,但是我们这里的需求其实很简单,完全不需要搞得那么复杂,所以最后我们也没有使用这个方法(你可以理解为本博主比较Lazy)。

#Broadcast 那么是否有更简单的方法呢?由于想到了ContentProvider,我们不由地想到另一个android组件,BroadcastReceiver。那么我们是否可以使用Broadcast 将我们收到的推送数据发送给app进程呢。bingo,这似乎正是我们寻找的又简单又能解决问题的方法。我们来看下代码。

首先在推送进程收到推送消息时,我们将推送数据存入SP,如果这时候没有app进程,那么下次app进程启动的时候该存入的数据就会被app进程读取到。而如果这时候app进程存在,那么之后的代码就会生效,它使用LocalBroadcastManager 发送一个广播。LocalBroadcastManager发送的广播不会被app之外接收到,通过它注册的Receiver也不会接收到app之外的广播,因此拥有更高的效率。

pushPref.add(push);

Intent intent = new Intent(PushHandler.KEY_GET_PUSH);
intent.putExtra(PushHandler.KEY_PUSH_CONTENT, d);
LocalBroadcastManager.getInstance(context).sendBroadcastSync(intent);

而我们在app进程则注册了一个BroadReceiver来接收上面发出的广播。在收到广播之后将推送数据存入SP。

public class PushHandler {

public static String KEY_GET_PUSH = "PUSH_RECEIVED";
public static String KEY_PUSH_CONTENT = "PUSH_CONTENT";

// region 推送处理push
/**
 * 当有推送时,发一次请求mPushReceiver
 */
private static BroadcastReceiver mPushReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        Timber.i("在NoticeAction中收到广播");
        PushPref pushPref = App.DI().pushPref();
        try {
            String pushContent = intent.getStringExtra(KEY_PUSH_CONTENT);
            PushEntity pushEntity = App.DI().gson().fromJson(pushContent, PushEntity.class);
            pushPref.add(pushEntity);
        } catch (Exception e){
            Timber.e(e, "存储推送内容出错");
        }
    }
};

public static  void startListeningToPush(){
    try {
        LocalBroadcastManager.getInstance(App.getContext()).registerReceiver(mPushReceiver, new IntentFilter(KEY_GET_PUSH));
    } catch (Exception e) {
        Timber.e(e, "wtf");
    }
}
public static void stopListeningToPush(){
    try {
        LocalBroadcastManager.getInstance(App.getContext()).unregisterReceiver(mPushReceiver);
    } catch (Exception e) {
        Timber.e(e, "wtf");
    }
}
// endregion
} 该方法相对于上面的方法使用简单,安全可靠,能够比较好的实现我们的需求。不过,在需求比较复杂的时候还是建议使用ContentProvider,因为毕竟这样的方法不是堂堂正道,有种剑走偏锋的感觉。

总结

实现一个需求可以有很多方法,而我们需要寻找的是又简单有可靠的方法,在写代码之前不如多找找资料,多听听别人的意见。

推荐:

不得不知Git远程操作详解

以上是关于android 开发进程 0.37 room数据存储的使用的主要内容,如果未能解决你的问题,请参考以下文章

Android移动应用开发之使用room实现数据库的增删改查

Android开发笔记(一百七十五)利用Room简化数据库操作

Android开发笔记(一百七十五)利用Room简化数据库操作

Android开发笔记(一百七十五)利用Room简化数据库操作

安卓开发Android平台的记账本app(全部代码+room框架操作数据库+设计报告)

Android jetpack room 记录数据库升级日志