Save data in a local database using Room

本文介绍如何使用Room库在Android应用中高效地存储和检索数据。Room提供了一个在SQLite之上的抽象层,允许开发者以类型安全的方式访问数据库。文章详细解释了如何定义实体、DAO和数据库,以及如何使用Room进行插入、更新、删除和查询操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文是翻译的Room的官方文档 Save data in a local database using Room

若翻译理解有误请跳转原文链接。

Overview

Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.

Room是在SQLite之上的一个抽象层,使开发者可以流畅的驾驭SQLite的全部功能。

主要分为三个部分:

  • Database

    A database holder, main acces point for connection to your raltional data.

    At runtime, you can acquire an instance of Database by calling Room.databaseBuilder().

    它是被@Database标记的类,并满足以下的条件:

    • 包含与table对应的所有实体类。
    • 是一个继承RoomDatabse的抽象类。
    • 包含没有参数并且返回DAO对象的抽象方法。
  • Entity

    代表database中的table。

  • DAO

    包含可以访问database的方法。

Room architecture diagram, like this :

room architecture diagram

sample code snippet:


	@Entity
	public class User {
	    @PrimaryKey
	    public int uid;
	
	    @ColumnInfo(name = "first_name")
	    public String firstName;
	
	    @ColumnInfo(name = "last_name")
	    public String lastName;
	}


	@Dao
	public interface UserDao {
	    @Query("SELECT * FROM user")
	    List<User> getAll();
	
	    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
	    List<User> loadAllByIds(int[] userIds);
	
	    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
	           "last_name LIKE :last LIMIT 1")
	    User findByName(String first, String last);
	
	    @Insert
	    void insertAll(User... users);
	
	    @Delete
	    void delete(User user);
	}


	@Database(entities = {User.class}, version = 1)
	public abstract class AppDatabase extends RoomDatabase {
	    public abstract UserDao userDao();
	}


	AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        	AppDatabase.class, "database-name").build();

Defining data using Room entities

For each entity, a table is created within the associated Database object to hold the items

🌟Note:

  • Room must have access to entity field, public or provide getter/setter.
  • Entities can have either an empty constructor or not , but constructor parameters belong to the entity fields.

Use a primary key

  • 每个Entity都必须有至少一个primary key,使用@PrimaryKey annotation。

    @Entity
    public class User {
        @PrimaryKey
        public int id;
    
        public String firstName;
        public String lastName;
    }
    
  • 默认情况下,Room使用Entity的类名作为table的名,但是也可以使用tableName属性设置不同的名,field对应table的column,使用@ColumnInfo的name属性。

    
    	@Entity(tableName = "users")
    	public class User {
    	    @ColumnInfo(name = "first_name")
    		public String firstName;
    	}
    	
    
  • 分配自动IDs, 使用 @PrimaryKey’s autoGenerate property.

    
    	@Entity
    	public class User {
    	    @PrimaryKey(autoGenerate = true)
    	    public int id;
    	
    	    public String firstName;
    	    public String lastName;
    	}
    	
    
  • 复合 primary key, 使用@Entit‘s primaryKeys property.

    
    	@Entity(primaryKeys = {"firstName", "lastName"})
    	public class User {
    	    public String firstName;
    	    public String lastName;
    	}
    	
    

Ignore fileds

  • 默认情况下,entity中的field和table中的column对应,如果不希望某个field在table中保存,可以使用@Ignore。

    
    	@Entity
    	public class User {
    	    @PrimaryKey
    	    public int id;
    	
    	    public String firstName;
    	    public String lastName;
    	
    	    @Ignore
    	    Bitmap picture;
    	}
    
    

    如果field继承自父类,使用@Entity’s ignoreColumns property

    
    	@Entity(ignoredColumns = "picture")
    	public class RemoteUser extends User {
    	    @PrimaryKey
    	    public int id;
    	
    	    public boolean hasVpn;
    	}
    
    

Fields be unique

by setting the unique property of an @Index annotation to true.

	@Entity(indices = {@Index(value = {"first_name", "last_name"},
	        unique = true)})
	public class User {
	    @PrimaryKey
	    public int id;
	
	    @ColumnInfo(name = "first_name")
	    public String firstName;
	
	    @ColumnInfo(name = "last_name")
	    public String lastName;
	
	    @Ignore
	    Bitmap picture;
	}

Define relationships between objects

Most object-relational mapping libraries allow entity objects to reference each other, but Room explicitly forbids this.

like this :


	@Entity
	public class User {
		String name;
		
		Book book;//Room明确禁止这样做.
	}

🌟Note :

  • Room虽然Room不能使用直接的引用关系,但是允许你定义实体之间外键约束。

Define one-to-many relationships

Zero or more instances of Book can be linked to a single instance of User through the user_id foreign key.

Sample code snippet:


	@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;
	}

Foreign keys功能非常强大,因为它在更新referenced entity的时候还可以指定database做些什么。

一个例子,使用 @ForeignKey 's property onDelete = CASCADE,你可以告诉database在删除一个user的时候删除user对应的所有books。

Create nested objects

🌟Note: Is not reference objects.

在database table包含多个column,但是你希望拥有在database logic中的内聚整体,也就是domain model。

总之就是,domain model is some subfields within a table。

Sample code snippet:


	public class Task {//domain model
	    public String name;
	    public long date;
	}
	
	@Entity(name = "task")
	public class TaskEntity {
	    @PrimaryKey public int id;
	    
	    @Embedded public Task task;
	}

🌟Note:

  • @Embedded 's property prefix can keep table column unique.

Define many-to-many relationships

where each entity can be linked to zero or more instances of the other.

一个例子,音乐App中,每个song可以添加到多个playlist,每个playlist也可以有很多song。

To model this relationship, you will need to create three objects:

  • An entity class for the playlists.
  • An entity class for the songs.
  • An intermediate class to hold the information about which songs are in each playlist.

Sample code snippet:


	@Entity
	public class Playlist {
	    @PrimaryKey public int id;
	
	    public String name;
	    public String description;
	}
	
	@Entity
	public class Song {
	    @PrimaryKey public int id;
	
	    public String songName;
	    public String artistName;
	}


	@Entity(tableName = "playlist_song_join",
	        primaryKeys = { "playlistId", "songId" },
	        foreignKeys = {
	                @ForeignKey(entity = Playlist.class,
	                            parentColumns = "id",
	                            childColumns = "playlistId"),
	                @ForeignKey(entity = Song.class,
	                            parentColumns = "id",
	                            childColumns = "songId")
	                })
	public class PlaylistSongJoin {
	    public int playlistId;
	    public int songId;
	}


	@Dao
	public interface PlaylistSongJoinDao {
	    @Insert
	    void insert(PlaylistSongJoin playlistSongJoin);
	
	    @Query("SELECT * FROM playlist " +
	           "INNER JOIN playlist_song_join " +
	           "ON playlist.id=playlist_song_join.playlistId " +
	           "WHERE playlist_song_join.songId=:songId")
	    List<Playlist> getPlaylistsForSong(final int songId);
	
	    @Query("SELECT * FROM song " +
	           "INNER JOIN playlist_song_join " +
	           "ON song.id=playlist_song_join.songId " +
	           "WHERE playlist_song_join.playlistId=:playlistId")
	    List<Song> getSongsForPlaylist(final int playlistId);
	}

Create views into a database

在version 2.1.0或者更高版本,Room提供了SQLite database views

Create a database view

Add the @DatabaseView annotation to a class. Set the annotation’s value to the query that the class should represent.


	@DatabaseView("SELECT user.id, user.name, user.departmentId," +
	              "department.name AS departmentName FROM user " +
	              "INNER JOIN department ON user.departmentId = department.id")
	public class UserDetail {
	    public long id;
	    public String name;
	    public long departmentId;
	    public String departmentName;
	}
	

Associate a view with your database

	
	@Database(entities = {User.class}, views = {UserDetail.class},
	          version = 1)
	public abstract class AppDatabase extends RoomDatabase {
	    public abstract UserDao userDao();
	}

Accessing data using Room DAOs

Data access objects, or DAOs,是Room Architecture中最要的组成部分。
Room会在编译时为每一个Dao创建实现类Dao_impl。

Define methods for convenience

常见的方法使用方式。

Insert

Room会生成向database table新增所有的entities的单个事务。


	@Dao
	public interface MyDao {
	    @Insert(onConflict = OnConflictStrategy.REPLACE)
	    public void insertUsers(User... users);
	
	    @Insert
	    public void insertBothUsers(User user1, User user2);
	
	    @Insert
	    public void insertUsersAndFriends(User user, List<User> friends);
	}

被@Insert标记的方法也可以有long类型的返回值,代表新增的行的rowID,如果方法参数是array / collection,则可以返回long[ ] / List<Long>。

Update

被@Update标记的方法,在database中是使用查询匹配每一个参数的primary key,然后update。


	@Dao
	public interface MyDao {
	    @Update
	    public void updateUsers(User... users);
	}

方法也可以有int类型的返回值,代表有多少行被影响。

Delete

被@Delete标记的方法,在database中是使用查询匹配每一个参数的primary key,然后delete。


@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

方法也可以有int类型的返回值,代表有多少行被影响。

Query for information

@Query是Dao class中比较特殊的注解,它允许你基于一个database进行read/write操作。

每一个@Query标记的方法都会在编译时被检查,如果@Query中包含语法错误,编译将报错并被出错误信息。

Sample code snippet:


	@Dao
	public interface MyDao {
	    @Query("SELECT * FROM user")
	    public User[] loadAllUsers();
	}

Passing parameters into the query

将方法的参数传递到query语句中。

At compile,Room matches the :minAge bind parameter with the minAge method parameter.


	@Dao
	public interface MyDao {
	    @Query("SELECT * FROM user WHERE age > :minAge")
	    public User[] loadAllUsersOlderThan(int minAge);
	}
	
	@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

Returning subsets of columns

查询返回table column的子集。

大多数情况下,你只需要查询部分的fields,比如只用于UI显示,Room可以满足这样的场景。


	public class NameTuple {
	    @ColumnInfo(name = "first_name")
	    public String firstName;
	
	    @ColumnInfo(name = "last_name")
	    @NonNull
	    public String lastName;
	}


	@Dao
	public interface MyDao {
	    @Query("SELECT first_name, last_name FROM user")
	    public List<NameTuple> loadFullName();
	}

Passing a collection of arguments

传递集合参数到查询语句中


	@Dao
	public interface MyDao {
	    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
	    public List<NameTuple> loadUsersFromRegions(List<String> regions);
	}

Observable queries

执行查询时,如果你希望你的应用程序的UI在数据更改时自动更新。


	@Dao
	public interface MyDao {
	    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
	    public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
	}

Room会生成必要的代码以便在database数据被更新的时候触发LiveData

Reactive queries with RxJava

Room也支持返回RxJava2的相关类型。

Configure build.gradle to support rxjava2


	dependencies {
	    def room_version = "2.1.0"
	    implementation 'androidx.room:room-rxjava2:$room_version'
	}


	@Dao
	public interface MyDao {
	    @Query("SELECT * from user where id = :id LIMIT 1")
	    public Flowable<User> loadUserById(int id);
	
	    // Emits the number of users added to the database.
	    @Insert
	    public Maybe<Integer> insertLargeNumberOfUsers(List<User> users);
	
	    // Makes sure that the operation finishes successfully.
	    @Insert
	    public Completable insertLargeNumberOfUsers(User... users);
	
	    /* Emits the number of users removed from the database. Always emits at
	       least one user. */
	    @Delete
	    public Single<Integer> deleteUsers(List<User> users);
	}

Querying multiple tables

多表查询。


	@Dao
	public interface MyDao {
	   @Query("SELECT user.name AS userName, pet.name AS petName " +
	          "FROM user, pet " +
	          "WHERE user.id = pet.user_id")
	   public LiveData<List<UserPet>> loadUserAndPetNames();
	
	   // You can also define this class in a separate file, as long as you add the
	   // "public" access modifier.
	   static class UserPet {
	       public String userName;
	       public String petName;
	   }
	}

Referencing complex data using Room

引用复杂数据。

Room允许在基本数据类型和装箱类型之间的转换,但是不允许reference。

Use type converters

使用类型转换器 @TypeConverter。

例如,如果要保留Date类型的实例,可以编写以下代码,以将等效的Unix时间戳存储在数据库中。

  • 定义TypeConverter class:

    	
    	public class Converters {
    	    @TypeConverter
    	    public static Date fromTimestamp(Long value) {
    	        return value == null ? null : new Date(value);
    	    }
    	
    	    @TypeConverter
    	    public static Long dateToTimestamp(Date date) {
    	        return date == null ? null : date.getTime();
    	    }
    	}
    
    
  • 接下来,将@TypeConverter注解添加到抽象的Database类上,这样Room就可以使用定义好的类型转换器:

    
    	@Database(entities = {User.class}, version = 1)
    	@TypeConverters({Converters.class})
    	public abstract class AppDatabase extends RoomDatabase {
    	    public abstract UserDao userDao();
    	}
    
    
    • 然后,你就可以像使用基本数据类型一样使用自定义的类型了:
    
    	@Entity
    	public class User {
    	    private Date birthday;
    	}
    
    
    
    	@Dao
    	public interface UserDao {
    	    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    	    List<User> findUsersBornBetweenDates(Date from, Date to);
    	}
    
    

Understand why Room doesn’t allow object references

理解为什么Room禁止Object-relational mappings。

  • Object-relational mappings通常是延迟加载,它发生在主线程上,但是客户端的主线程非常敏感,然而如果你不使用延迟加载,可能会获取更多不必须的数据,从而造成更多内存消耗问题。
  • Object-relational mappings通常由开发人员来决定程序和UI之间的共享模型,但是这个共享模型不能很好的扩展,因为随着UI的变化,共享数据变得很难预料和调试。

总之,Room觉得,要达到多个对象之间的关系映射,可以使用这样的方式更好:

  • 创建一个包含所需要的Field的POJO
  • 然后编写一个连接相应表的多表查询(Querying multiple tables)

这种方式和Room强大的查询验证功能结合起来,可以使你的应用程序在加载数据的时候消耗更少的资源,从而改善应用程序的性能和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值