代码展示
1.数据实体类
@Entity
public class User {
@PrimaryKey(autoGenerate = true)
private long id;
private String name;
private String age;
private String sex;
public User(String name, String age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
说明:用户表,包括id(作为主键,自动递增)、name、age、sex四个字段
2.Dao类
@Dao
public interface UserDao {
@Update
void updateUser(User user);
}
说明:只展示更新语句
3.用户仓库类
/**
* 用户仓库类,负责用户数据的增删改查操作。
*/
public class UserRepository {
private final UserDao userDao;
/**
* 构造函数,初始化用户数据访问对象。
*
* @param database 应用数据库实例。
*/
public UserRepository(AppDatabase database) {
this.userDao = database.getUserDao();
}
/**
* 更新用户信息。如果用户不存在,则操作失败。
*
* @param user 要更新的用户对象。
* @return 返回操作结果,成功或失败。
*/
@Transaction
public UserRepositoryResult updateUser(User user) {
// 确认用户存在
if (userDao.findUserByName(user.getName()) == null) {
return new UserRepositoryResult(
UserRepositoryResult.Type.FAILURE,
RESULT_MESSAGE_USER_NOT_EXIST);
}
userDao.updateUser(user);
return UserRepositoryResult.success();
}
}
说明:只展示update实现
4.MainViewModel
/**
* 主要的ViewModel类,用于处理与用户相关的数据操作。
*/
public class MainViewModel extends ViewModel {
// 用户数据仓库接口
private final UserRepository userRepository;
// 执行器服务,用于在后台线程中执行数据库操作
private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
/**
* 构造函数,初始化用户数据仓库。
*
* @param userRepository 用户数据仓库实例。
*/
public MainViewModel(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 用于存储更新用户操作结果的LiveData对象
private final MutableLiveData<UserRepositoryResult> updateUserResult = new MutableLiveData<>();
/**
* 获取更新用户操作的结果。
*
* @return UserRepositoryResult 更新操作的结果。
*/
public LiveData<UserRepositoryResult> getUpdateUserResult() {
return updateUserResult;
}
// 更新用户
public void updateUser(final User user) {
EXECUTOR_SERVICE.execute(() -> {
updateUserResult.postValue(userRepository.updateUser(user));
});
}
/**
* ViewModel工厂类,用于创建MainViewModel实例。
*/
public static class Factory extends ViewModelProvider.NewInstanceFactory {
// 用户数据仓库实例
private final UserRepository userRepository;
/**
* 构造函数,初始化用户数据仓库。
*
* @param userRepository 用户数据仓库实例。
*/
public Factory(UserRepository userRepository) {
this.userRepository = userRepository;
}
/**
* 创建MainViewModel实例。
*
* @param modelClass ViewModel的类类型。
* @return MainViewModel 实例。
*/
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new MainViewModel(userRepository);
}
}
}
5.MainActivity
/**
* 主活动类,负责管理应用程序的主要界面。
*/
public class MainActivity extends AppCompatActivity {
private MainViewModel viewModel; // 视图模型,用于管理活动背后的业务逻辑
private ActivityMainBinding binding; // 数据绑定实例,用于简化UI更新
private UserRepository userRepository;
/**
* 在活动创建时调用。
*
* @param savedInstanceState 如果活动之前被销毁,这参数包含之前的状态。如果活动没被销毁之前,这参数是null。
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 启用边缘到边缘的UI
EdgeToEdge.enable(this);
// 设置数据绑定
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 设置视图的内边距,以适应系统栏位的高度
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
userRepository = new UserRepository(AppDatabase.getDatabase(MVVMApplication.getInstance()));
// 初始化视图模型
viewModel = new ViewModelProvider(this, new MainViewModel.Factory(userRepository)).get(MainViewModel.class);
binding.setViewModel(viewModel);
initListeners();
initObserver();
}
/**
* 初始化视图监听器。
*/
private void initListeners() {
// 清空输入框操作监听
binding.btnClearEdit.setOnClickListener(v -> {
clearEditText();
});
// 更新用户操作监听
binding.btnUpdate.setOnClickListener(v -> {
Log.i("swy","update按钮被点击");
User userChecked = checkUserInfo();
if (userChecked != null) {
Log.i("swy","更新用户");
viewModel.updateUser(userChecked);
}
});
}
private void initObserver() {
// 观察更新结果
viewModel.getUpdateUserResult().observe(this, result -> {
Log.i("swy","updateresult发生变化");
if (result.getType() == UserRepositoryResult.Type.SUCCESS) {
showToast("更新成功");
} else {
showToast(result.getErrorMessage());
}
});
}
// 封装对用户信息输入的验证
private User checkUserInfo() {
String name = binding.etName.getText().toString();
if (name.isEmpty()) {
showToast("请输入姓名");
return null;
}
String age = binding.etAge.getText().toString();
if (age.isEmpty()) {
showToast("请输入年龄");
return null;
}
String sex = binding.etSex.getText().toString();
if (sex.isEmpty()) {
showToast("请输入性别");
return null;
}
return new User(name, age, sex);
}
// 封装对姓名输入的检查
private String checkName() {
String name = binding.etName.getText().toString();
if (name.isEmpty()) {
showToast("请输入姓名");
return "";
}
return name;
}
// 清除编辑文本框中的内容
private void clearEditText() {
binding.etName.setText("");
binding.etAge.setText("");
binding.etSex.setText("");
}
// 简化Toast消息的显示
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
至此,一套MVVM架构的Room数据库Update的语法即调用完成,感兴趣的同学可以多看一下这个流程和代码,观察一下是否有问题,多观察几分钟,分析一下流程,因为或许你正在跟我一样经历这个问题,或者未来可能被这类问题给绕进去。
好的,直接来说结果,上面的代码看似是完整的,且调用无误的。
但是里面有一个比较大的问题,这个问题在根儿上,也就是数据实体类,我们再次查看数据实体
@PrimaryKey(autoGenerate = true)
private long id;
private String name;
private String age;
private String sex;
前面也说了,这里面id为自增主键。
学过数据库的都知道,主键是用来区分两条数据的,比如说我这里有两条数据
id = 1,name = 张三,age = 18,sex = 男
和
id = 2,name = 张三,age = 18,sex = 男
假如有这么两条数据,id为主键的话,虽然名字年龄性别都是一样的张三,但是对于数据库而言,这是两条数据,对于用户表而言,这是两个同名的人
比如说,我给用户展现的是这样一个页面,支持常规的增删改查,但是用户只可以输入name、age、sex这三个数据
如果第一次用户输入了“name = 张三,age = 18,sex = 男”,点击增加按钮,显然是可以成功的
那么当用户再次输入“name = 张三,age = 18,sex = 男”,这里就有两种做法
第一种:允许输入
上面说了,user表以id为主键,所以从insert语句的执行来看,没有问题,就是完完全全的两条数据,这也是比较符合现实的,因为中国这么大,人这么多,肯定有很频繁的重名现象,所以一般我们来区分人并不是以姓名来区分,而是采用身份证号(ID),而后再因为身份证号码只有一个且比较重要不能随便示人,同时又要保证关联到人,我们现在一般用手机号来区分人,当然,手机号都有实名认证,即关联了身份证,且一个身份证可以办理多个手机号。当然,那个问题与本文无关,只是说到这里了。
第二种:不允许输入
这种设计方案一般在游戏里面比较常见,比如说,游戏刚开服,大家一窝蜂冲进去很重要一件事是干什么,抢注游戏昵称,因为如果某一个昵称比较好听或者寓意比较好,大家都想用,但是游戏的系统只允许一个人用,也就是不允许相同昵称出现这种策略。当然现在的大部分新游戏都是支持相同昵称的,所以在那些游戏的个人页面都支持一键赋值uid,这个uid就是游戏里面的身份证了,然后我们通过uid进行好友查找。
好了,上面描述了两种管理用户的策略,只是作为辅助思考
回归正题,讨论我前面展示的代码中的错误问题
通过上面的分析,我们知道了把id作为主键之后,其他属性并不足以区分用户这件关键信息了
所以来看这两部分的代码
1.从用户输入,组装更新User对象
// 封装对用户信息输入的验证
private User checkUserInfo() {
String name = binding.etName.getText().toString();
if (name.isEmpty()) {
showToast("请输入姓名");
return null;
}
String age = binding.etAge.getText().toString();
if (age.isEmpty()) {
showToast("请输入年龄");
return null;
}
String sex = binding.etSex.getText().toString();
if (sex.isEmpty()) {
showToast("请输入性别");
return null;
}
return new User(name, age, sex);
}
// 更新用户操作监听
binding.btnUpdate.setOnClickListener(v -> {
Log.i("swy","update按钮被点击");
User userChecked = checkUserInfo();
if (userChecked != null) {
Log.i("swy","更新用户");
viewModel.updateUser(userChecked);
}
});
2.传入user对象,执行Update操作
@Transaction
public UserRepositoryResult updateUser(User user) {
// 确认用户存在
if (userDao.findUserByName(user.getName()) == null) {
return new UserRepositoryResult(
UserRepositoryResult.Type.FAILURE,
RESULT_MESSAGE_USER_NOT_EXIST);
}
userDao.updateUser(user);
return UserRepositoryResult.success();
}
@Update
void updateUser(User user);
其实,说到这里,我相信大家已经看出来哪里出现问题了
我就不妨再说的细致一些吧,这是Room框架对于@Update注解的处理逻辑:
当您使用@Update注解进行用户数据更新时,Room库会根据传入的User对象的主键(即id)来定位到要更新的记录。具体处理逻辑如下:
a. 查找匹配的主键:Room会根据User对象的id值在数据库中查找具有相同id的记录。
b. 执行更新:如果找到了匹配的记录,则按照传入的User对象中的非空属性值更新对应的字段。这意味着如果您仅修改了name、age或sex中的某个属性值,并将这个更新后的User对象传递给@Update方法,只会更新这些已更改的属性,而不会影响其他未修改的属性或主键id。
c. 无匹配记录则不执行任何操作:如果数据库中找不到与传入User对象id相匹配的记录,Room将不做任何更新操作。
所以,问题就出现在了我们调用updateUser方法时,传入的user对象本身,即
new User(name, age, sex);
可见,这里面是没有id的,那么没有id意味着什么,意味着id为null
对于Room而言,id为null时,它会怎么去理解我们传入的user对象呢——不做任何更新操作
所以,问题就找到了
也非常好解决,有两种解决方法,第一种就是给用户提供id的输入框(这种可以联想一下上面说过的游戏里面输入uid查找好友)
我们这里就简单一点吧
看一下我们的UserRepository中的具体调用
// 确认用户存在
if (userDao.findUserByName(user.getName()) == null) {
return new UserRepositoryResult(
UserRepositoryResult.Type.FAILURE,
RESULT_MESSAGE_USER_NOT_EXIST);
}
是的,你更新用户,首先得先确定用户存在对吧。
我是因为考虑一切从简,所以设计时是不允许相同name出现的,所以直接用name来查重了,当然也可以通过id来查找User
直接展示最终的处理办法
@Transaction
public UserRepositoryResult updateUser(User user) {
// 确认用户存在
User findUser = userDao.findUserByName(user.getName());
if (findUser == null) {
return new UserRepositoryResult(
UserRepositoryResult.Type.FAILURE,
RESULT_MESSAGE_USER_NOT_EXIST);
}
user.setId(findUser.getId());
userDao.updateUser(user);
return UserRepositoryResult.success();
}
通过name找到数据库中的旧的User对象,因为id是主键,所以id不会更改,直接把旧的id塞到我们的新的user对象中,然后再传给updateUser方法,即可以实现Update方法了