はじめに
本記事では型パラメーターやJava8から導入された関数型インターフェースを利用して、同じような処理を共通化する方法について紹介します。
自分は過去にJavaGold SE8の資格勉強1をしていました。
その際、関数型インターフェースが出題範囲に含まれていたため、勉強したことはあるのですが、
- 資格勉強においては暗記で乗り切っている
- 自分が経験した現場のコードの多くはJava8以前で実装されているものが多い
ということもあり、StreamAPIやラムダ式を実務では使っているものの、関数型インターフェースそのものを意識して使ったことがありませんでした。
ただ、ここ最近は関数型インターフェースに触れる機会も増えてきたので、実際の修正例を交えて、関数型インターフェースを利用した実装について紹介します。
関数型インターフェースとは
関数型インターフェースとは、抽象メソッドを1つだけ持つインターフェースのことを指します。
Java8で導入されたラムダ式やメソッド参照と組み合わせて利用されることが多いです。
これだけだとイメージが湧きづらいと思うので、詳細については分かりやすそうな記事を紹介します。
関数型インターフェースにはいくつか種類がありますが、今回は関数型インターフェースの中でも、 Function<T, R> を利用したいと思います。
サンプルコード
詳細な説明は上記の記事に譲りますが、簡単なサンプルコードを以下に示します。
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です。
修正前のソースコード
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が気になる方はこちら
<?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への変換処理ですが、
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メソッドで実行しているラムダ式
- StreamAPIの中間操作を行う
以外は同じ動作になっています。
また、次のメソッドは、SQL実行、DTOからEntityへの変換処理の呼出しの2つを行っていますが、
@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メソッドを追加します。
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への変換処理
@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メソッドが呼び出されます。
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クラスに実装して、メソッド参照を利用することで、より簡潔に記述できます。
@Data
public class ColumnDto {
private String schemaName;
private String tableName;
private String columnInfo;
+ public ColumnEntity toEntity() {
+ return new ColumnEntity(schemaName, tableName, columnInfo);
+ }
}
- 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メソッドを利用します。
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メソッドを利用すると、重複コードを一掃できることがわかります。
/**
* {@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年目でJavaGold合格できたので勉強法について語っていくの記事参照 ↩