← 返回文章列表

backend项目架构设计 - 通用的按关键词搜索的Mapper

分类:计算机/架构/backend项目架构设计

搜索是最常用的功能之一,具体到搜索某一种数据,通常有两种形式:

  1. 单个搜索框:用户输入关键词(keyword)文本,在数据的多个属性中用关键词筛选。输入简单,但结果可能不够精确。
  2. 多个筛选条件:为数据的多个属性分别设置筛选条件,在数据中分别用对应的筛选条件筛选。输入更复杂,但结果更精确。

我们在这里只讨论第一种搜索形式,并将其实现为通用的底层功能,以便在需要时对任意数据快速实现搜索。

前端页面:页面上设置一个单行文本框,用户可输入关键词列表,多个关键词用空格隔开。前端保证关键词列表的每个元素为非空字符串。

后端接口:接收3个参数:

后端在Mapper层实现通用的按关键词搜索(SearchByKeywordsMapper接口),并留出扩展性。实体Mapper接口调用SearchByKeywordsMapper接口的方法,并传入以下信息:

为了简化实现,我们使用PageHelper分页插件实现自动分页,并支持使用Map+简短标记(如=like)来简化属性条件的表达。

参考代码:

/**
 * 通用的按关键词搜索的Mapper(面向MySQL数据库)
 * @param <T> 实体类
 */
public interface SearchByKeywordsMapper<T> {
    /**
     * 搜索并分页
     * @param keywords 搜索关键词列表
     * @param pageNum 当前页码,从1开始。
     * @param pageSize 每页条数
     * @return 分页对象
     * @implNote 请在实体Mapper中使用{@code default}方法实现此方法,并在{@code default}方法中调用
     *     {@link #_search(String, List, Map, String, int, int)}方法实现搜索和分页功能。
     */
    Page<T> search(List<String> keywords, int pageNum, int pageSize);

    /**
     * 搜索并分页
     * @param select {@code select}SQL语句,可以包含连表,但不能包含{@code where}子句和{@code order by}子句。
     * @param keywords 搜索关键词列表
     * @param keywordConditions 单个关键词的搜索条件映射表。
     *     <p>其中,键为列名,例如{@code "e.name"}。</p>
     *     <p>值为条件,支持以下4种形式:<p>
     *     <ol>
     *         <li>{@code "="}:表示等于当前关键词,即<code>"column = #{keyword}"</code>。</li>
     *         <li>{@code "like"}:表示字符串包含当前关键词,即<code>"column like concat('%', #{keyword}, '%')"</code>。</li>
     *         <li>{@code "find_in_set"}:表示使用{@code find_in_set()}函数判断包含,即<code>"find_in_set(#{keyword}, column)"</code>。</li>
     *         <li>自定义条件:不含列名,关键词的占位符为<code>#{keyword}</code>,即<code>"column 自定义条件"</code>。</li>
     *     </ol>
     * @param orderBy {@code order by}子句,不含{@code order by}关键字。{@code null}或{@code ""}表示不排序。
     * @param pageNum 当前页码,从1开始。
     * @param pageSize 每页条数
     * @return 分页对象
     */
    default Page<T> _search(
        String select,
        List<String> keywords,
        Map<String, String> keywordConditions,
        @Nullable String orderBy,
        int pageNum,
        int pageSize
    ) {
        var keywordConditionsString = keywordConditions.entrySet().stream()
            .map(entry -> {
                var column = entry.getKey();
                var condition = entry.getValue();
                if (condition.equals("=")) {
                    return column + " = #{keyword}";
                } else if (condition.equalsIgnoreCase("like")) {
                    return column + " like concat('%', #{keyword}, '%')";
                } else if (condition.equalsIgnoreCase("find_in_set")) {
                    return "find_in_set(#{keyword}, " + column + ")";
                } else {
                    return column + " " + condition;
                }
            })
            .collect(Collectors.joining(" or "));
        return _search(select, keywords, keywordConditionsString, orderBy, pageNum, pageSize);
    }

    /**
     * 使用PageHelper分页插件执行搜索并分页
     * @param select {@code select}SQL语句,可以包含连表,但不能包含{@code where}子句和{@code order by}子句。
     * @param keywords 搜索关键词列表
     * @param keywordConditions 单个关键词的搜索条件,其中关键词的占位符为<code>#{keyword}</code>。
     * @param orderBy {@code order by}子句,不含{@code order by}关键字。{@code null}或{@code ""}表示不排序。
     * @param pageNum 当前页码,从1开始。
     * @param pageSize 每页条数
     * @return 分页对象
     */
    @Select("""
        <script>
            <![CDATA[ ${select} ]]>
            <where>
                <foreach item="keyword" collection="keywords" separator="and">
                    ( <![CDATA[ ${keywordConditions} ]]> )
                </foreach>
            </where>
            <if test="orderBy != null and orderBy != ''">
                order by <![CDATA[ ${orderBy} ]]>
            </if>
        </script>
    """)
    Page<T> _search(
        String select,
        List<String> keywords,
        String keywordConditions,
        @Nullable String orderBy,
        int pageNum,
        int pageSize
    );
}
// 实体Mapper
public interface EntityMapper extends SearchByKeywordsMapper<Entity> {
    /**
     * 搜索实体
     * @param keywords 搜索关键词列表
     * @param pageNum 当前页码
     * @param pageSize 每页条数
     * @return 分页对象
     */
    default Page<Entity> search(List<String> keywords, int pageNum, int pageSize) {
        return _search(
            "select ...",
            keywords,
            Map.of(
                "column1", "=",
                "column2", "like"
            ),
            "column1, column2 desc",
            pageNum,
            pageSize
        );
    }
}