Как удалить много дублирующегося кода при извлечении из почти идентичных таблиц, если я использую myBatis - PullRequest
0 голосов
/ 21 октября 2018

Я использую myBatis для работы с myMysql.У меня есть несколько идентичных таблиц (актеры, продюсеры, композиторы и т. Д.), Которые содержат только два поля - id и name.

Мне нужно написать много практически идентичного кода для работы с этим.Например, mapper

<?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>

Как видно из картографа, очень похожи методы и запросы, которые отличаются только именем таблицы и возвращаемым типом.На самом деле таких методов больше, и это выглядит ужасно.

И это интерфейс

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);
}

Я пытался сделать это так, я сделал общий интерфейс для каждой похожей модели,(BaseModel)

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

И реализовал этот интерфейс во всех моделях, таких как Actor ...

Но это привело к провалу, потому что неясно, как объяснить мапперу созданиеэкземпляр желаемого класса.Как передать тип, который нужно создать в xml mapper?

Что-то в этом роде

public interface NamedEntityMapper<T extends BaseModel> {

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

}

и xml mapper

<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>

Но я не смог пройтитип возвращаемого значения в качестве параметра для сопоставителя.Это можно сделать?Это позволит удалить много дублирующегося кода в моем случае.

Ответы [ 2 ]

0 голосов
/ 22 октября 2018

Mybatis (с версии 3.3.5) не имеет элегантного способа решить эту проблему.

Вы можете устранить некоторые дубликаты, используя описанную ниже технику, но

  1. не все
  2. на стоимость усложнения кода.

CrudMapper

Вы можете попытаться (несколько) избавиться от дублирования в интерфейсе mapper, определив универсальный mapper следующим образом:

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

Вы также можете попробовать использовать дискриминатор дляповторно использовать карту результатов:

<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>

Но это требует усложнения запроса, а именно добавления нового столбца worker_type в каждый запрос выбора:

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

К сожалению, нет способа избежатьсоздание отдельных методов в xml mapper.Единственное, что вы можете сделать, это использовать макросы скорости, чтобы запрос находился в одном месте (а именно в макрокоманде скорости).В этом случае метод может выглядеть следующим образом:

<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 в картограф.Проблема здесь в том, что в этом случае нам нужно построить динамический запрос, а тип сущности (или таблица) является параметром.Где-то должна быть отправка.Один из способов - создать слой хранилища над мапперами, который будет выполнять такую ​​диспетчеризацию следующим образом:

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');
    }
}

Возможно, вы захотите пересмотреть проблему.Учитывая, что все классы имеют одинаковые поля, вы можете хранить все данные в одной таблице и иметь столбец дискриминатора, такой как worker_type в этой таблице, чтобы знать фактический тип объекта.В этом случае вы полностью избежите этой проблемы, поскольку у вас есть одна таблица, но вы все равно можете получить разные классы в Java (возможно, с общим родителем).

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);    
}

В этом случае вы вообще не создаете картографы и используетеCrudRepository в клиентах, которые раньше использовали mybatis mappers.CrudRepository дает базовые и дополнительные автоматически генерируемые методы на основе сигнатуры метода.Для получения более подробной информации см. Spring-data документация .

0 голосов
/ 21 октября 2018

Если все используемые вами классы моделей (Actor, Writer и т. Д.) Имеют одинаковые атрибуты (id, name), то у вас есть правильная идея при создании общей модели для него.Но вместо интерфейса создайте для него общий класс.Из того, что я понимаю, тип resultMap или resultType может иметь только примитивный тип или объект, на который он может быть отображен, поэтому интерфейс не будет работать.Затем вы используете общий класс (например, BaseModel) в качестве типа для вашего тега resultMap и всех атрибутов 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>

Если естьнет никакой разницы в ваших модельных классах, тогда я предлагаю вам использовать только BaseModel и удалить другие.В противном случае вы можете сделать так, чтобы они расширяли BaseModel , а при получении возвращенного объекта преобразователя приводили его к подтипу.

Примечание: Чтобы избежать получения ClassCastException , необходимо добавить оператор if, чтобы проверить instanceof возвращенного объекта BaseModel перед его понижением.Подробности смотрите по этой ссылке: явное приведение из суперкласса к подклассу

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