如果我使用 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 的 type 或 resultType 只能是原始类型或可以映射到的对象,所以接口赢了工作。然后你使用常见的 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版本开始)没有优雅的方法来解决这个问题。
您可以使用下述技术消除一些重复,但是
- 不是全部
- 代码复杂化的代价。
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 映射器的客户端中使用 CrudRepository
。
CrudRepository
给出了基本的 crud 和额外的基于方法签名的自动生成的方法。有关详细信息,请参阅 spring-data documentation。
我使用 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 的 type 或 resultType 只能是原始类型或可以映射到的对象,所以接口赢了工作。然后你使用常见的 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版本开始)没有优雅的方法来解决这个问题。
您可以使用下述技术消除一些重复,但是
- 不是全部
- 代码复杂化的代价。
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 映射器的客户端中使用 CrudRepository
。
CrudRepository
给出了基本的 crud 和额外的基于方法签名的自动生成的方法。有关详细信息,请参阅 spring-data documentation。