我可以在 Room 中添加检查约束吗?

Can I add check constraints in Room?

在我的数据库中,我有 3 个 table:problemsusersrolesroles table 有两个值:clientcoach

数据库规则:

  1. 一个用户只有一个角色(客户或教练)。
  2. 一个问题有一位教练和一位客户。

problems 实体有两个指向 users table 的外键:coach_id client_id。我可以使用 Android Room 添加检查约束以确保问题符合此规则吗?

我想避免一个问题有两个客户或两个教练的情况。

数据库概念图

目前ROOM不支持添加CHECK约束。

但是,您可以通过删除 table 然后创建 table 来引入 CHECK 约束(可能不可取,因为这可能会引入持续的并发症,例如在迁移时)

  • 但是,CHECK 约束是有限的,不能包括子查询,我认为这会使事情复杂化,甚至可能排除 suitable/usable CHECK 约束。

    • 我相信您会看到类似 coach INTEGER CHECK((SELECT authority FROM user JOIN role ON role.roleid = user.role) = 0) REFERENCES user(userid) ON DELETE CASCADE ON UPDATE CASCADE, 的内容(假设,由于不清楚哪一列表示角色类型,教练必须拥有权限为 0 的角色)。但是,不能使用 CHECK,因为它会导致 subqueries prohibited in CHECK constraints 错误。

因此,在插入问题的过程中以编程方式进行此类检查可能会更简单。也就是说,对于每种类型 (coach/client),如果指定了不正确的角色,则从用户那里获取角色并拒绝实际插入到数据库中。这更类似于 Room 的 OO 立场(即 tables 支持并根据对象创建)。

例子

或许根据你的问题考虑以下几点:-

Role.java(角色Table)

@Entity(tableName = "role")
public class Role {

    public static final int AUTHORITY_COACH = 0;
    public static final int AUTHORITY_CLIENT = 1;

    @PrimaryKey
    Long roleid;
    String description;
    int authority;

    public Role(String description, int authority) {
        this.description = description;
        if ( authority >=  AUTHORITY_CLIENT) {
            this.authority = AUTHORITY_CLIENT;
        } else {
            this.authority = AUTHORITY_COACH;
        }
    }

    public Long getRoleid() {
        return roleid;
    }

    public void setRoleid(Long roleid) {
        this.roleid = roleid;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getAuthority() {
        return authority;
    }

    public void setAuthority(int authority) {
        this.authority = authority;
    }
}

User.java(用户 table)

@Entity(
        tableName = "user",
        foreignKeys = {
                @ForeignKey(
                        entity = Role.class,
                        parentColumns = {"roleid"},
                        childColumns = {"role"},
                        onDelete = ForeignKey.CASCADE,
                        onUpdate = ForeignKey.CASCADE
                        )
        },
        indices = {
                @Index("role")
        }
)
public class User {

    @PrimaryKey
    Long userid;
    String name;
    String surname;
    String username;
    String email;
    String password;
    long role;

    public User(String name, String surname, String username, String email, String password, long role) {
        this.name = name;
        this.surname = surname;
        this.username = username;
        this.email = email;
        this.password = password;
        this.role = role;
    }

    public Long getUserid() {
        return userid;
    }

    public void setUserid(Long userid) {
        this.userid = userid;
    }

    public String getName() {
        return name;
    }

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

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public long getRole() {
        return role;
    }

    public void setRole(long role) {
        this.role = role;
    }
}

Problem.java(问题Table)

@Entity(
        tableName = "problem",
        foreignKeys = {
                @ForeignKey(
                        entity = User.class,
                        parentColumns = {"userid"},
                        childColumns = {"coach"},
                        onDelete = ForeignKey.CASCADE,
                        onUpdate = ForeignKey.CASCADE
                ),
                @ForeignKey(
                        entity = User.class,
                        parentColumns = {"userid"},
                        childColumns = {"client"},
                        onDelete = ForeignKey.CASCADE,
                        onUpdate = ForeignKey.CASCADE
                )
        },
        indices = {
                @Index(value = "coach"),
                @Index(value = "client")
        }
)
public class Problem {

    @PrimaryKey
    Long problemid;
    String title;
    String status;
    long coach;
    long client;

    public Problem(){}

    public Problem(String title, String status, User coach, User client) {
        this.title = title;
        this.status = status;
        this.coach = coach.getUserid();
        this.client = client.getUserid();
    }

    public Long getProblemid() {
        return problemid;
    }

    public void setProblemid(Long problemid) {
        this.problemid = problemid;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public long getCoach() {
        return coach;
    }

    public void setCoach(long coach) {
        this.coach = coach;
    }

    public long getClient() {
        return client;
    }

    public void setClient(long client) {
        this.client = client;
    }
}

UserWithRole.java(POJO 将 User 与 User 的角色结合起来)

public class UserWithRole {

    @Embedded
    User user;
    @Relation(entity = Role.class,entityColumn = "roleid",parentColumn = "role")
    Role userrole;

    public UserWithRole(){}

    public User getUser() {
        return user;
    }

    public Role getUserrole() {
        return userrole;
    }
}
  • 未实际使用(但可用于检查)

AllDao.java(数据访问全部合并)

@Dao
public interface AllDao {

    @Insert
    long insertRole(Role role);

    @Insert
    long insertUser(User user);

    @Insert
    long insertProblem(Problem problem);

    @Query("SELECT (authority = " + Role.AUTHORITY_COACH + ") FROM user JOIN role ON role.roleid = user.role WHERE userid = :userid ")
    boolean isUserACoach(long userid);

    @Query("SELECT * FROM user WHERE userid = :userid")
    User getUserById(long userid);

}
  • isUuserACoach 是复制上面假设的 CHECK 约束的重要查询。

Database.java(房间@Database)

@androidx.room.Database(version = 1,entities = {Role.class,User.class,Problem.class})
public abstract class Database extends RoomDatabase {

    abstract AllDao allDao();
}

MainActivity.java(全部测试)

public class MainActivity extends AppCompatActivity {
    Database database;
    AllDao allDao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        database = Room.databaseBuilder(
                this,
                Database.class,
                "mydb"
        )
                .allowMainThreadQueries()
                .build();
        allDao = database.allDao();

        long coachRole = allDao.insertRole(new Role("Coach",Role.AUTHORITY_COACH));
        long newClientRole = allDao.insertRole(new Role("New Client",Role.AUTHORITY_CLIENT));
        long oldClientRole = allDao.insertRole(new Role("Old Client",Role.AUTHORITY_CLIENT));

        long fredBloggs = allDao.insertUser(new User("Fred","Bloggs","fblog","fblog@mail.com","password",coachRole));
        long marySmith = allDao.insertUser(new User("Mary","Smith","msmith","msmith@mail.com","password",newClientRole));
        long anneWalker = allDao.insertUser(new User("Anne","Walker","awalker","awalkjer@mail.com","password",oldClientRole));

        //<<<<<<<<<< SHOULD BE OK TO ADD >>>>>>>>>>
        if (verifyAndAddProblem("Problem 1","new",allDao.getUserById(fredBloggs),allDao.getUserById(marySmith)) > 0) {
            Log.d("PROBLEMADDRESULT","Problem Added");
        } else {
            Log.d("PROBLEMADDRESULT","Problem not added");
        }

        //<<<<<<<<<< SHOULD NOT BE ADDED (annWalker is NOT a coach) >>>>>>>>>>
        if (verifyAndAddProblem("Problem 2","new",allDao.getUserById(anneWalker),allDao.getUserById(marySmith)) > 0) {
            Log.d("PROBLEMADDRESULT","Problem Added");
        } else {
            Log.d("PROBLEMADDRESULT","Problem not added");
        }

        //<<<<<<<<<< SHOULD NOT BE ADDED (fredBloggs is a coach BUT is NOT a client) >>>>>>>>>>
        if (verifyAndAddProblem("Problem 3","new",allDao.getUserById(fredBloggs),allDao.getUserById(fredBloggs)) > 0) {
            Log.d("PROBLEMADDRESULT","Problem Added");
        } else {
            Log.d("PROBLEMADDRESULT","Problem not added");
        }
    }

    //ALTERNATIVE TO CHECK CONSTRAINT DONE HERE
    private long verifyAndAddProblem(String title, String status, User coach, User client) {

        long rv = -1;
        if (!allDao.isUserACoach(coach.getUserid()) || allDao.isUserACoach(client.getUserid())) return rv;
        return allDao.insertProblem(new Problem(title,status,coach,client));
    }
}

当 运行 日志包含:-

2020-01-02 08:47:33.647 D/PROBLEMADDRESULT: Problem Added
2020-01-02 08:47:33.658 D/PROBLEMADDRESULT: Problem not added
2020-01-02 08:47:33.661 D/PROBLEMADDRESULT: Problem not added