diff --git a/jig-core/src/main/java/org/dddjava/jig/application/InfrastructureQueryService.java b/jig-core/src/main/java/org/dddjava/jig/application/InfrastructureQueryService.java index 809a5f96b..edaac8fd9 100644 --- a/jig-core/src/main/java/org/dddjava/jig/application/InfrastructureQueryService.java +++ b/jig-core/src/main/java/org/dddjava/jig/application/InfrastructureQueryService.java @@ -29,6 +29,6 @@ public OutputImplementations outputImplementations(JigRepository jigRepository) public DatasourceAngles datasourceAngles(JigRepository jigRepository) { var jigTypes = typesQueryService.jigTypes(jigRepository); var outputImplementations = outputImplementations(jigRepository); - return DatasourceAngles.from(outputImplementations, jigRepository.jigDataProvider().fetchMybatisStatements(), MethodRelations.from(jigTypes)); + return DatasourceAngles.from(outputImplementations, jigRepository.jigDataProvider().fetchSqlStatements(), MethodRelations.from(jigTypes)); } } diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/JigDataProvider.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/JigDataProvider.java index 92b0ab387..e93d3f0e9 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/JigDataProvider.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/JigDataProvider.java @@ -1,7 +1,7 @@ package org.dddjava.jig.domain.model.data; import org.dddjava.jig.domain.model.data.enums.EnumModels; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatements; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatements; import java.util.List; @@ -10,8 +10,8 @@ public interface JigDataProvider { static JigDataProvider none() { return new JigDataProvider() { @Override - public MyBatisStatements fetchMybatisStatements() { - return MyBatisStatements.empty(); + public SqlStatements fetchSqlStatements() { + return SqlStatements.empty(); } @Override @@ -21,7 +21,7 @@ public EnumModels fetchEnumModels() { }; } - MyBatisStatements fetchMybatisStatements(); + SqlStatements fetchSqlStatements(); EnumModels fetchEnumModels(); } diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatementId.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatementId.java deleted file mode 100644 index 319d8a1a4..000000000 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatementId.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.dddjava.jig.domain.model.data.rdbaccess; - -import java.util.Objects; - -/** - * MyBatisのステートメントID - * - * namespaceとidを.で連結したもの。 - * - * 以下のMapperXMLとMapperインタフェースの場合、ステートメントIDは `com.example.mybatis.ExampleMapper.selectAll` となります。 - *
- * {@code
- * 
- *     
- * 
- * }
- * 
- *
- * {@code
- * package com.example.mybatis;
- * interface ExampleMapper {
- *     List selectAll();
- * }
- * }
- * 
- */ -public record MyBatisStatementId(String value, String namespace, String id) { - - public static MyBatisStatementId from(String value) { - var namespaceIdSeparateIndex = value.lastIndexOf('.'); - if (namespaceIdSeparateIndex != -1) { - return new MyBatisStatementId(value, value.substring(0, namespaceIdSeparateIndex), value.substring(namespaceIdSeparateIndex + 1)); - } else { - return new MyBatisStatementId(value, "", value); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MyBatisStatementId that = (MyBatisStatementId) o; - return Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(value); - } - - public String namespace() { - return namespace; - } - - public String id() { - return id; - } -} diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/Query.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/Query.java index e3891c3cb..e9be45fa9 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/Query.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/Query.java @@ -5,30 +5,70 @@ /** * クエリ */ -public record Query(String text) { +public record Query(String rawText, String normalizedQuery) { public static final String UNSUPPORTED = "<>"; public static Query from(@Nullable String text) { if (text == null) return unsupported(); - return new Query(text); + var normalizedQuery = normalizeSql(text); + if (normalizedQuery.isEmpty()) return unsupported(); + return new Query(text, normalizedQuery); } public static Query unsupported() { - return new Query(UNSUPPORTED); + return new Query(UNSUPPORTED, UNSUPPORTED); } @Override - public String text() { - if (UNSUPPORTED.equals(text)) { + public String rawText() { + if (UNSUPPORTED.equals(rawText)) { // 特殊値を返さないようにする // Queryのtextは外部から使用しないので例外でよい。これが発生したらバグ。 throw new IllegalArgumentException("BUG!!"); } - return text; + return rawText; } public boolean supported() { - return !UNSUPPORTED.equals(text); + return !UNSUPPORTED.equals(rawText); + } + + @Override + public String normalizedQuery() { + if (UNSUPPORTED.equals(rawText)) { + // 特殊値を返さないようにする + // Queryのtextは外部から使用しないので例外でよい。これが発生したらバグ。 + throw new IllegalArgumentException("BUG!!"); + } + return normalizedQuery; + } + + private static String normalizeSql(String query) { + String remaining = query; + while (true) { + String trimmed = remaining.stripLeading(); + if (trimmed.startsWith("\uFEFF")) { + remaining = trimmed.substring(1); + continue; + } + if (trimmed.startsWith("--")) { + int newlineIndex = trimmed.indexOf('\n'); + if (newlineIndex < 0) return ""; + remaining = trimmed.substring(newlineIndex + 1); + continue; + } + if (trimmed.startsWith("/*")) { + int commentEndIndex = trimmed.indexOf("*/"); + if (commentEndIndex < 0) return ""; + remaining = trimmed.substring(commentEndIndex + 2); + continue; + } + if (trimmed.startsWith("(")) { + remaining = trimmed.substring(1); + continue; + } + return trimmed; + } } } diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatement.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatement.java similarity index 56% rename from jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatement.java rename to jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatement.java index 593ba85dc..0070a1f52 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatement.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatement.java @@ -3,11 +3,11 @@ /** * SQL */ -public record MyBatisStatement(MyBatisStatementId myBatisStatementId, Query query, SqlType sqlType) { +public record SqlStatement(SqlStatementId sqlStatementId, Query query, SqlType sqlType) { public Tables tables() { if (query.supported()) { - Table table = sqlType.extractTable(query.text(), myBatisStatementId); + Table table = sqlType.extractTable(query.normalizedQuery(), sqlStatementId); return new Tables(table); } return new Tables(sqlType.unexpectedTable()); diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatementId.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatementId.java new file mode 100644 index 000000000..e5159e926 --- /dev/null +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatementId.java @@ -0,0 +1,43 @@ +package org.dddjava.jig.domain.model.data.rdbaccess; + +import java.util.Objects; + +/** + * SQLステートメントID + * + * namespaceとidを.で連結したもの。 + * + * TODO namespaceやidはMyBatisの用語なのでこの形のままとするかは一考の余地がある + */ +public record SqlStatementId(String value, String namespace, String id) { + + public static SqlStatementId from(String value) { + var namespaceIdSeparateIndex = value.lastIndexOf('.'); + if (namespaceIdSeparateIndex != -1) { + return new SqlStatementId(value, value.substring(0, namespaceIdSeparateIndex), value.substring(namespaceIdSeparateIndex + 1)); + } else { + return new SqlStatementId(value, "", value); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SqlStatementId that = (SqlStatementId) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + public String namespace() { + return namespace; + } + + public String id() { + return id; + } +} diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatements.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatements.java similarity index 52% rename from jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatements.java rename to jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatements.java index 5e8cdaebd..183457017 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/MyBatisStatements.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlStatements.java @@ -8,34 +8,34 @@ /** * SQL一覧 */ -public record MyBatisStatements(List list) { +public record SqlStatements(List list) { - public static MyBatisStatements empty() { - return new MyBatisStatements(Collections.emptyList()); + public static SqlStatements empty() { + return new SqlStatements(Collections.emptyList()); } private Tables tables(SqlType sqlType) { return list.stream() - .filter(myBatisStatement -> myBatisStatement.sqlType() == sqlType) - .map(MyBatisStatement::tables) + .filter(sqlStatement -> sqlStatement.sqlType() == sqlType) + .map(SqlStatement::tables) .reduce(Tables::merge) .orElse(Tables.nothing()); } - public Optional findById(MyBatisStatementId myBatisStatementId) { + public Optional findById(SqlStatementId sqlStatementId) { return list.stream() - .filter(myBatisStatement -> myBatisStatement.myBatisStatementId().equals(myBatisStatementId)) + .filter(sqlStatement -> sqlStatement.sqlStatementId().equals(sqlStatementId)) .findFirst(); } /** * 引数のメソッドに関連するステートメントに絞り込む */ - public MyBatisStatements filterRelationOn(Predicate myBatisStatementPredicate) { - List myBatisStatements = list.stream() - .filter(myBatisStatementPredicate) + public SqlStatements filterRelationOn(Predicate sqlStatementPredicate) { + List sqlStatements = list.stream() + .filter(sqlStatementPredicate) .toList(); - return new MyBatisStatements(myBatisStatements); + return new SqlStatements(sqlStatements); } public boolean isEmpty() { diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlType.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlType.java index 3fc7f4b09..eaceb30c6 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlType.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/data/rdbaccess/SqlType.java @@ -4,6 +4,8 @@ import org.slf4j.LoggerFactory; import java.util.List; +import java.util.Locale; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -33,7 +35,7 @@ public enum SqlType { * 現在は1テーブルのみ対応 * 複問い合わせやWITHなどは未対応 */ - public Table extractTable(String sql, MyBatisStatementId myBatisStatementId) { + public Table extractTable(String sql, SqlStatementId sqlStatementId) { for (Pattern pattern : patterns) { Matcher matcher = pattern.matcher(sql.replaceAll("\n", " ")); if (matcher.matches()) { @@ -41,10 +43,9 @@ public Table extractTable(String sql, MyBatisStatementId myBatisStatementId) { } } - logger.warn("{} {} を {} としてテーブル名が解析できませんでした。このMapper由来の解析結果はドキュメントに出力されません。" + - "MyBatisの動的なSQLなどは完全に再現できません。JIGが認識しているSQL文=[{}]", - myBatisStatementId.namespace(), - myBatisStatementId.id(), + logger.warn("{} {} を {} としてテーブル名が解析できませんでした。テーブル名は「解析失敗」と表示されます。JIGが認識しているSQL文=[{}]", + sqlStatementId.namespace(), + sqlStatementId.id(), this, sql); return unexpectedTable(); } @@ -52,4 +53,15 @@ public Table extractTable(String sql, MyBatisStatementId myBatisStatementId) { public Table unexpectedTable() { return new Table("(解析失敗)"); } + + public static Optional inferSqlTypeFromQuery(Query query) { + String normalizedQuery = query.normalizedQuery().toLowerCase(Locale.ROOT); + if (normalizedQuery.startsWith("insert")) return Optional.of(INSERT); + if (normalizedQuery.startsWith("select")) return Optional.of(SELECT); + if (normalizedQuery.startsWith("update")) return Optional.of(UPDATE); + if (normalizedQuery.startsWith("delete")) return Optional.of(DELETE); + + logger.info("SQLの種類がQuery文字列 [{}] から判別できませんでした。", query.rawText()); + return Optional.empty(); + } } diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/information/outputs/OutputImplementations.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/information/outputs/OutputImplementations.java index f2bb1d366..caad7d7fa 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/information/outputs/OutputImplementations.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/information/outputs/OutputImplementations.java @@ -1,8 +1,12 @@ package org.dddjava.jig.domain.model.information.outputs; import org.dddjava.jig.domain.model.information.types.JigTypes; +import org.dddjava.jig.domain.model.data.types.JavaTypeDeclarationKind; import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.function.Function; import java.util.stream.Stream; import static java.util.stream.Collectors.collectingAndThen; @@ -25,13 +29,29 @@ public Gateways repositoryMethods() { // FIXME これのテストがない public static OutputImplementations from(JigTypes jigTypes, OutputAdapters outputAdapters) { return outputAdapters.stream() - // output adapterの実装しているoutput portのgatewayを - .flatMap(outputAdapter -> outputAdapter.implementsPortStream(jigTypes) + // interfaceのRepository(Spring Data JDBCなど)は実装クラスが存在しないため、自身をoutput portとして扱う + .flatMap(outputAdapter -> outputPorts(outputAdapter, jigTypes) .flatMap(outputPort -> outputPort.gatewayStream() // 実装しているinvocationが .flatMap(gateway -> outputAdapter.resolveInvocation(gateway).stream() .map(invocation -> new OutputImplementation(gateway, invocation, outputPort))))) - .collect(collectingAndThen(toList(), OutputImplementations::new)); + .collect(collectingAndThen(toList(), outputImplementations -> + new OutputImplementations(outputImplementations.stream() + .collect(collectingAndThen( + java.util.stream.Collectors.toMap( + outputImplementation -> outputImplementation.outputPortGateway().jigMethodId().namespace() + + "#" + outputImplementation.outputPortGateway().name(), + Function.identity(), + (existing, ignored) -> existing, + LinkedHashMap::new), + map -> List.copyOf(map.values())))))); + } + + private static Stream outputPorts(OutputAdapter outputAdapter, JigTypes jigTypes) { + if (outputAdapter.jigType().jigTypeHeader().javaTypeDeclarationKind() == JavaTypeDeclarationKind.INTERFACE) { + return Stream.of(new OutputPort(outputAdapter.jigType())); + } + return outputAdapter.implementsPortStream(jigTypes); } public Stream stream() { diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/knowledge/datasource/DatasourceAngles.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/knowledge/datasource/DatasourceAngles.java index fbbc10465..36ba3b626 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/knowledge/datasource/DatasourceAngles.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/knowledge/datasource/DatasourceAngles.java @@ -1,8 +1,9 @@ package org.dddjava.jig.domain.model.knowledge.datasource; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatementId; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatements; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatementId; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatements; import org.dddjava.jig.domain.model.information.members.CallerMethods; +import org.dddjava.jig.domain.model.information.outputs.OutputImplementation; import org.dddjava.jig.domain.model.information.outputs.OutputImplementations; import org.dddjava.jig.domain.model.information.relation.methods.CallerMethodsFactory; @@ -14,17 +15,14 @@ */ public record DatasourceAngles(List list) { - public static DatasourceAngles from(OutputImplementations outputImplementations, MyBatisStatements myBatisStatements, CallerMethodsFactory callerMethodsFactory) { + public static DatasourceAngles from(OutputImplementations outputImplementations, SqlStatements sqlStatements, CallerMethodsFactory callerMethodsFactory) { return new DatasourceAngles(outputImplementations.stream() .map(outputImplementation -> { CallerMethods callerMethods = callerMethodsFactory.callerMethodsOf(outputImplementation.outputPortGateway().jigMethodId()); - var crudTables = myBatisStatements.filterRelationOn(myBatisStatement -> { - MyBatisStatementId myBatisStatementId = myBatisStatement.myBatisStatementId(); - // namespaceはメソッドの型のFQNに該当し、idはメソッド名に該当するので、それを比較する。 - return outputImplementation.usingMethods() - .containsAny(methodCall -> methodCall.methodOwner().fqn().equals(myBatisStatementId.namespace()) - && methodCall.methodName().equals(myBatisStatementId.id())); + var crudTables = sqlStatements.filterRelationOn(sqlStatement -> { + SqlStatementId sqlStatementId = sqlStatement.sqlStatementId(); + return gatewayUseSQL(outputImplementation, sqlStatementId) || invocationUseSQL(outputImplementation, sqlStatementId); }).crudTables(); return new DatasourceAngle(outputImplementation, crudTables, callerMethods); @@ -32,4 +30,30 @@ public static DatasourceAngles from(OutputImplementations outputImplementations, .sorted(Comparator.comparing(datasourceAngle -> datasourceAngle.interfaceMethod().jigMethodId().value())) .toList()); } + + /** + * InvocationがDBアクセスしているかを判定する + * + * 使用しているメソッドがSQLステートメントかで判断する + * TODO プライベートメソッドとか辿らないといけないような・・・ + */ + private static boolean invocationUseSQL(OutputImplementation outputImplementation, SqlStatementId sqlStatementId) { + return outputImplementation.usingMethods() + .containsAny(methodCall -> matchStatement(sqlStatementId, methodCall.methodOwner().fqn(), methodCall.methodName())); + } + + /** + * GatewayがDBアクセスするものかを判定する + * + * SpringDataJDBCを直接Serviceで使用している場合などにRepositoryインタフェースとSQLステートメントが一致する。 + */ + private static boolean gatewayUseSQL(OutputImplementation outputImplementation, SqlStatementId sqlStatementId) { + var gatewayMethodId = outputImplementation.outputPortGateway().jigMethodId(); + return matchStatement(sqlStatementId, gatewayMethodId.namespace(), gatewayMethodId.name()); + } + + private static boolean matchStatement(SqlStatementId sqlStatementId, String namespace, String name) { + // namespaceはメソッドの型のFQNに該当し、idはメソッド名に該当するので、それを比較する。 + return namespace.equals(sqlStatementId.namespace()) && name.equals(sqlStatementId.id()); + } } diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/ReadStatus.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/ReadStatus.java index 2d72b0728..2a4a6d289 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/ReadStatus.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/ReadStatus.java @@ -13,8 +13,8 @@ public enum ReadStatus { "バイナリソース(*.class)が見つかりませんでした。出力ディレクトリの指定を確認してください。", "Binary Source file(*.class) was not found. Check the output directory specification."), SQLなし( - "SQLが見つかりませんでした。SQLを実装していない場合やMyBatisを使用していない場合は正常です。CRUDに関わる情報が出力されません。", - "SQL was not found. It is normal if you do not implement SQL or if you are not using MyBatis. If this message appears, CRUD is not output in the data source list."), + "SQLが見つかりませんでした。SQLを実装していない場合やMyBatis・Spring Data JDBCを使用していない場合は正常です。CRUDに関わる情報が出力されません。", + "SQL was not found. It is normal if you do not implement SQL or if you are not using MyBatis/Spring Data JDBC. If this message appears, CRUD is not output in the data source list."), SQL読み込み一部失敗( "SQLの読み込みに一部失敗しました。CRUDの出力に欠落が存在します。", "Partial loading of SQL failed. There is a missing in the output of CRUD."), diff --git a/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/mybatis/MyBatisReadResult.java b/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/mybatis/MyBatisReadResult.java index acddbad8e..6693cdc06 100644 --- a/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/mybatis/MyBatisReadResult.java +++ b/jig-core/src/main/java/org/dddjava/jig/domain/model/sources/mybatis/MyBatisReadResult.java @@ -1,15 +1,15 @@ package org.dddjava.jig.domain.model.sources.mybatis; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatements; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatements; -public record MyBatisReadResult(MyBatisStatements myBatisStatements, SqlReadStatus sqlReadStatus) { +public record MyBatisReadResult(SqlStatements sqlStatements, SqlReadStatus sqlReadStatus) { public MyBatisReadResult(SqlReadStatus sqlReadStatus) { - this(MyBatisStatements.empty(), sqlReadStatus); + this(SqlStatements.empty(), sqlReadStatus); } public SqlReadStatus status() { - if (sqlReadStatus == SqlReadStatus.成功 && myBatisStatements.isEmpty()) { + if (sqlReadStatus == SqlReadStatus.成功 && sqlStatements.isEmpty()) { return SqlReadStatus.SQLなし; } return sqlReadStatus; diff --git a/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigDataProvider.java b/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigDataProvider.java index 16aff3b6e..71423af86 100644 --- a/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigDataProvider.java +++ b/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigDataProvider.java @@ -2,15 +2,15 @@ import org.dddjava.jig.domain.model.data.JigDataProvider; import org.dddjava.jig.domain.model.data.enums.EnumModels; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatements; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatements; import org.dddjava.jig.domain.model.sources.javasources.JavaSourceModel; record DefaultJigDataProvider(JavaSourceModel javaSourceModel, - MyBatisStatements myBatisStatements) implements JigDataProvider { + SqlStatements sqlStatements) implements JigDataProvider { @Override - public MyBatisStatements fetchMybatisStatements() { - return myBatisStatements; + public SqlStatements fetchSqlStatements() { + return sqlStatements; } @Override diff --git a/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigRepositoryFactory.java b/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigRepositoryFactory.java index cbd9638fb..193b9f24f 100644 --- a/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigRepositoryFactory.java +++ b/jig-core/src/main/java/org/dddjava/jig/infrastructure/javaproductreader/DefaultJigRepositoryFactory.java @@ -6,7 +6,7 @@ import org.dddjava.jig.application.GlossaryRepository; import org.dddjava.jig.application.JigEventRepository; import org.dddjava.jig.domain.model.data.JigDataProvider; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatements; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatements; import org.dddjava.jig.domain.model.data.terms.Glossary; import org.dddjava.jig.domain.model.data.types.JigTypeHeader; import org.dddjava.jig.domain.model.information.JigRepository; @@ -17,16 +17,19 @@ import org.dddjava.jig.domain.model.sources.filesystem.SourceBasePaths; import org.dddjava.jig.domain.model.sources.javasources.JavaSourceModel; import org.dddjava.jig.domain.model.sources.mybatis.MyBatisStatementsReader; +import org.dddjava.jig.domain.model.sources.mybatis.SqlReadStatus; import org.dddjava.jig.infrastructure.asm.AsmClassSourceReader; import org.dddjava.jig.infrastructure.asm.ClassDeclaration; import org.dddjava.jig.infrastructure.configuration.Configuration; import org.dddjava.jig.infrastructure.javaparser.JavaparserReader; import org.dddjava.jig.infrastructure.mybatis.MyBatisStatementsReaderImpl; +import org.dddjava.jig.infrastructure.springdatajdbc.SpringDataJdbcStatementsReader; import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; public class DefaultJigRepositoryFactory { @@ -35,15 +38,17 @@ public class DefaultJigRepositoryFactory { private final AsmClassSourceReader asmClassSourceReader; private final JavaparserReader javaparserReader; private final MyBatisStatementsReader myBatisStatementsReader; + private final SpringDataJdbcStatementsReader springDataJdbcStatementsReader; private final JigEventRepository jigEventRepository; private final GlossaryRepository glossaryRepository; - public DefaultJigRepositoryFactory(ClassOrJavaSourceCollector sourceCollector, AsmClassSourceReader asmClassSourceReader, JavaparserReader javaparserReader, MyBatisStatementsReader myBatisStatementsReader, JigEventRepository jigEventRepository, GlossaryRepository glossaryRepository) { + public DefaultJigRepositoryFactory(ClassOrJavaSourceCollector sourceCollector, AsmClassSourceReader asmClassSourceReader, JavaparserReader javaparserReader, MyBatisStatementsReader myBatisStatementsReader, SpringDataJdbcStatementsReader springDataJdbcStatementsReader, JigEventRepository jigEventRepository, GlossaryRepository glossaryRepository) { this.sourceCollector = sourceCollector; this.asmClassSourceReader = asmClassSourceReader; this.javaparserReader = javaparserReader; this.myBatisStatementsReader = myBatisStatementsReader; + this.springDataJdbcStatementsReader = springDataJdbcStatementsReader; this.glossaryRepository = glossaryRepository; this.jigEventRepository = jigEventRepository; } @@ -54,6 +59,7 @@ public static DefaultJigRepositoryFactory init(Configuration configuration) { new AsmClassSourceReader(), new JavaparserReader(), new MyBatisStatementsReaderImpl(), + new SpringDataJdbcStatementsReader(), configuration.jigEventRepository(), configuration.glossaryRepository() ); } @@ -102,11 +108,11 @@ private JigRepository analyze(FilesystemSources sources) { Metrics.timer(metricName, "phase", "class_file_parsing").record(() -> asmClassSourceReader.readClasses(sources.classFilePaths()))); - MyBatisStatements myBatisStatements = Objects.requireNonNull(Metrics.timer(metricName, "phase", "mybatis_reading").record(() -> + SqlStatements sqlStatements = Objects.requireNonNull(Metrics.timer(metricName, "phase", "mybatis_reading").record(() -> readMyBatisStatements(sources, classDeclarations))); return Metrics.timer(metricName, "phase", "jig_repository_creation").record(() -> { - DefaultJigDataProvider defaultJigDataProvider = new DefaultJigDataProvider(javaSourceModel, myBatisStatements); + DefaultJigDataProvider defaultJigDataProvider = new DefaultJigDataProvider(javaSourceModel, sqlStatements); JigTypes jigTypes = JigTypeFactory.createJigTypes(classDeclarations, glossaryRepository.all()); return new JigRepository() { @Override @@ -139,7 +145,7 @@ public JigResult.JigSummary summary() { })); } - private MyBatisStatements readMyBatisStatements(FilesystemSources sources, Collection classDeclarations) { + private SqlStatements readMyBatisStatements(FilesystemSources sources, Collection classDeclarations) { // MyBatisの読み込み対象となるMapperインタフェース識別のためにJigTypeHeaderを抽出 Collection jigTypeHeaders = classDeclarations.stream() .map(ClassDeclaration::jigTypeHeader) @@ -148,10 +154,24 @@ private MyBatisStatements readMyBatisStatements(FilesystemSources sources, Colle List classPaths = sources.sourceBasePaths().classSourceBasePaths(); var myBatisReadResult = myBatisStatementsReader.readFrom(jigTypeHeaders, classPaths); + var springDataJdbcStatements = springDataJdbcStatementsReader.readFrom(classDeclarations); - if (myBatisReadResult.status().not正常()) { + SqlStatements mergedStatements = mergeStatements(myBatisReadResult.sqlStatements(), springDataJdbcStatements); + + SqlReadStatus sqlReadStatus = myBatisReadResult.status(); + if (sqlReadStatus == SqlReadStatus.SQLなし && mergedStatements.isEmpty()) { + jigEventRepository.recordEvent(sqlReadStatus.toReadStatus()); + } else if (sqlReadStatus != SqlReadStatus.成功 && sqlReadStatus != SqlReadStatus.SQLなし) { jigEventRepository.recordEvent(myBatisReadResult.status().toReadStatus()); } - return myBatisReadResult.myBatisStatements(); + return mergedStatements; + } + + private SqlStatements mergeStatements(SqlStatements myBatisStatements, SqlStatements springDataJdbcStatements) { + return new SqlStatements(Stream.concat( + myBatisStatements.list().stream(), + springDataJdbcStatements.list().stream()) + .distinct() + .toList()); } } diff --git a/jig-core/src/main/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementsReaderImpl.java b/jig-core/src/main/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementsReaderImpl.java index 1e465c24e..f34f461da 100644 --- a/jig-core/src/main/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementsReaderImpl.java +++ b/jig-core/src/main/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementsReaderImpl.java @@ -50,7 +50,7 @@ public MyBatisReadResult readFrom(Collection jigTypeHeaders, List .toList(); // 該当なしの場合に余計なClassLoader生成やMyBatisの初期化を行わないための早期リターン - if (classNames.isEmpty()) return new MyBatisReadResult(MyBatisStatements.empty(), SqlReadStatus.成功); + if (classNames.isEmpty()) return new MyBatisReadResult(SqlStatements.empty(), SqlReadStatus.成功); URL[] classLocationUrls = classPaths.stream() .flatMap(path -> { @@ -99,14 +99,14 @@ private MyBatisReadResult extractSql(Collection classNames, ClassLoader } } - List list = new ArrayList<>(); + List list = new ArrayList<>(); Collection mappedStatements = config.getMappedStatements(); logger.debug("MappedStatements: {}件", mappedStatements.size()); for (Object obj : mappedStatements) { // config.getMappedStatementsにAmbiguityが入っていることがあったので型を確認する if (obj instanceof MappedStatement mappedStatement) { - MyBatisStatementId myBatisStatementId = MyBatisStatementId.from(mappedStatement.getId()); + SqlStatementId sqlStatementId = resolveStatementId(mappedStatement); Query query; try { @@ -130,13 +130,40 @@ private MyBatisReadResult extractSql(Collection classNames, ClassLoader yield SqlType.SELECT; } }; - MyBatisStatement myBatisStatement = new MyBatisStatement(myBatisStatementId, query, sqlType); + SqlStatement myBatisStatement = new SqlStatement(sqlStatementId, query, sqlType); list.add(myBatisStatement); } } logger.debug("取得したSQL: {}件", list.size()); - return new MyBatisReadResult(new MyBatisStatements(list), sqlReadStatus); + return new MyBatisReadResult(new SqlStatements(list), sqlReadStatus); + } + + /** + * MyBatisのステートメント情報からSQLステートメントIDを作成する + * + * 以下のMapperXMLとMapperインタフェースの場合、ステートメントIDは `com.example.mybatis.ExampleMapper.selectAll` となる。 + * + *
+     * {@code
+     * 
+     *     
+     * 
+     * }
+     * 
+ *
+     * {@code
+     * package com.example.mybatis;
+     * interface ExampleMapper {
+     *     List selectAll();
+     * }
+     * }
+     * 
+ */ + private static SqlStatementId resolveStatementId(MappedStatement mappedStatement) { + return SqlStatementId.from(mappedStatement.getId()); } private Query getQuery(MappedStatement mappedStatement) throws NoSuchFieldException, IllegalAccessException { diff --git a/jig-core/src/main/java/org/dddjava/jig/infrastructure/springdatajdbc/SpringDataJdbcStatementsReader.java b/jig-core/src/main/java/org/dddjava/jig/infrastructure/springdatajdbc/SpringDataJdbcStatementsReader.java new file mode 100644 index 000000000..c38360837 --- /dev/null +++ b/jig-core/src/main/java/org/dddjava/jig/infrastructure/springdatajdbc/SpringDataJdbcStatementsReader.java @@ -0,0 +1,199 @@ +package org.dddjava.jig.infrastructure.springdatajdbc; + +import org.dddjava.jig.domain.model.data.rdbaccess.*; +import org.dddjava.jig.domain.model.data.types.JavaTypeDeclarationKind; +import org.dddjava.jig.domain.model.data.types.JigTypeHeader; +import org.dddjava.jig.domain.model.data.types.JigTypeReference; +import org.dddjava.jig.domain.model.data.types.TypeId; +import org.dddjava.jig.infrastructure.asm.ClassDeclaration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class SpringDataJdbcStatementsReader { + private static final Logger logger = LoggerFactory.getLogger(SpringDataJdbcStatementsReader.class); + + private static final String SPRING_DATA_REPOSITORY_PREFIX = "org.springframework.data.repository."; + private static final String SPRING_DATA_TABLE = "org.springframework.data.relational.core.mapping.Table"; + private static final String SPRING_DATA_QUERY = "org.springframework.data.jdbc.repository.query.Query"; + + /** + * ASMで読み取ったクラス情報から、Spring Data JDBCのRepositoryメソッドをSQLステートメントとして抽出する。 + * + * 対象は次の条件をすべて満たす型: + * 1) interface である + * 2) 継承先(再帰含む)に {@code org.springframework.data.repository.*} を持つ + */ + public SqlStatements readFrom(Collection classDeclarations) { + Map declarationMap = classDeclarations.stream() + .collect(java.util.stream.Collectors.toMap( + declaration -> declaration.jigTypeHeader().id(), + declaration -> declaration, + (left, right) -> left, + LinkedHashMap::new)); + + Map statements = new LinkedHashMap<>(); + + classDeclarations.stream() + .filter(this::isInterface) + .filter(declaration -> extendsSpringDataRepository(declaration.jigTypeHeader(), declarationMap, new HashSet<>())) + .forEach(declaration -> { + Optional tableName = resolveTableName(declaration.jigTypeHeader(), declarationMap, new HashSet<>()); + Map queryByMethodName = declaration.jigMethodDeclarations().stream() + .collect(java.util.stream.Collectors.toMap( + methodDeclaration -> methodDeclaration.header().name(), + methodDeclaration -> methodDeclaration.header().declarationAnnotationStream() + .filter(annotation -> annotation.id().fqn().equals(SPRING_DATA_QUERY)) + .findFirst() + .flatMap(annotation -> annotation.elementTextOf("value")) + .map(Query::from) + .orElse(Query.unsupported()), + (left, right) -> left.supported() ? left : right.supported() ? right : left, + LinkedHashMap::new)) + .entrySet().stream() + .filter(entry -> entry.getValue().supported()) + .collect(java.util.stream.Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (left, right) -> left, + LinkedHashMap::new)); + + declaration.jigMethodDeclarations().stream() + .map(jigMethodDeclaration -> jigMethodDeclaration.header().name()) + .distinct() + .forEach(methodName -> { + Query query = queryByMethodName.getOrDefault(methodName, Query.unsupported()); + Optional inferredSqlType = query.supported() + ? SqlType.inferSqlTypeFromQuery(query) + : inferSqlType(methodName); + inferredSqlType.ifPresent(sqlType -> { + Query resolvedQuery = query.supported() + ? query + : tableName.map(name -> Query.from(defaultQuery(sqlType, name))).orElse(Query.unsupported()); + String statementValue = declaration.jigTypeHeader().fqn() + "." + methodName; + SqlStatementId statementId = SqlStatementId.from(statementValue); + statements.put(statementId, new SqlStatement(statementId, resolvedQuery, sqlType)); + }); + }); + }); + + return new SqlStatements(List.copyOf(statements.values())); + } + + private boolean isInterface(ClassDeclaration declaration) { + JigTypeHeader header = declaration.jigTypeHeader(); + return header.javaTypeDeclarationKind() == JavaTypeDeclarationKind.INTERFACE; + } + + private boolean extendsSpringDataRepository(JigTypeHeader header, Map declarationMap, Set visited) { + if (!visited.add(header.id())) return false; + + for (JigTypeReference interfaceType : header.interfaceTypeList()) { + TypeId interfaceId = interfaceType.id(); + // CrudRepository / PagingAndSortingRepository / Repository などを包含するプレフィックス判定 + if (interfaceId.fqn().startsWith(SPRING_DATA_REPOSITORY_PREFIX)) return true; + + ClassDeclaration declaration = declarationMap.get(interfaceId); + if (declaration != null && extendsSpringDataRepository(declaration.jigTypeHeader(), declarationMap, visited)) { + return true; + } + } + return false; + } + + private Optional resolveTableName(JigTypeHeader repositoryHeader, Map declarationMap, Set visited) { + Optional entityTypeId = resolveEntityTypeId(repositoryHeader, declarationMap, visited); + if (entityTypeId.isEmpty()) return Optional.empty(); + + TypeId typeId = entityTypeId.orElseThrow(); + ClassDeclaration entityDeclaration = declarationMap.get(typeId); + if (entityDeclaration == null) { + return Optional.of(toSnakeCase(typeId.asSimpleText())); + } + + Optional tableName = entityDeclaration.jigTypeHeader().jigTypeAttributes().declarationAnnotationInstances().stream() + .filter(annotation -> annotation.id().fqn().equals(SPRING_DATA_TABLE)) + .findFirst() + .flatMap(annotation -> annotation.elementTextOf("value")) + .filter(value -> !value.isBlank()); + + if (tableName.isPresent()) return tableName; + return Optional.of(toSnakeCase(entityDeclaration.jigTypeHeader().simpleName())); + } + + private Optional resolveEntityTypeId(JigTypeHeader header, Map declarationMap, Set visited) { + if (!visited.add(header.id())) return Optional.empty(); + + for (JigTypeReference interfaceType : header.interfaceTypeList()) { + TypeId interfaceId = interfaceType.id(); + if (interfaceId.fqn().startsWith(SPRING_DATA_REPOSITORY_PREFIX) + && !interfaceType.typeArgumentList().isEmpty()) { + // Repository の先頭型引数Tをエンティティ型として扱う + return Optional.of(interfaceType.typeArgumentList().getFirst().typeId()); + } + + ClassDeclaration declaration = declarationMap.get(interfaceId); + if (declaration != null) { + Optional entityTypeId = resolveEntityTypeId(declaration.jigTypeHeader(), declarationMap, visited); + if (entityTypeId.isPresent()) return entityTypeId; + } + } + return Optional.empty(); + } + + /** + * SQLの種類を推測する + * + * @see Defining Query Methods + */ + private Optional inferSqlType(String methodName) { + String normalizedMethodName = methodName.toLowerCase(Locale.ROOT); + + if (normalizedMethodName.startsWith("find") + || normalizedMethodName.startsWith("read") + || normalizedMethodName.startsWith("get") + || normalizedMethodName.startsWith("query") + || normalizedMethodName.startsWith("count") + || normalizedMethodName.startsWith("exists")) { + return Optional.of(SqlType.SELECT); + } + if (normalizedMethodName.startsWith("save") + || normalizedMethodName.startsWith("insert") + || normalizedMethodName.startsWith("create") + || normalizedMethodName.startsWith("add")) { + return Optional.of(SqlType.INSERT); + } + if (normalizedMethodName.startsWith("update") + || normalizedMethodName.startsWith("set")) { + return Optional.of(SqlType.UPDATE); + } + if (normalizedMethodName.startsWith("delete") + || normalizedMethodName.startsWith("remove")) { + return Optional.of(SqlType.DELETE); + } + + // 判別できないものは空にしておく + logger.info("SQLの種類がメソッド名 {} から判別できませんでした。CRUDのどれかに該当する場合は対象にしたいのでissueお願いします。", methodName); + return Optional.empty(); + } + + private String defaultQuery(SqlType sqlType, String tableName) { + return switch (sqlType) { + case INSERT -> "insert into " + tableName + " values (?)"; + case SELECT -> "select * from " + tableName; + case UPDATE -> "update " + tableName + " set id = id"; + case DELETE -> "delete from " + tableName; + }; + } + + private String toSnakeCase(String text) { + StringBuilder builder = new StringBuilder(text.length() + 4); + for (int i = 0; i < text.length(); i++) { + char current = text.charAt(i); + if (Character.isUpperCase(current) && i > 0) builder.append('_'); + builder.append(Character.toLowerCase(current)); + } + return builder.toString(); + } +} diff --git a/jig-core/src/test/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementReaderTest.java b/jig-core/src/test/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementReaderTest.java index fd41323bf..19ac3dfe2 100644 --- a/jig-core/src/test/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementReaderTest.java +++ b/jig-core/src/test/java/org/dddjava/jig/infrastructure/mybatis/MyBatisStatementReaderTest.java @@ -1,8 +1,8 @@ package org.dddjava.jig.infrastructure.mybatis; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatement; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatementId; -import org.dddjava.jig.domain.model.data.rdbaccess.MyBatisStatements; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatement; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatementId; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatements; import org.dddjava.jig.domain.model.data.rdbaccess.SqlType; import org.dddjava.jig.domain.model.information.JigRepository; import org.junit.jupiter.api.Test; @@ -16,46 +16,47 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; @JigTest class MyBatisStatementReaderTest { @Test void bindを使ってても解析できる(JigRepository jigRepository) { - MyBatisStatements myBatisStatements = jigRepository.jigDataProvider().fetchMybatisStatements(); + SqlStatements myBatisStatements = jigRepository.jigDataProvider().fetchSqlStatements(); - MyBatisStatement myBatisStatement = myBatisStatements.findById(MyBatisStatementId.from(SampleMapper.class.getCanonicalName() + ".binding")).orElseThrow(); + SqlStatement myBatisStatement = myBatisStatements.findById(SqlStatementId.from(SampleMapper.class.getCanonicalName() + ".binding")).orElseThrow(); assertEquals("[fuga]", myBatisStatement.tables().asText()); } @Test void OGNLを使ったSELECTが解析できない(JigRepository jigRepository) { - MyBatisStatements myBatisStatements = jigRepository.jigDataProvider().fetchMybatisStatements(); + SqlStatements myBatisStatements = jigRepository.jigDataProvider().fetchSqlStatements(); - MyBatisStatement myBatisStatement = myBatisStatements.findById(MyBatisStatementId.from(ComplexMapper.class.getCanonicalName() + ".select_ognl")).orElseThrow(); + SqlStatement myBatisStatement = myBatisStatements.findById(SqlStatementId.from(ComplexMapper.class.getCanonicalName() + ".select_ognl")).orElseThrow(); assertEquals("[(解析失敗)]", myBatisStatement.tables().asText()); - // OGNLを使ったSQLは現時点では空になる - assertEquals("", myBatisStatement.query().text()); + // OGNLを使ったSQLは現時点では空になりunsupportedになる + assertFalse(myBatisStatement.query().supported()); } @Test void OGNLを使ったSELECTが解析できない2(JigRepository jigRepository) { - MyBatisStatements myBatisStatements = jigRepository.jigDataProvider().fetchMybatisStatements(); + SqlStatements myBatisStatements = jigRepository.jigDataProvider().fetchSqlStatements(); - MyBatisStatement myBatisStatement = myBatisStatements.findById(MyBatisStatementId.from(ComplexMapper.class.getCanonicalName() + ".select_ognl_where")).orElseThrow(); + SqlStatement myBatisStatement = myBatisStatements.findById(SqlStatementId.from(ComplexMapper.class.getCanonicalName() + ".select_ognl_where")).orElseThrow(); assertEquals("[(解析失敗)]", myBatisStatement.tables().asText()); // OGNLを使ったSQLは現時点では空になる // ・・・のだが、 タグなどで分割されているとOGNLを使用していない部分だけクエリが出てくる - assertEquals("order by 1", myBatisStatement.query().text()); + assertEquals("order by 1", myBatisStatement.query().rawText()); } @ParameterizedTest @MethodSource void 標準的なパターン(String methodName, String tableName, SqlType sqlType, JigRepository jigRepository) { - MyBatisStatements myBatisStatements = jigRepository.jigDataProvider().fetchMybatisStatements(); + SqlStatements myBatisStatements = jigRepository.jigDataProvider().fetchSqlStatements(); - MyBatisStatement myBatisStatement = myBatisStatements.findById(MyBatisStatementId.from("stub.infrastructure.datasource.CanonicalMapper." + methodName)).orElseThrow(); + SqlStatement myBatisStatement = myBatisStatements.findById(SqlStatementId.from("stub.infrastructure.datasource.CanonicalMapper." + methodName)).orElseThrow(); assertEquals("[" + tableName + "]", myBatisStatement.tables().asText()); assertEquals(sqlType, myBatisStatement.sqlType()); } diff --git a/jig-core/src/test/java/org/dddjava/jig/infrastructure/springdatajdbc/SpringDataJdbcStatementReaderTest.java b/jig-core/src/test/java/org/dddjava/jig/infrastructure/springdatajdbc/SpringDataJdbcStatementReaderTest.java new file mode 100644 index 000000000..71aa47d04 --- /dev/null +++ b/jig-core/src/test/java/org/dddjava/jig/infrastructure/springdatajdbc/SpringDataJdbcStatementReaderTest.java @@ -0,0 +1,71 @@ +package org.dddjava.jig.infrastructure.springdatajdbc; + +import org.dddjava.jig.application.JigService; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlStatementId; +import org.dddjava.jig.domain.model.data.rdbaccess.SqlType; +import org.dddjava.jig.domain.model.information.JigRepository; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import stub.infrastructure.datasource.springdata.SpringDataJdbcOrderRepository; +import testing.JigTest; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JigTest +class SpringDataJdbcStatementReaderTest { + + @ParameterizedTest + @MethodSource("repositoryMethodAndSqlType") + void SpringDataJdbcのRepositoryメソッドをSQLとして取得できる( + String methodName, + SqlType expectedSqlType, + JigRepository jigRepository + ) { + var statements = jigRepository.jigDataProvider().fetchSqlStatements(); + var namespace = SpringDataJdbcOrderRepository.class.getCanonicalName(); + var statement = statements.findById(SqlStatementId.from(namespace + "." + methodName)).orElseThrow(); + + assertEquals("[spring_data_jdbc_orders]", statement.tables().asText()); + assertEquals(expectedSqlType, statement.sqlType()); + } + + @ParameterizedTest + @MethodSource("repositoryMethodAndSqlType") + void DatasourceAnglesにSpringDataJdbcのCRUDが反映される( + String methodName, + SqlType expectedSqlType, + JigService jigService, + JigRepository jigRepository + ) { + var datasourceAngles = jigService.datasourceAngles(jigRepository).list().stream() + .filter(angle -> angle.declaringType().fqn().equals(SpringDataJdbcOrderRepository.class.getCanonicalName())) + .toList(); + var angle = datasourceAngles.stream() + .filter(found -> found.interfaceMethod().name().equals(methodName)) + .findFirst() + .orElseThrow(); + + assertEquals(5, datasourceAngles.size()); + var expectedTables = "[spring_data_jdbc_orders]"; + switch (expectedSqlType) { + case INSERT -> assertEquals(expectedTables, angle.insertTables()); + case SELECT -> assertEquals(expectedTables, angle.selectTables()); + case UPDATE -> assertEquals(expectedTables, angle.updateTables()); + case DELETE -> assertEquals(expectedTables, angle.deleteTables()); + default -> throw new IllegalStateException("未対応のSQL種別: " + expectedSqlType); + } + } + + static Stream repositoryMethodAndSqlType() { + return Stream.of( + Arguments.of("save", SqlType.INSERT), + Arguments.of("findById", SqlType.SELECT), + Arguments.of("deleteById", SqlType.DELETE), + Arguments.of("updateById", SqlType.UPDATE), + Arguments.of("updateByIdWithComment", SqlType.UPDATE) + ); + } +} diff --git a/jig-core/src/test/java/org/springframework/data/jdbc/repository/query/Query.java b/jig-core/src/test/java/org/springframework/data/jdbc/repository/query/Query.java new file mode 100644 index 000000000..4944f88b3 --- /dev/null +++ b/jig-core/src/test/java/org/springframework/data/jdbc/repository/query/Query.java @@ -0,0 +1,12 @@ +package org.springframework.data.jdbc.repository.query; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Query { + String value(); +} diff --git a/jig-core/src/test/java/org/springframework/data/package-info.java b/jig-core/src/test/java/org/springframework/data/package-info.java new file mode 100644 index 000000000..e47029ae9 --- /dev/null +++ b/jig-core/src/test/java/org/springframework/data/package-info.java @@ -0,0 +1,6 @@ +/** + * SpringDataJdbc対応のテスト用 + * + * テストだけなら依存させていい気はする + */ +package org.springframework.data; \ No newline at end of file diff --git a/jig-core/src/test/java/org/springframework/data/relational/core/mapping/Table.java b/jig-core/src/test/java/org/springframework/data/relational/core/mapping/Table.java new file mode 100644 index 000000000..bbbbae751 --- /dev/null +++ b/jig-core/src/test/java/org/springframework/data/relational/core/mapping/Table.java @@ -0,0 +1,12 @@ +package org.springframework.data.relational.core.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Table { + String value() default ""; +} diff --git a/jig-core/src/test/java/org/springframework/data/repository/CrudRepository.java b/jig-core/src/test/java/org/springframework/data/repository/CrudRepository.java new file mode 100644 index 000000000..603f376d4 --- /dev/null +++ b/jig-core/src/test/java/org/springframework/data/repository/CrudRepository.java @@ -0,0 +1,10 @@ +package org.springframework.data.repository; + +public interface CrudRepository extends Repository { + + S save(S entity); + + T findById(ID id); + + void deleteById(ID id); +} diff --git a/jig-core/src/test/java/org/springframework/data/repository/Repository.java b/jig-core/src/test/java/org/springframework/data/repository/Repository.java new file mode 100644 index 000000000..7070fdaec --- /dev/null +++ b/jig-core/src/test/java/org/springframework/data/repository/Repository.java @@ -0,0 +1,4 @@ +package org.springframework.data.repository; + +public interface Repository { +} diff --git a/jig-core/src/test/java/stub/infrastructure/datasource/springdata/SpringDataJdbcOrder.java b/jig-core/src/test/java/stub/infrastructure/datasource/springdata/SpringDataJdbcOrder.java new file mode 100644 index 000000000..19d853e08 --- /dev/null +++ b/jig-core/src/test/java/stub/infrastructure/datasource/springdata/SpringDataJdbcOrder.java @@ -0,0 +1,9 @@ +package stub.infrastructure.datasource.springdata; + +import org.springframework.data.relational.core.mapping.Table; + +@Table("spring_data_jdbc_orders") +public class SpringDataJdbcOrder { + + Long id; +} diff --git a/jig-core/src/test/java/stub/infrastructure/datasource/springdata/SpringDataJdbcOrderRepository.java b/jig-core/src/test/java/stub/infrastructure/datasource/springdata/SpringDataJdbcOrderRepository.java new file mode 100644 index 000000000..c2861fbd3 --- /dev/null +++ b/jig-core/src/test/java/stub/infrastructure/datasource/springdata/SpringDataJdbcOrderRepository.java @@ -0,0 +1,24 @@ +package stub.infrastructure.datasource.springdata; + +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SpringDataJdbcOrderRepository extends CrudRepository { + + @Override + SpringDataJdbcOrder save(SpringDataJdbcOrder entity); + + @Override + SpringDataJdbcOrder findById(Long id); + + @Override + void deleteById(Long id); + + @Query("update spring_data_jdbc_orders set id = :id where id = :id") + void updateById(Long id); + + @Query(" /* leading comment */\n\tupdate spring_data_jdbc_orders set id = :id where id = :id") + void updateByIdWithComment(Long id); +}