Java新人易错:当修改遇到唯一(万字长文)

写在前面

现在是Java培训机构林立的时代,更有360行,行行转java的谣传。但是,我们很多人都一味地追求知识面的广度,孜孜不倦地学习各种新技术。

网上大量地博客都打着xxx管理系统的亮眼标题,哇,免费的可以白嫖,赶紧下载,编译,部署。搭出来一个博客页面,有类型,文章的增删改查,各种新技术,boot,cloud,vue,感觉自己碉堡了。

但是,我们往往却疏忽了业务逻辑的锻炼,那些项目再好,页面再华丽,也无非就是最最简单的增删改查,为一大群crud boy提供了肥沃的土壤。

于是,又有人说,我要跳出舒适圈,去卷算法。

算法对应高阶的程序来说是有意义的,但是我们更多时候,工作还是写业务代码为主。

不要求你的代码有多么牛逼,但是要求你的代码质量过硬,不要频繁出现各种BUG,引起生产事故。

试金石

不要觉得这是一件很简单的事情哦,往往一个很小的功能,就会埋藏了数不清的坑。

甚至你都会觉得愕然,我TM一个妥妥的crud小能手,怎么遇到这么简单的需求,一不小心就跳进了坑里。

这是一个思维的问题,业务思维无法用crud的数量来升级。

要我说,对一个初入java行业的小白来说,最好的试金石就是这个修改唯一数据的问题。

准备下环境

先把环境搞一搞,为了方便起见,我直接使用了之前写的 《清新版 Springboot日记本系统》的代码,添加好test依赖:


 org.springframework.boot
 spring-boot-starter-test

直接搞起。

先来一个Customer类:

@Data
@Builder
public class Customer {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private String idno;
    private String isDel;
}

对应表结构:

Java新人易错:当修改遇到唯一(万字长文)_第1张图片

测试添加数据

@Slf4j
@SpringBootTest(classes = {DiaryApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class MyTest {

    @Resource
    CustomerMapper customerMapper;

    @Test
    public void tt(){
        customerMapper.insert(Customer.builder().name("小火龙").idno("1").build());
        customerMapper.insert(Customer.builder().name("杰尼龟").idno("2").build());
        customerMapper.insert(Customer.builder().name("妙蛙种子").idno("3").build());
    }
}

得到

Java新人易错:当修改遇到唯一(万字长文)_第2张图片

至此,环境准备完毕。

身份证号唯一

需求很简单,要把身份证号设置唯一。

我们知道,id是自动递增的,这个没问题。身份证号唯一,无非就是新增和修改的时候判断一下呗。

新增判断唯一

这个简单,无非就是新增的时候判断一下idno有没有呗。 你很快就写出了代码,并且为自己封装了checkIdno方法而沾沾自喜:

/**
 * 测试新增
 */
@Test
public void insert(){
    //模拟前台传过来的Req
    Customer customerDto = Customer.builder().name("火球鼠").idno("1").build();
    //先检查身份证号是否存在于数据库中
    checkIdno(customerDto.getIdno());
    //正常入库
    customerMapper.insert(customerDto);
}

private void checkIdno(String idno) {
    LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
    Customer one = customerMapper.selectOne(lambdaQueryWrapper.eq(Customer::getIdno, idno));
    //不是空,说明有这个idno的数据
    if(one != null){
        throw new WrongArgumentException("身份证重复!");
    }
}

测试,报错了,业务正确。

Java新人易错:当修改遇到唯一(万字长文)_第3张图片

编辑判断唯一

新增已经开了一个好头,接下来是编辑,作为一个刚毕业后进入公司的萌新,此时正兴致勃勃地敲起了键盘。

嗯,修改的时候正好可以复用刚刚封装的checkIdno方法。

/**
 * 测试修改
 */
@Test
public void edit(){
    //模拟前台传过来的Req
    Customer customerReq = Customer.builder().id(1).idno("2").build();
    //先检查身份证号是否存在于数据库中
    checkIdno(customerReq.getIdno());
    //根据id查询数据库的数据
    Customer customerDto = getById(customerReq.getId());
    //更新idno
    customerDto.setIdno(customerReq.getIdno());
    //入库
    customerMapper.updateById(customerDto);
}

private Customer getById(Integer id) {
    return customerMapper.selectById(id);
}

封装性,封装性,这是培训班老师教的,能封装的就封装。 嗯,我又封装了一个getById方法,这是荣誉的勋章,也是我技术实力的证明!

上面代码测试修改小火龙,把身份证从1改成2,与杰尼龟冲突。

来吧,测试。

Java新人易错:当修改遇到唯一(万字长文)_第4张图片

( •̀ ω •́ )yeah!

打完收工,注释完美,代码完整,我还封装了呢!

接下来就是升职加薪,迎娶白富美,从此走上人生巅峰了吧。

假如没改idno呢?

人生总是起起落落,酸甜苦辣都有,不可能只有甜。

直到有一天,测试小姐姐找到你,为什么我修改个姓名,也报错了呢?

你直接懵逼了,马上打开postman自测,结果发现还真有问题。

@Test
public void edit(){
    //模拟前台传过来的Req
    Customer customerReq = Customer.builder().id(1).name("喷火龙").idno("1").build();
    //先检查身份证号是否存在于数据库中
    checkIdno(customerReq.getIdno());
    //根据id查询数据库的数据
    Customer customerDto = getById(customerReq.getId());
    //更新name
    customerDto.setName(customerReq.getName());
    //入库
    customerMapper.updateById(customerDto);
}

idno还是1,但是送过来了,小火龙本来的idno就是1,但是因为有了验重逻辑,导致报错了。

你顿时慌张起来,怎么就这么简单的一个小需求,还有这么多弯弯绕呢?

解决方案

一个最简单的思路就是,你编辑的时候,是不是肯定有个id传过来,毕竟我要根据这个id去更新嘛。

那么,我也可以通过这个id,去查询对应的idno,和这次送进来的idno是不是一样的。

如果跟原来的idno不一致,说明这次你改了idno,我就要进行校验,看你修改后的idno有没有和别的数据对应的idno重复。

方法就是拿着修改后的idno去查,查出来就说明有重复,否则就是唯一的,允许修改。

为了测试方便,给edit方法添加参数。

public void edit(Customer customerReq){

    //根据id查询数据库是否有这条数据
    Customer customer = customerMapper.selectById(customerReq.getId());
    //如果不存在或者跟原来的idno不一致,才进行校验
    Boolean isNeedCheck = true;
    if(customer != null && customer.getIdno().equals(customerReq.getIdno())){
        isNeedCheck = false;
    }

    //检查身份证号是否存在于数据库中
    if(isNeedCheck) {
        checkIdno(customerReq.getIdno());
    }
    //根据id查询数据库的数据
    Customer customerDto = getById(customerReq.getId());
    //更新name
    customerDto.setName(customerReq.getName());
    //更新idno
    customerDto.setIdno(customerReq.getIdno());
    //入库
    customerMapper.updateById(customerDto);
}

然后是测试用例:

//测试编辑功能,修改其他字段但idno不变
@Test
public void testEdit01(){
    //模拟前台传过来的Req => 只改姓名不改身份证
    Customer customerReq = Customer.builder().id(1).name("喷火龙").idno("1").build();
    edit(customerReq);
}

//测试编辑功能,修改idno但是和其他数据的idno一样
@Test
public void testEdit02(){
    //模拟前台传过来的Req => 只改姓名不改身份证
    Customer customerReq = Customer.builder().id(1).name("喷火龙").idno("2").build();
    edit(customerReq);
}

//测试编辑功能,修改idno但是和其他数据的idno都不一样
@Test
public void testEdit03(){
    Customer customerReq = Customer.builder().id(1).name("喷火龙").idno("8").build();
    edit(customerReq);
}

第一个测试是正常的,因为没有改idno。第二个测试报错了,也符合预期。第三个是正例,结果依然没问题。

方案2

上面的逻辑是没问题的,代码可读性也很强,但是不够简洁。在实际开发中,我也看到有人是这么处理的。

直接上代码,注释很详细了。

public void edit2(Customer customerReq){

    /**
     * 直接根据idno查询,无非有两种情况
     *  1.查出来为null,说明没有这个idno,安全
     *  2.查出来有一条数据,又分两种情况
     *      2.1 这条数据就是要编辑的原数据:说明我们修改这条记录并且没改idno,安全
     *      2.2 这条数据不是要编辑的原数据:说明我们修改这条记录并且改了idno,还和别的记录的idno重复了,不安全
     *  总结:如果查出来有数据并且id和要编辑数据的id不同,才不安全
     */
    LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
    Customer one = customerMapper.selectOne(lambdaQueryWrapper.eq(Customer::getIdno, customerReq.getIdno()));
    if(Objects.nonNull(one) && !Objects.equals(one.getId(),customerReq.getId())){
        throw new WrongArgumentException("身份证重复!");
    }

    //根据id查询数据库的数据
    Customer customerDto = getById(customerReq.getId());
    //更新name
    customerDto.setName(customerReq.getName());
    //更新idno
    customerDto.setIdno(customerReq.getIdno());
    //入库
    customerMapper.updateById(customerDto);
}

思路就是直接通过idno去查,无非这四种情况。

直接根据idno查询,无非有两种情况:

1.查出来为null,说明没有这个idno,安全。

2.查出来有一条数据,又分两种情况。

2.1 这条数据就是要编辑的原数据:说明我们修改这条记录并且没改idno,安全。

2.2 这条数据不是要编辑的原数据:说明我们修改这条记录并且改了idno,还和别的记录的idno重复了,不安全。

总结:如果查出来有数据并且id和要编辑数据的id不同,才不安全。

乍一看似乎不好理解,但是只要我们把所有的情况都理清楚了,还是很清晰的。

这个方案确实很简洁,巧妙就巧妙在,不知不觉间它把新增的情况也包含了。

如果是新增,那么customerReq是没有id的,那么getId就是null,必然与one的getId不符合。也能走进这个逻辑:

LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
Customer one = customerMapper.selectOne(lambdaQueryWrapper.eq(Customer::getIdno, customerReq.getIdno()));
if(Objects.nonNull(one) && !Objects.equals(one.getId(),customerReq.getId())){
    throw new WrongArgumentException("身份证重复!");
}

我们不妨把这个逻辑抽取出来封装成方法。

private void checkIdnoNew(Customer customerReq) {
    /**
     * 直接根据idno查询,无非有两种情况
     *  1.查出来为null,说明没有这个idno,安全
     *  2.查出来有一条数据,又分两种情况
     *      2.1 这条数据就是要编辑的原数据:说明我们修改这条记录并且没改idno,安全
     *      2.2 这条数据不是要编辑的元数据:说明我们修改这条记录并且改了idno,还和别的记录的idno重复了,不安全
     *  总结:如果查出来有数据并且id和要编辑数据的id不同,才不安全
     */
    LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
    Customer one = customerMapper.selectOne(lambdaQueryWrapper.eq(Customer::getIdno, customerReq.getIdno()));
    if(Objects.nonNull(one) && !Objects.equals(one.getId(),customerReq.getId())){
        throw new WrongArgumentException("身份证重复!");
    }
}

编辑方法就变成了这样:

public void edit2(Customer customerReq){

    checkIdnoNew(customerReq);

    //根据id查询数据库的数据
    Customer customerDto = getById(customerReq.getId());
    //更新name
    customerDto.setName(customerReq.getName());
    //更新idno
    customerDto.setIdno(customerReq.getIdno());
    //入库
    customerMapper.updateById(customerDto);
}

新增方法也修改下:

@Test
public void insert(){
    //模拟前台传过来的Req
    Customer customerReq = Customer.builder().name("火球鼠").idno("6").build();
    //先检查身份证号是否存在于数据库中
    checkIdnoNew(customerReq);
    //正常入库
    customerMapper.insert(customerReq);
}

对比之前的方法:

//根据id查询数据库是否有这条数据
Customer customer = customerMapper.selectById(customerReq.getId());
//如果不存在或者跟原来的idno不一致,才进行校验
Boolean isNeedCheck = true;
if(customer != null && customer.getIdno().equals(customerReq.getIdno())){
    isNeedCheck = false;
}

//检查身份证号是否存在于数据库中
if(isNeedCheck) {
    checkIdno(customerReq.getIdno());
}

是不是要简洁好多呢,这就是业务思维的魅力。

本文写成真的挺不容易,应该是现在网上少有的手把手教人理业务逻辑的博客了。看似一个小功能,却有这么多弯弯绕在里面,哈哈,没想到吧。

现在很多教程,包括很多视频课,都是一大堆技术给你安排上,动不动就商城啊,什么redis啊,ES啊,都给你整上。学了半天,看似学了很多,可往往都不精,成就了全栈HelloWolrd小能手。

很多人都跟我抱怨,为什么我学了那么多,乱七八糟的课程报了一大堆,商城系统也做了一堆,怎么去面试还是要被面试官diao呢,进了公司熬不过试用期就被开了?

那是因为,你还是没有养成真正的业务思维,思维还停留在学生时代,要别人把嚼碎了喂给你,缺少主动去钻研的精神。说白了,项目经验太少,不知道厂里到底要我干些什么?不要觉得会用MybatisPlus生成个Crud代码,会用redis简单的get/set,就能说精通了,现在行业内卷的时代,哪有这么简单。

可是你也许会说,不让我入职,我怎么积累项目经验?

看吧,又是死锁问题。

这些问题暴露出来,可能有网络上众多贩卖焦虑的因素在作怪,也有大环境的原因。

可是不论怎么样,我们都应该保持一颗平常心,多看,多练,不要一味地贪多,没必要达成各种helloworld成就。

看看现在视频网站上面各种脚手架的漂亮博客系统,收藏量和点赞量都高的吓人。

可是我个人觉得,这些对于真正提升自己的代码能力,帮助并不大。

一句话,真的,别被带歪了。

以上都属于我个人吐槽,仅代表我本人观点,欢迎一起交流讨论。本文也主要针对新入行的萌新小白,大佬轻喷。

你可能感兴趣的:(java教程,java,开发语言)