5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaの関数型インターフェース活用術 ─ リファクタリングで重複コードをなくす

Last updated at Posted at 2025-09-22

はじめに

本記事では型パラメーターやJava8から導入された関数型インターフェースを利用して、同じような処理を共通化する方法について紹介します。

自分は過去にJavaGold SE8の資格勉強1をしていました。
その際、関数型インターフェースが出題範囲に含まれていたため、勉強したことはあるのですが、

  • 資格勉強においては暗記で乗り切っている
  • 自分が経験した現場のコードの多くはJava8以前で実装されているものが多い

ということもあり、StreamAPIやラムダ式を実務では使っているものの、関数型インターフェースそのものを意識して使ったことがありませんでした。

ただ、ここ最近は関数型インターフェースに触れる機会も増えてきたので、実際の修正例を交えて、関数型インターフェースを利用した実装について紹介します。

関数型インターフェースとは

関数型インターフェースとは、抽象メソッドを1つだけ持つインターフェースのことを指します。
Java8で導入されたラムダ式やメソッド参照と組み合わせて利用されることが多いです。

これだけだとイメージが湧きづらいと思うので、詳細については分かりやすそうな記事を紹介します。

関数型インターフェースにはいくつか種類がありますが、今回は関数型インターフェースの中でも、 Function<T, R> を利用したいと思います。

サンプルコード

詳細な説明は上記の記事に譲りますが、簡単なサンプルコードを以下に示します。

FunctionSample.java
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class FunctionSample {
    public static void main(String[] args) {
        // StringをIntegerに変換するFunction
        Function<String, Integer> stringToInteger = s -> Integer.parseInt(s);

        // applyメソッドの例
        Integer result = stringToInteger.apply("123");
        System.out.println("変換結果: " + result); // 変換結果: 123
    }
}

サンプルコードの説明

Function型の定義

Function<String, Integer> stringToInteger = s -> Integer.parseInt(s);
  • 型宣言 Function<String, Integer>:文字列(String)を受け取って整数(Integer)を返すFunction型を定義
  • ラムダ式 s -> Integer.parseInt(s): 文字列を整数に変換する処理を定義

applyメソッドの使用

Integer result = stringToInteger.apply("123");
  • Functionインターフェースのapplyメソッドを使って実際に変換処理を実行
  • 文字列"123"を整数123に変換

Function型を利用することで、処理を変数として扱うことができ、柔軟に処理を切り替えたり、共通化したりすることができます。
代入された変数は、Function型のapplyメソッドを通じて実行されます。

今回紹介するコード

今回取り上げるソースコードは過去に投稿した記事で利用しているものです。

GitHubリポジトリはtakashi-ebina/exportTableDefinitionです。

修正前のソースコード

AbstractTableDefinitionRepository.java
public abstract class AbstractTableDefinitionRepository implements TableDefinitionRepository {

    private final String baseSqlPath;

    /**
     * コンストラクタ
     */
    protected AbstractTableDefinitionRepository(DatabaseType databaseType) {
        this.baseSqlPath = "com.export_table_definition.domain.repository." + databaseType.getName() + ".TableDefinitionRepository.";
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public BaseInfoEntity selectBaseInfo() {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final BaseInfoDto dto = session.selectOne(baseSqlPath + "selectBaseInfo");
            return makeBaseInfoEntity(dto);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<TableEntity> selectTableInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<TableDto> dtoList = session.selectList(baseSqlPath + "selectTableInfo", param);
            return makeTableEntityList(dtoList);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<ColumnEntity> selectColumnInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<ColumnDto> dtoList = session.selectList(baseSqlPath + "selectColumnInfo", param);
            return makeColumnEntityList(dtoList);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<IndexEntity> selectIndexInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<IndexDto> dtoList = session.selectList(baseSqlPath + "selectIndexInfo", param);
            return makeIndexEntityList(dtoList);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<ConstraintEntity> selectConstraintInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<ConstraintDto> dtoList = session.selectList(baseSqlPath +"selectConstraintInfo", param);
            return makeConstraintEntityList(dtoList);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<ForeignkeyEntity> selectForeignkeyInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<ForeignkeyDto> dtoList = session.selectList(baseSqlPath + "selectForeignkeyInfo", param);
            return makeForeignkeyEntityList(dtoList);
        }
    }

    private BaseInfoEntity makeBaseInfoEntity(BaseInfoDto dto) {
        return new BaseInfoEntity(dto.getDbName(), dto.getBaseInfo());
    }
    private List<TableEntity> makeTableEntityList(List<TableDto> dtoList) {
        return dtoList.stream()
                .map(dto -> new TableEntity(dto.getSchemaName(), dto.getLogicalTableName(),
                        dto.getPhysicalTableName(), dto.getTableType(), dto.getTableInfoList(),
                        dto.getTableInfo(), dto.getDefinition())).collect(Collectors.toList());
    }
    private List<ColumnEntity> makeColumnEntityList(List<ColumnDto> dtoList) {
        return dtoList.stream()
                .map(dto -> new ColumnEntity(dto.getSchemaName(), dto.getTableName(),
                        dto.getColumnInfo())).collect(Collectors.toList());
    }
    private List<IndexEntity> makeIndexEntityList(List<IndexDto> dtoList) {
        return dtoList.stream()
                .map(dto -> new IndexEntity(dto.getSchemaName(), dto.getTableName(),
                        dto.getIndexInfo())).collect(Collectors.toList());
    }
    private List<ConstraintEntity> makeConstraintEntityList(List<ConstraintDto> dtoList) {
        return dtoList.stream()
                .map(dto -> new ConstraintEntity(dto.getSchemaName(), dto.getTableName(),
                        dto.getConstraintInfo())).collect(Collectors.toList());
    }
    private List<ForeignkeyEntity> makeForeignkeyEntityList(List<ForeignkeyDto> dtoList) {
        return dtoList.stream()
                .map(dto -> new ForeignkeyEntity(dto.getSchemaName(), dto.getTableName(),
                        dto.getForeignkeyInfo())).collect(Collectors.toList());
    }
}

ソースコードの概要

このソースコードは、Mybatisを利用してデータベースからテーブル定義情報を取得するリポジトリクラスの一部です。
SQLが複雑のため、XMLベースのMapperファイルを利用してSQLを管理しています。

SQLの実行結果をDTOで受け取り、Entityに変換して返却しています。
主に2パターンのメソッドに分類されます。

  • select${Entity}Infoメソッド
    • SQLを実行しているメソッド
  • make${Entity}Listメソッド
    • DTOからEntityへの変換処理
SQLが気になる方はこちら
tableDefinitionMapper.xml
<?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="com.export_table_definition.domain.repository.postgresql.TableDefinitionRepository">

<select id="selectBaseInfo" resultType="BaseInfoDto">
SELECT current_database() AS "dbName", concat('|', 'PostgreSQL', '|', current_database(), '|', to_char(current_timestamp, 'yyyy/MM/dd'), '|') AS "baseInfo" fetch first 1 rows only;
</select>

<select id="selectTableInfo" resultType="TableDto">
WITH all_tables AS (
    SELECT 
        coalesce(t1.schemaname, '') AS schema_name,
        'table' AS table_type,
        coalesce(regexp_replace(pg_catalog.obj_description(concat(t1.schemaname, '.', t1.tablename)::regclass),'\r|\n|\r\n', '', 'g'), '') AS logical_table_name,
        coalesce(t1.tablename, '') AS physical_table_name,
        '' AS remarks,
        concat('[■](./', current_database(), '/', t1.schemaname, '/table/', t1.tablename, '.md)') AS link,
        '' AS definition
    FROM pg_catalog.pg_tables t1
    WHERE t1.schemaname NOT IN ('pg_catalog', 'information_schema')
    <if test="schemaList != null and schemaList.size() > 0">
        AND t1.schemaname IN
        <foreach item="schema" index="index" collection="schemaList"
            open="(" separator="," close=")">
            #{schema}
        </foreach>
    </if>
    <if test="tableList != null and tableList.size() > 0">
        AND t1.tablename IN
        <foreach item="table" index="index" collection="tableList"
            open="(" separator="," close=")">
            #{table}
        </foreach>
    </if>
    
    UNION ALL

    SELECT 
        coalesce(t2.schemaname, '') AS schema_name,
        'view' AS table_type,
        coalesce(regexp_replace(obj_description(concat(t2.schemaname, '.', t2.viewname)::regclass),'\r|\n|\r\n', '', 'g'), '') AS logical_table_name,
        coalesce(t2.viewname, '') AS physical_table_name,
        ' ' AS remarks,
        concat('[■](./', current_database(), '/', t2.schemaname, '/view/', t2.viewname, '.md)') AS link,
        definition AS definition
    FROM pg_catalog.pg_views t2
    WHERE t2.schemaname NOT IN ('pg_catalog', 'information_schema')
    <if test="schemaList != null and schemaList.size() > 0">
        AND t2.schemaname IN
        <foreach item="schema" index="index" collection="schemaList"
            open="(" separator="," close=")">
            #{schema}
        </foreach>
    </if>
    <if test="tableList != null and tableList.size() > 0">
        AND t2.tablename IN
        <foreach item="table" index="index" collection="tableList"
            open="(" separator="," close=")">
            #{table}
        </foreach>
    </if>

    UNION ALL

    SELECT 
        coalesce(t3.schemaname, '') AS schema_name,
        'materialized_view' AS table_type,
        coalesce(regexp_replace(obj_description(concat(t3.schemaname, '.', t3.matviewname)::regclass),'\r|\n|\r\n', '', 'g'), '') AS logical_table_name,
        coalesce(t3.matviewname, '') AS physical_table_name,
        ' ' AS remarks,
        concat('[■](./', current_database(), '/', t3.schemaname, '/materialized_view/', t3.matviewname, '.md)') AS link,
        definition AS definition
    FROM pg_catalog.pg_matviews t3
    WHERE t3.schemaname NOT IN ('pg_catalog', 'information_schema')
    <if test="schemaList != null and schemaList.size() > 0">
        AND t3.schemaname IN
        <foreach item="schema" index="index" collection="schemaList"
            open="(" separator="," close=")">
            #{schema}
        </foreach>
    </if>
    <if test="tableList != null and tableList.size() > 0">
        AND t3.tablename IN
        <foreach item="table" index="index" collection="tableList"
            open="(" separator="," close=")">
            #{table}
        </foreach>
    </if>
)
SELECT 
    schema_name AS "schemaName"
    , logical_table_name AS "logicalTableName"
    , physical_table_name AS "physicalTableName"
    , table_type AS "tableType"
    , concat('|', row_number() over (ORDER BY schema_name, physical_table_name), '|', schema_name, '|', logical_table_name, '|', physical_table_name, '|', table_type, '|', link, '|', remarks, '|') AS "tableInfoList"
    , concat('|', schema_name, '|', logical_table_name, '|', physical_table_name, '|', table_type, '|', remarks, '|') AS "tableInfo"
    , definition AS definition
FROM all_tables
ORDER BY 
    schema_name, physical_table_name;
</select>

<select id="selectColumnInfo" resultType="ColumnDto">
WITH all_columns AS (
    (
        SELECT
            t1.schemaname AS schema_name,
            t1.tablename AS table_name,
            row_number() over (PARTITION BY t1.schemaname, t1.tablename ORDER BY t3.attnum) AS column_num,
            coalesce(substring(coalesce((t4.description), ''), '^[^\t\r\n\((]*'), '') AS logical_column_name,
            t3.attname AS physical_column_name,
            pg_catalog.format_type(t3.atttypid, t3.atttypmod) AS column_type,
            coalesce((
                SELECT '○'
                FROM pg_catalog.pg_constraint c
                WHERE c.conrelid = t2.oid
                AND t3.attnum = ANY (c.conkey)
                AND c.contype = 'p'
            ), '') AS primary_key,
            CASE t3.attnotnull 
                WHEN true THEN '○' 
                ELSE '' 
            END AS not_null,
            coalesce((
                SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
                FROM pg_catalog.pg_attrdef d
                WHERE d.adrelid = t3.attrelid
                AND d.adnum = t3.attnum
            ), ' ') AS default_value,
            regexp_replace(coalesce(substring(coalesce((t4.description), ''), '^.*[\t\r\n\((]+(.*)'), ''), '[\\))]$', '', 'g') AS remarks
        FROM
            pg_catalog.pg_tables t1
            LEFT OUTER JOIN pg_catalog.pg_class t2
                ON t1.tablename = t2.relname
            LEFT OUTER JOIN pg_catalog.pg_attribute t3
                ON t2.oid = t3.attrelid
            LEFT OUTER JOIN pg_catalog.pg_description t4
                ON t3.attrelid = t4.objoid AND t3.attnum = t4.objsubid
        WHERE
            t1.schemaname NOT IN ('pg_catalog', 'information_schema')
            AND t2.relkind = 'r'
            AND t3.attnum > 0
            AND t3.attname NOT LIKE '%pg.dropped%'
            <if test="schemaList != null and schemaList.size() > 0">
                AND t1.schemaname IN
                <foreach item="schema" index="index" collection="schemaList"
                    open="(" separator="," close=")">
                    #{schema}
                </foreach>
            </if>
            <if test="tableList != null and tableList.size() > 0">
                AND t1.tablename IN
                <foreach item="table" index="index" collection="tableList"
                    open="(" separator="," close=")">
                    #{table}
                </foreach>
            </if>
        ORDER BY
            t1.schemaname, t1.tablename, t3.attnum
    )    
    UNION ALL
    (
        SELECT
            t1.schemaname AS schema_name,
            t1.viewname AS table_name,
            row_number() over (PARTITION BY t1.schemaname, t1.viewname ORDER BY t3.attnum) AS column_num,
            coalesce(substring(coalesce((t4.description), ''), '^[^\t\r\n\((]*'), '') AS logical_column_name,
            t3.attname AS physical_column_name,
            pg_catalog.format_type(t3.atttypid, t3.atttypmod) AS column_type,
            coalesce((
                SELECT '○'
                FROM pg_catalog.pg_constraint c
                WHERE c.conrelid = t2.oid
                AND t3.attnum = ANY (c.conkey)
                AND c.contype = 'p'
            ), '') AS primary_key,
            CASE t3.attnotnull 
                WHEN true THEN '○' 
                ELSE '' 
            END AS not_null,
            coalesce((
                SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
                FROM pg_catalog.pg_attrdef d
                WHERE d.adrelid = t3.attrelid
                AND d.adnum = t3.attnum
            ), ' ') AS default_value,
            regexp_replace(coalesce(substring(coalesce((t4.description), ''), '^.*[\t\r\n\((]+(.*)'), ''), '[\\))]$', '', 'g') AS remarks
        FROM
            pg_catalog.pg_views t1
            LEFT OUTER JOIN pg_catalog.pg_class t2
                ON t1.viewname = t2.relname
            LEFT OUTER JOIN pg_catalog.pg_attribute t3
                ON t2.oid = t3.attrelid
            LEFT OUTER JOIN pg_catalog.pg_description t4
                ON t3.attrelid = t4.objoid AND t3.attnum = t4.objsubid
        WHERE
            t1.schemaname NOT IN ('pg_catalog', 'information_schema')
            AND t2.relkind = 'v'
            AND t3.attnum > 0
            AND t3.attname NOT LIKE '%pg.dropped%'
            <if test="schemaList != null and schemaList.size() > 0">
                AND t1.schemaname IN
                <foreach item="schema" index="index" collection="schemaList"
                    open="(" separator="," close=")">
                    #{schema}
                </foreach>
            </if>
            <if test="tableList != null and tableList.size() > 0">
                AND t1.viewname IN
                <foreach item="table" index="index" collection="tableList"
                    open="(" separator="," close=")">
                    #{table}
                </foreach>
            </if>
        ORDER BY
            t1.schemaname, t1.viewname, t3.attnum
    )
    UNION ALL
    (
        SELECT
            t1.schemaname AS schema_name,
            t1.matviewname AS table_name,
            row_number() over (PARTITION BY t1.schemaname, t1.matviewname ORDER BY t3.attnum) AS column_num,
            coalesce(substring(coalesce((t4.description), ''), '^[^\t\r\n\((]*'), '') AS logical_column_name,
            t3.attname AS physical_column_name,
            pg_catalog.format_type(t3.atttypid, t3.atttypmod) AS column_type,
            coalesce((
                SELECT '○'
                FROM pg_catalog.pg_constraint c
                WHERE c.conrelid = t2.oid
                AND t3.attnum = ANY (c.conkey)
                AND c.contype = 'p'
            ), '') AS primary_key,
            CASE t3.attnotnull 
                WHEN true THEN '○' 
                ELSE '' 
            END AS not_null,
            coalesce((
                SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
                FROM pg_catalog.pg_attrdef d
                WHERE d.adrelid = t3.attrelid
                AND d.adnum = t3.attnum
            ), ' ') AS default_value,
            regexp_replace(coalesce(substring(coalesce((t4.description), ''), '^.*[\t\r\n\((]+(.*)'), ''), '[\\))]$', '', 'g') AS remarks
        FROM
            pg_catalog.pg_matviews t1
            LEFT OUTER JOIN pg_catalog.pg_class t2
                ON t1.matviewname = t2.relname
            LEFT OUTER JOIN pg_catalog.pg_attribute t3
                ON t2.oid = t3.attrelid
            LEFT OUTER JOIN pg_catalog.pg_description t4
                ON t3.attrelid = t4.objoid AND t3.attnum = t4.objsubid
        WHERE
            t1.schemaname NOT IN ('pg_catalog', 'information_schema')
            AND t2.relkind = 'm'
            AND t3.attnum > 0
            AND t3.attname NOT LIKE '%pg.dropped%'
            <if test="schemaList != null and schemaList.size() > 0">
                AND t1.schemaname IN
                <foreach item="schema" index="index" collection="schemaList"
                    open="(" separator="," close=")">
                    #{schema}
                </foreach>
            </if>
            <if test="tableList != null and tableList.size() > 0">
                AND t1.matviewname IN
                <foreach item="table" index="index" collection="tableList"
                    open="(" separator="," close=")">
                    #{table}
                </foreach>
            </if>
        ORDER BY
            t1.schemaname, t1.matviewname, t3.attnum
    )
)
SELECT
    schema_name AS "schemaName",
    table_name AS "tableName",
    concat('|', column_num, '|', logical_column_name, '|', physical_column_name, '|', column_type, '|', primary_key, '|', not_null, '|', default_value, '|', remarks, '|') AS "columnInfo"
FROM
    all_columns
ORDER BY
    schema_name, table_name, column_num;
</select>

<select id="selectIndexInfo" resultType="IndexDto">
WITH all_indexes AS (
    SELECT
        t5.schemaname AS schema_name,
        t1.relname AS table_name,
        t2.relname AS index_name,
        coalesce(array_to_string(array_agg(t4.attname ORDER BY t4.attnum), ','), '') AS column_names
    FROM
        pg_class AS t1
    LEFT OUTER JOIN
        pg_index AS t3 ON t1.oid = t3.indrelid
    LEFT OUTER JOIN
        pg_class AS t2 ON t2.oid = t3.indexrelid
    LEFT OUTER JOIN
        pg_attribute AS t4 ON t4.attrelid = t1.oid AND t4.attnum = ANY(t3.indkey)
    LEFT OUTER JOIN
        pg_tables AS t5 ON t1.relname = t5.tablename
    WHERE
        t3.indisprimary = false
        AND t3.indisunique = false
        AND t1.relkind = 'r'
        AND t5.schemaname NOT IN ('pg_catalog', 'information_schema')
        <if test="schemaList != null and schemaList.size() > 0">
            AND t5.schemaname IN
            <foreach item="schema" index="index" collection="schemaList"
                open="(" separator="," close=")">
                #{schema}
            </foreach>
        </if>
        <if test="tableList != null and tableList.size() > 0">
            AND t1.relname IN
            <foreach item="table" index="index" collection="tableList"
                open="(" separator="," close=")">
                #{table}
            </foreach>
        </if>
    GROUP BY
        t5.schemaname, t1.relname, t2.relname
)
SELECT
    schema_name AS "schemaName",
    table_name AS "tableName",
    CASE WHEN index_name IS NOT NULL THEN
        concat('|', row_number() over (PARTITION BY schema_name, table_name ORDER BY index_name), '|', index_name, '|', column_names, '|')
    ELSE NULL END AS "indexInfo"
FROM
    all_indexes
ORDER BY
    schema_name, table_name;
</select>

<select id="selectConstraintInfo" resultType="ConstraintDto">
WITH all_constraints AS (
    SELECT
        t3.nspname AS schema_name,
        t2.relname AS table_name,
        t1.conname AS constraints_name,
        CASE t1.contype 
            WHEN 'c' THEN 'CHECK' 
            WHEN 'f' THEN 'FOREIGN KEY' 
            WHEN 'p' THEN 'PRIMARY KEY' 
            WHEN 'u' THEN 'UNIQUE' 
            ELSE NULL 
        END AS constraints_type,
        pg_get_constraintdef(t1.oid) AS constraints_definition
    FROM
        pg_catalog.pg_constraint t1
    LEFT OUTER JOIN pg_catalog.pg_class t2
        ON t1.conrelid = t2.oid
    LEFT OUTER JOIN pg_catalog.pg_namespace t3
        ON t2.relnamespace = t3.oid
    WHERE
        t1.contype in ('c', 'f', 'p', 'u')
        AND t3.nspname NOT IN ('pg_catalog', 'information_schema')
        <if test="schemaList != null and schemaList.size() > 0">
            AND t3.nspname IN
            <foreach item="schema" index="index" collection="schemaList"
                open="(" separator="," close=")">
                #{schema}
            </foreach>
        </if>
        <if test="tableList != null and tableList.size() > 0">
            AND t2.relname IN
            <foreach item="table" index="index" collection="tableList"
                open="(" separator="," close=")">
                #{table}
            </foreach>
        </if>
)
SELECT
    schema_name AS "schemaName",
    table_name AS "tableName",
    concat('|', row_number() over (PARTITION BY schema_name, table_name ORDER BY constraints_type, constraints_name), '|', constraints_name, '|', constraints_type, '|', constraints_definition, '|') AS "constraintInfo"
FROM
    all_constraints
ORDER BY
    schema_name, table_name, constraints_type, constraints_name;
</select>

<select id="selectForeignkeyInfo" resultType="ForeignkeyDto">
WITH all_foreignkeys AS (
    SELECT
        t1.constraint_name AS foreignkey_name,
        t2.table_schema AS schema_name,
        t2.table_name AS table_name,
        coalesce(array_to_string(array_agg(DISTINCT t2.column_name), ','), '') AS column_names,
        t3.table_schema AS reference_schema_name,
        t3.table_name AS reference_table_name,
        coalesce(array_to_string(array_agg(DISTINCT t3.column_name), ','), '') AS reference_column_names
    FROM
        information_schema.table_constraints AS t1
    LEFT OUTER JOIN information_schema.key_column_usage AS t2
        ON t1.constraint_name = t2.constraint_name
        AND t1.constraint_catalog = t2.table_catalog
        AND t1.constraint_schema = t2.table_schema
        AND t1.table_name = t2.table_name
    LEFT OUTER JOIN information_schema.constraint_column_usage AS t3
        ON t2.constraint_name = t3.constraint_name
        AND t2.table_catalog = t3.constraint_catalog
        AND t2.table_schema = t3.constraint_schema
    WHERE
        t1.constraint_type = 'FOREIGN KEY'
        AND t1.constraint_schema NOT IN ('pg_catalog', 'information_schema')
        <if test="schemaList != null and schemaList.size() > 0">
            AND t1.constraint_schema IN
            <foreach item="schema" index="index" collection="schemaList"
                open="(" separator="," close=")">
                #{schema}
            </foreach>
        </if>
        <if test="tableList != null and tableList.size() > 0">
            AND t2.table_schema IN
            <foreach item="table" index="index" collection="tableList"
                open="(" separator="," close=")">
                #{table}
            </foreach>
        </if>
    GROUP BY
        t1.constraint_name, t2.table_schema, t2.table_name, t3.table_schema, t3.table_name
    ORDER BY 
        t1.constraint_name, t2.table_schema, t2.table_name, t3.table_schema, t3.table_name
)
SELECT
    schema_name AS "schemaName",
    table_name AS "tableName",
    CASE WHEN foreignkey_name IS NOT NULL THEN
        concat('|', row_number() over (PARTITION BY schema_name, table_name ORDER BY foreignkey_name), 
        '|', foreignkey_name, '|', column_names, '|', reference_schema_name, '.' , reference_table_name, '|', reference_column_names, '|')
    ELSE NULL END AS "foreignkeyInfo"
FROM
    all_foreignkeys
ORDER BY
    schema_name, table_name;
</select>

</mapper>

ソースコードの改善点

SQLの実行結果のDTOをStreamAPIやラムダ式を利用してEntityに変換しています。
一見すると関数型インターフェースを活用できているように見えますが、改善点があります。

改善点:重複コードが多い

次のメソッドは、DTOからEntityへの変換処理ですが、

DTOをEntityに変換するメソッド(make${Entity}Listメソッド)
    private List<ColumnEntity> makeColumnEntityList(List<ColumnDto> dtoList) {
        return dtoList.stream()
                .map(dto -> new ColumnEntity(dto.getSchemaName(), dto.getTableName(),
                        dto.getColumnInfo())).collect(Collectors.toList());
    }
    private List<IndexEntity> makeIndexEntityList(List<IndexDto> dtoList) {
        return dtoList.stream()
                .map(dto -> new IndexEntity(dto.getSchemaName(), dto.getTableName(),
                        dto.getIndexInfo())).collect(Collectors.toList());
    }
  • DTOとEntityの型
  • DTOをEntityに変換する処理
    • StreamAPIの中間操作を行うmapメソッドで実行しているラムダ式

以外は同じ動作になっています。

また、次のメソッドは、SQL実行DTOからEntityへの変換処理の呼出しの2つを行っていますが、

SQL実行メソッド(select${Entity}Infoメソッド)
    @Override
    public List<ColumnEntity> selectColumnInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<ColumnDto> dtoList = session.selectList(baseSqlPath + "selectColumnInfo", param);
            return makeColumnEntityList(dtoList);
        }
    }
    @Override
    public List<IndexEntity> selectIndexInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<IndexDto> dtoList = session.selectList(baseSqlPath + "selectIndexInfo", param);
            return makeIndexEntityList(dtoList);
        }
    }
  • どのSQLを実行するか
  • DTOとEntityの型
  • DTOをEntityに変換する処理

が異なる以外は同じ動作になっています。

改善のポイント

重複コードが多いのが上記のソースコードの改善点でした。

改善するには、重複コードの共通化が必要です。共通化を実現するには、

  • DTOとEntityの型定義
  • DTOをEntityに変換する実装

それぞれを動的に指定できることがポイントになりそうです。

修正後のソースコード

上記のソースを型パラメーター関数型インターフェースを活用して改善していきます。

DTOからEntityへの変換処理(make${Entity}Listメソッド) の改善

まずはDTOをEntityに変換するメソッドを改善します。
インターフェースであるTableDefinitionRepository.javaにデフォルトメソッドとしてmakeEntityListメソッドを追加します。

TableDefinitionRepository.java
public interface TableDefinitionRepository {

    // 他のメソッドは省略

+    /**
+     * DTOのListをEntityのListに変換する共通メソッド
+     * 
+     * @param <T> DTOクラスの型
+     * @param <R> Entityクラスの型
+     * @param dtoList DTOのList
+     * @param mapper DTOからEntityへの変換関数
+     * @return EntityのList
+     */
+    default <T, R> List<R> makeEntityList(List<T> dtoList, Function<T, R> mapper) {
+        return dtoList.stream()
+                .map(mapper)
+                .collect(Collectors.toList());
+    }
}

型パラメーターについて

今回の makeEntityList メソッドでは、型パラメーター <T, R> を使っています。

  • T入力の型(この例では DTO クラス)
  • R出力の型(この例では Entity クラス)

つまり「T を受け取って R を返す」ような処理を共通化したい、という設計になっています。
型パラメーターを利用することで、メソッドの宣言時にはどの型でも動作するようなり、具体的な型はメソッド呼び出し時に決定されます。

Function<T, R>について

引数のFunction<T, R> mapperは、DTOからEntityへの変換を行う関数を表します。
以下の修正前コードのラムダ式が関数型インターフェースFunction<T, R>の引数のmapperに該当します。

.map(dto -> new ColumnEntity(dto.getSchemaName(), dto.getTableName(), dto.getColumnInfo()))

また、Function<T, R>Tは引数の型(=DTOクラス)、Rは戻り値の型(=Entityクラス)です。

つまり、下記のラムダ式をmakeEntityListの引数として渡すことができます。

  • 引数  :DTOクラス
  • 戻り値 :Entityクラス
  • 処理内容:DTOからEntityへの変換処理
TableDefinitionRepository.java
    @Override
    public List<ColumnEntity> selectColumnInfo(List<String> schemaList, List<String> tableList) {
        try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
            final Map<String, Object> param = new HashMap<>();
            param.put("schemaList", schemaList);
            param.put("tableList", tableList);
            final List<ColumnDto> dtoList = session.selectList(baseSqlPath + "selectColumnInfo", param);
-           return makeColumnEntityList(dtoList);
+           return makeEntityList(dtoList, (ColumnDto dto) -> new ColumnEntity(dto.getSchemaName(), dto.getTableName(), dto.getColumnInfo()));
        }
    }

引数で渡されたmapperが、mapメソッドの中で呼び出され、DTOからEntityへの変換が行われます。
applyメソッドの使用でFunction型の変数はapplyメソッドを通じて実行されると説明しました。
StreamAPIの中間操作であるmapメソッドにおいては、自動的にapplyメソッドが呼び出されます。

TableDefinitionRepository.java
     default <T, R> List<R> makeEntityList(List<T> dtoList, Function<T, R> mapper) {
         return dtoList.stream()
                 .map(mapper)
                 .collect(Collectors.toList());
     }

これで、SQLごとに必要なDTOからEntityへの変換処理を共通化することができました。

ただし、DTOからEntityへの変換処理をラムダ式で指定するのは冗長です。
DTOからEntityへの変換処理をDTOクラスに実装して、メソッド参照を利用することで、より簡潔に記述できます。

ColumnDto.java
@Data
public class ColumnDto {
    private String schemaName;
    private String tableName;
    private String columnInfo;
+   public ColumnEntity toEntity() {
+       return new ColumnEntity(schemaName, tableName, columnInfo);
+   }
}
TableDefinitionRepository.java
-           return makeEntityList(dtoList, (ColumnDto dto) -> new ColumnEntity(dto.getSchemaName(), dto.getTableName(), dto.getColumnInfo()));
+           return makeEntityList(dtoList, ColumnDto::toEntity);

クラス名/インスタンス変数名::メソッド名の形式でメソッド参照を指定できます。

SQL実行メソッド(select${Entity}Infoメソッド) の改善

次にSQL実行メソッドを改善するにあたり、各メソッドで共通している部分を抜き出します。
DTOとEntityの型定義が異なる部分は型パラメーターで対応します。

  • 戻り値の型:List<R>
  • SQLの実行結果のDTOの型:List<T>

また、SQLの実行結果をDTOからEntityに変換する処理は、先ほど作成したmakeEntityListメソッドを利用します。

TableDefinitionRepository.java
   private <T, R> List<R> selectTableDefinition(List<String> schemaList, List<String> tableList, String sqlId,
           Function<T, R> mapper) {
       try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
           final Map<String, Object> param = new HashMap<>();
           param.put("schemaList", schemaList);
           param.put("tableList", tableList);
           final List<T> dtoList = session.selectList(baseSqlPath + sqlId, param);
           return makeEntityList(dtoList, mapper);
       }
   }

上記のselectTableDefinitionメソッドを利用すると、重複コードを一掃できることがわかります。

TableDefinitionRepository.java
    /**
     * {@inheritDoc}
     */
    @Override
    public List<TableEntity> selectTableInfo(List<String> schemaList, List<String> tableList) {
-       try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
-           final Map<String, Object> param = new HashMap<>();
-           param.put("schemaList", schemaList);
-           param.put("tableList", tableList);
-           final List<TableDto> dtoList = session.selectList(baseSqlPath + "selectTableInfo", param);
-           return makeTableEntityList(dtoList);
-       }
+       return selectTableDefinition(schemaList, tableList, "selectTableInfo", TableDto::toEntity);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<ColumnEntity> selectColumnInfo(List<String> schemaList, List<String> tableList) {
-       try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
-           final Map<String, Object> param = new HashMap<>();
-           param.put("schemaList", schemaList);
-           param.put("tableList", tableList);
-           final List<ColumnDto> dtoList = session.selectList(baseSqlPath + "selectColumnInfo", param);
-           return makeColumnEntityList(dtoList);
-       }
+       return selectTableDefinition(schemaList, tableList, "selectColumnInfo", ColumnDto::toEntity);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<IndexEntity> selectIndexInfo(List<String> schemaList, List<String> tableList) {
-       try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
-           final Map<String, Object> param = new HashMap<>();
-           param.put("schemaList", schemaList);
-           param.put("tableList", tableList);
-           final List<IndexDto> dtoList = session.selectList(baseSqlPath + "selectIndexInfo", param);
-           return makeIndexEntityList(dtoList);
-       }
+       return selectTableDefinition(schemaList, tableList, "selectIndexInfo", IndexDto::toEntity);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<ConstraintEntity> selectConstraintInfo(List<String> schemaList, List<String> tableList) {
-       try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
-           final Map<String, Object> param = new HashMap<>();
-           param.put("schemaList", schemaList);
-           param.put("tableList", tableList);
-           final List<ConstraintDto> dtoList = session.selectList(baseSqlPath +"selectConstraintInfo", param);
-           return makeConstraintEntityList(dtoList);
-       }
+       return selectTableDefinition(schemaList, tableList, "selectConstraintInfo", ConstraintDto::toEntity);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<ForeignkeyEntity> selectForeignkeyInfo(List<String> schemaList, List<String> tableList) {
-       try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
-           final Map<String, Object> param = new HashMap<>();
-           param.put("schemaList", schemaList);
-           param.put("tableList", tableList);
-           final List<ForeignkeyDto> dtoList = session.selectList(baseSqlPath + "selectForeignkeyInfo", param);
-           return makeForeignkeyEntityList(dtoList);
-       }
+       return selectTableDefinition(schemaList, tableList, "selectForeignkeyInfo", ForeignkeyDto::toEntity);
    }
    
+   private <T, R> List<R> selectTableDefinition(List<String> schemaList, List<String> tableList, String sqlId,
+           Function<T, R> mapper) {
+       try (SqlSession session = MyBatisSqlSessionFactory.openSession()) {
+           final Map<String, Object> param = new HashMap<>();
+           param.put("schemaList", schemaList);
+           param.put("tableList", tableList);
+           final List<T> dtoList = session.selectList(baseSqlPath + sqlId, param);
+           return makeEntityList(dtoList, mapper);
+       }
+   }

    private BaseInfoEntity makeBaseInfoEntity(BaseInfoDto dto) {
        return new BaseInfoEntity(dto.getDbName(), dto.getBaseInfo());
    }
-   private List<TableEntity> makeTableEntityList(List<TableDto> dtoList) {
-       return dtoList.stream()
-               .map(dto -> new TableEntity(dto.getSchemaName(), dto.getLogicalTableName(),
-                       dto.getPhysicalTableName(), dto.getTableType(), dto.getTableInfoList(),
-                       dto.getTableInfo(), dto.getDefinition())).collect(Collectors.toList());
-   }
-   private List<ColumnEntity> makeColumnEntityList(List<ColumnDto> dtoList) {
-       return dtoList.stream()
-               .map(dto -> new ColumnEntity(dto.getSchemaName(), dto.getTableName(),
-                       dto.getColumnInfo())).collect(Collectors.toList());
-   }
-   private List<IndexEntity> makeIndexEntityList(List<IndexDto> dtoList) {
-       return dtoList.stream()
-               .map(dto -> new IndexEntity(dto.getSchemaName(), dto.getTableName(),
-                       dto.getIndexInfo())).collect(Collectors.toList());
-   }
-   private List<ConstraintEntity> makeConstraintEntityList(List<ConstraintDto> dtoList) {
-       return dtoList.stream()
-               .map(dto -> new ConstraintEntity(dto.getSchemaName(), dto.getTableName(),
-                       dto.getConstraintInfo())).collect(Collectors.toList());
-   }
-   private List<ForeignkeyEntity> makeForeignkeyEntityList(List<ForeignkeyDto> dtoList) {
-       return dtoList.stream()
-               .map(dto -> new ForeignkeyEntity(dto.getSchemaName(), dto.getTableName(),
-                       dto.getForeignkeyInfo())).collect(Collectors.toList());
-   }

まとめ

  • 型定義を柔軟に扱いたい場合は型パラメーターを活用する
  • 処理内容を柔軟に扱いたい場合は関数型インターフェースを活用する
  1. 詳細は入社1年目でJavaGold合格できたので勉強法について語っていくの記事参照

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?