mybatis-mapper / mapper

MyBatis Mapper
https://mapper.mybatis.io
Apache License 2.0
325 stars 47 forks source link

求教一个问题,deleteByFieldList(T::getId, ids); NPE #50

Closed shaoguanlee closed 1 year ago

shaoguanlee commented 1 year ago

问题描述

我有一个基类,里面有一个deleteByIdList方法,希望提供通用的删除操作。


public class BaseId  {
    /**
     * 使用BigInt作主键,在插入时需要自己填充键值
     */
    @Entity.Column(id = true, remark = "主键,bigint类型", updatable = false, insertable = true)
    private Long id;
}

public abstract class AbstractIdService<T extends BaseId, M extends BaseMapper<T>>
        extends AbstractService<T, Long, M> {

    /**
     * 根据ID列表进行删除
     */
    public int deleteByIdList(List<Long> ids) {
        return deleteByFieldList(T::getId, ids);
    }
}

调用这个方法的时候发生了NPE如下 io.mybatis.provider.EntityFactory

public static EntityTable create(Class<?> entityClass) {
    //处理EntityTable
    EntityTableFactory.Chain entityTableFactoryChain = Instance.getEntityTableFactoryChain();
    //创建 EntityTable,不处理列(字段),此时返回的 EntityTable 已经经过了所有处理链的加工
    EntityTable entityTable = entityTableFactoryChain.createEntityTable(entityClass);
    if (entityTable == null) {
      throw new NullPointerException("Unable to get " + entityClass.getName() + " entity class information");  //这里抛出了异常
    }
    //省略后面的代码
  }

debug,发现entityClass为BaseId。

但是当这么使用时,就一切正常

public class DingtalkDeviceGatewayService
        extends AbstractIdService<DingtalkDeviceGateway, DingtalkDeviceGatewayMapper> {
}

gatewayService.deleteByFieldList(DingtalkDeviceGateway::getId, ids)

请教

我不确定这是bug还是我使用不当,所以想问一下 deleteByFieldList(T::getId, ids);这种使用方法是否正确。

abel533 commented 1 year ago

你用的不是最新版本吧?测试也没有问题。

注意看 Reflections,增加了下面的代码,会找到基类的子类:

 //主要是这里  serializedLambda.getInstantiatedMethodType()
      Matcher matcher = INSTANTIATED_CLASS_PATTERN.matcher(serializedLambda.getInstantiatedMethodType());
      String implClass;
      if (matcher.find()) {
        implClass = matcher.group("cls").replaceAll("/", "\\.");
      } else {
        implClass = serializedLambda.getImplClass().replaceAll("/", "\\.");
      }
abel533 commented 1 year ago

2021年11月3号发布的1.0.3版本就解决了。

shaoguanlee commented 1 year ago

非常感谢您的回复

我使用的版本是

        <!--通用Mapper-->
        <dependency>
            <groupId>io.mybatis</groupId>
            <artifactId>mybatis-service</artifactId>
            <version>1.2.2</version>
        </dependency>
        <!--通用Mapper的SpringBootStarter-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

我仔细跟踪了代码的执行过程,发现问题并不是在Reflections类上,Reflections确实能找到类

1659331453664

image

1659331219830

abel533 commented 1 year ago

能否提交个单测复现这个问题?

1962247851 commented 1 year ago

一样的问题,Fn<T, Object> fn 用范型T::getXxx()报错,而且没有直接传字段名的方法,从tk转过来很难受,打算转回去了

public class BaseService<D extends IBaseMapper<T>, T extends BaseDO> {

    @Autowired
    protected D dao;

    public List<T> findIds(List<String> idList) {
        if (CollUtil.isEmpty(idList)) {
            return Collections.emptyList();
        }

        Example<T> example = dao.wrapper()
                .in(t -> "uuid", idList)
                .example();

        return dao.selectByExample(example);
    }

}
1962247851 commented 1 year ago

可以试试这样获取范型

        ParameterizedType parameterizedType = (ParameterizedType) this.getClass().getGenericSuperclass();
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        return (Class<T>) actualTypeArguments[0]; // 下标是范型顺序
abel533 commented 1 year ago

这里是针对当前问题的单测: Assert.assertEquals(2, userService.deleteByIdList(ids));

可以看看和自己用法有什么区别,可以提交一个能复现问题的示例。

abel533 commented 1 year ago

可以加入QQ群讨论

shaoguanlee commented 1 year ago

我对于该问题提交了一个测试案例 https://github.com/shaoguanlee/mybatis-mapper-issues-50 可以复现这个问题 再次感谢

abel533 commented 1 year ago

这里确实存在问题,而且根据class记录的信息来说,T::getId 没有包含子类的信息,只能找到 BaseId,当前项目的测试中,之所以没有出错,是因为 BaseId 也有注解:

@Entity.Table
public class BaseId<T extends BaseId> {
  @Entity.Column(id = true, insertable = false)
  private Integer id;

如果去掉这里的 @Entity.Table 就能复现该问题。

上面的例子也可以作为一个解决办法,在你的 BaseId 添加注解也能解决问题。

目前没有办法从BaseId获取到子类。

abel533 commented 1 year ago

问题已解决,Fn增加下面方法,在调用过程中提前记录下实体类:


  /**
   * 当前字段所属的实体类,当实体存在继承关系时
   * 父类的方法引用无法获取字段所属的实体类,需要通过该方法指定
   *
   * @param entityClass 指定实体类
   * @return 带有指定实体类的 Fn
   */
  default Fn<T, R> in(Class<?> entityClass) {
    return new FnImpl<>(this, entityClass);
  }

对应的 FnImpl:

  /**
   * 带有指定类型的方法引用
   */
  class FnImpl<T, R> implements Fn<T, R> {

    final Fn<T, R> fn;
    final Class<?> entityClass;

    public FnImpl(Fn<T, R> fn, Class<?> entityClass) {
      this.fn = fn;
      this.entityClass = entityClass;
    }

    @Override
    public R apply(T t) {
      return fn.apply(t);
    }

  }

在 Reflections 中处理时会判断:

  public static ClassField fnToFieldName(Fn fn) {
    try {
      Class<?> clazz = null;
      if (fn instanceof Fn.FnImpl) {
        clazz = ((Fn.FnImpl<?, ?>) fn).entityClass;
        fn = ((Fn.FnImpl<?, ?>) fn).fn;
        //避免嵌套多次的情况
        while (fn instanceof Fn.FnImpl) {
          fn = ((Fn.FnImpl<?, ?>) fn).fn;
        }
      }
//其他...

目前内置的方法中,有两个地方已经加上,自己实现抽象类时不用在指定:

  /**
   * 根据指定字段集合查询:field in (fieldValueList)
   * <p>
   * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
   *
   * @param field          字段
   * @param fieldValueList 字段值集合
   * @param <F>            字段类型
   * @return 实体列表
   */
  default <F> List<T> selectByFieldList(Fn<T, F> field, Collection<F> fieldValueList) {
    Example<T> example = new Example<>();
    example.createCriteria().andIn((Fn<T, Object>) field.in(entityClass()), fieldValueList);
    return selectByExample(example);
  }

  /**
   * 根据指定字段集合删除:field in (fieldValueList)
   * <p>
   * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
   *
   * @param field          字段
   * @param fieldValueList 字段值集合
   * @param <F>            字段类型
   * @return 实体列表
   */
  default <F> int deleteByFieldList(Fn<T, F> field, Collection<F> fieldValueList) {
    Example<T> example = new Example<>();
    example.createCriteria().andIn((Fn<T, Object>) field.in(entityClass()), fieldValueList);
    return deleteByExample(example);
  }

BaseMapper 中已经加上,所以你的用法中不用特殊处理。

以后遇到类似问题时,比如基类不支持,你的方法可以修改:

    /**
     * 根据ID列表进行删除
     */
    public int deleteByIdList(List<Long> ids) {
        return deleteByFieldList(((Fn<T,Long>)T::getId).in(baseMapper.entityClass()), ids);
    }