如果我使用 myBatis 从几乎相同的表中提取时如何删除大量重复代码

How to remove a lot of duplicate code when extracting from almost identical tables if i use myBatis

我使用 myBatis 来处理 myMysql。我有几个相同的 tables(演员、制作人、作曲家等),它们只包含两个字段 - id 和 name。

我必须编写很多几乎相同的代码才能使用它。例如映射器

<?xml version = "1.0" encoding = "UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="ru.common.mapper.NamedEntityMapper">

    <resultMap id="actor" type="Actor">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>
    <select id="getActorByName" parameterType="String" resultMap="actor">
        SELECT * FROM actors WHERE name = #{name}
    </select>
    <select id="getActorById" parameterType="String" resultMap="actor">
        SELECT * FROM actors WHERE id = #{id}
    </select>
    <insert id="saveActor" useGeneratedKeys="true" parameterType="Actor" keyProperty="id">
        INSERT INTO actors (name) VALUES (#{name})
    </insert>

    <resultMap id="writer" type="Writer">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>
    <select id="getWriterByName" parameterType="String" resultMap="writer">
        SELECT * FROM writers WHERE name = #{name}
    </select>
    <select id="getWriterById" parameterType="String" resultMap="writer">
        SELECT * FROM writers WHERE id = #{id}
    </select>
    <insert id="saveWriter" useGeneratedKeys="true" parameterType="Writer" keyProperty="id">
        INSERT INTO writers (name) VALUES (#{name})
    </insert>

</mapper>

正如在映射器中所见,非常相似的方法和查询仅在 table 的名称和被 return 编辑的类型上有所不同。现实中这样的方法还有很多,看起来很糟糕。

而且是一个接口

public interface NamedEntityMapper {

    Actor getActorById(long id);
    Actor getActorByName(String name);
    void saveActor(Actor actor);

    Writer getWriterById(long id);
    Writer getWriterByName(String name);
    void saveWriter(Writer writer);
}

我试过这样做,我为每个相似的模型做了一个通用的接口。 (基础模型)

public interface BaseModel {
    int getId();
    void setId(int id);
    String getName();
    void setName(String name);
}

并在像 Actor 这样使用的所有模型中实现了这个接口...

但这导致失败,因为不清楚如何向映射器解释创建所需的实例class。如何传输需要在 xml 映射器中创建的类型?

类似的东西

public interface NamedEntityMapper<T extends BaseModel> {

    T getEntityById(long id, String tableName, Class clazz);

}

和xml映射器

<mapper namespace="ru.common.mapper.NamedEntityMapper">

    <resultMap id="entity" type="${clazz}">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>
    <select id="getEntityById" parameterType="String" resultMap="entity">
        SELECT * FROM ${tableName} WHERE id = #{id}
    </select>    
</mapper>

但是我无法将 return 类型作为参数传递给映射器。这可以做到吗?在我的案例中,这将允许删除大量重复代码。

如果您使用的所有模型 classes(Actor、Writer 等)都具有相同的属性(id、name),那么您创建通用模型的想法是正确的它。但不是接口,而是为它创建一个通用的 class 。据我了解,resultMap 的 typeresultType 只能是原始类型或可以映射到的对象,所以接口赢了工作。然后你使用常见的 class (例如 BaseModel)作为你的 resultMap 标签的类型和所有指向它的 id 的 resultMap 属性.

<resultMap id="base" type="BaseModel">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
</resultMap>
<select id="getActorByName" parameterType="String" resultMap="base">
  SELECT * FROM actors WHERE name = #{name}
</select>

如果您的模型 classes 完全没有区别,那么我建议您只使用 BaseModel 并删除其他模型。否则,您可以让它们扩展 BaseModel 并在获取映射器的返回对象时,将其转换为子类型。

注意: 为避免出现 ClassCastException,您必须添加一个 if 语句来检查返回的 instanceof BaseModel 对象在向下转型之前。有关详细信息,请参阅此 link:explicit casting from super class to subclass

BaseModel baseModel = namedEntityMapperImpl.getActorById(id);
if (baseModel instanceof Actor) {
    Actor actor = (Actor)baseModel;
}

Mybatis(从3.3.5版本开始)没有优雅的方法来解决这个问题。

您可以使用下述技术消除一些重复,但是

  1. 不是全部
  2. 代码复杂化的代价。

CrudMapper

您可以尝试(某种程度上)通过定义通用映射器来消除映射器接口中的重复:

interface CrudMapper<T> {
   T getById(int id);
   T getByName(String name);
   void create(T);
   void update(T);
}

然后使用它为实体定义单独的映射器:

interface AuthorMapper extends CrudMapper<Author> {}

interface WriterMapper extends CrudMapper<Writer> {}

具有xml

的鉴别器

您也可以尝试使用discriminator重用结果图:

<resultMap id="workerResult" type="Worker">
  <id property="id" column="id" />
  <result property="name" column="name"/>
  <discriminator javaType="string" column="worker_type">
    <case value="author" resultType="Author"/>
    <case value="writer" resultType="Writer"/>
  </discriminator>
</resultMap>

但它需要使查询复杂化,即向每个 select 查询添加新列 worker_type

<select id="getByName" parameterType="String" resultMap="workerResult">
    SELECT 'actor' as worker_type, id, name FROM actors WHERE name = #{name}
</select>

不幸的是,无法避免在 xml 映射器中创建单独的方法。您唯一可以做的就是使用速度宏在一个地方进行查询(即在速度宏中)。在这种情况下,方法可能如下所示:

<select id="getByName" parameterType="String" resultMap="workerResult">
    #select_by_name('actor')
</select>

宏为:

#macro(select_by_name $worker_table)
   SELECT '${worker_table}' as worker_type, id, name FROM ${worker_table}s WHERE name = @name

Java API

中的鉴别器

Java API 在这方面可能更好,但并非没有缺点。

public interface HierarchyMapper<T> {
    @SelectProvider(method = "buildGetByName", type = HierarchySqlBuilder.class)
    @Results(id = "workerResult", value = {
              @Result(property = "id", column = "id", id = true),
              @Result(property = "name", column = "name")
            })
    @TypeDiscriminator(cases = {
            @Case(type = Actor.class, value = "actor"),
            @Case(type = Writer.class, value = "writer")},
            column = "worker_type")
    T getByName(@Param("name") String name, @Param("table") String table);
}

@Mapper
public interface ActorMapper extends HierarchyMapper<Actor> {
}

public class HierarchySqlBuilder {
  public static String buildGetByName(
          @Param("name") String name, @Param("table") String table) {
        return String.format(
                "SELECT '%s' as worker_type, id, name from %s where name = #{name}", table, table);
      }
}

不幸的是,我不知道如何避免将 table 传递给映射器。这里的问题是,在这种情况下,我们需要构建动态查询,实体类型(或 table)是参数。调度应该发生在某个地方。一种方法是在映射器之上有一个存储库层,它会像这样进行调度:

class WorkerRepository {
    @Autowired ActorMapper actorMapper;
    @Autowired WriterMapper writerMapper;

    public Actor getActorByName(String name) {
        return actorMapper.getByNae(name, 'actor');
    }

    public Writer getWriterByName(String name) {
        return writerMapper.getByNae(name, 'writer');
    }
}

您可能需要重新考虑这个问题。鉴于所有 类 都具有相同的字段,您可以将所有数据存储在一个 table 中,并在 table 中有一个像 worker_type 这样的鉴别器列来了解 table 的实际类型=77=]。在这种情况下,你完全避免了这个问题,因为你有一个 table,但仍然可以在 Java 中得到不同的 类(可能有共同的 parent)。

spring-data-mybatis

您可以尝试的一件事是 spring-data-mybatis。它允许注释实体:

@Entity
class Actor extends LongId {
    private String name;

    // getters and setters
}

@Entity
class Writer extends LongId {
    private String name;

    // getters and setters
}

然后定义存储库 类,这基本上是 spring 数据存储库:

public interface AuthorRepository extends CrudRepository<Author, Long> {
  List<Author> findByName(String name);    
}

public interface WriterRepository extends CrudRepository<Writer, Long> {
  List<Writer> findByName(String name);    
}

在这种情况下,您根本不需要手动创建映射器,而是在以前使用 mybatis 映射器的客户端中使用 CrudRepositoryCrudRepository 给出了基本的 crud 和额外的基于方法签名的自动生成的方法。有关详细信息,请参阅 spring-data documentation