Skip to content

Commit

Permalink
Enable mapping of arbitrary SQL results to aggregate entities
Browse files Browse the repository at this point in the history
  • Loading branch information
nakamura-to committed Jan 26, 2025
1 parent b5cee08 commit e621d3e
Show file tree
Hide file tree
Showing 47 changed files with 1,927 additions and 139 deletions.
1 change: 1 addition & 0 deletions doma-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
exports org.seasar.doma.internal.expr.node;
exports org.seasar.doma.internal.jdbc.sql.node;
exports org.seasar.doma.internal.jdbc.util;
exports org.seasar.doma.jdbc.aggregate;

// Requires
requires transitive java.sql;
Expand Down
30 changes: 30 additions & 0 deletions doma-core/src/main/java/org/seasar/doma/Association.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Doma Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.seasar.doma;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates an association between entities.
*
* <p>This annotation is applied to fields that represent a relationship between entities.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Association {}
46 changes: 46 additions & 0 deletions doma-core/src/main/java/org/seasar/doma/AssociationLinker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright Doma Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.seasar.doma;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.function.BiFunction;

/**
* Associates a field in an entity class with properties in a related object for the purpose of
* creating object relationships when mapping between database tables and entities.
*
* <p>This class can only be annotated on {@code public static final} fields of type {@link
* BiFunction}.
*/
@Target(java.lang.annotation.ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AssociationLinker {
/**
* Specifies the path to the property in the related object.
*
* @return the property path as a string
*/
String propertyPath();

/**
* Defines the prefix for the column in the database table that is linked to the field.
*
* @return the column prefix as a string
*/
String columnPrefix();
}
11 changes: 11 additions & 0 deletions doma-core/src/main/java/org/seasar/doma/Select.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.Statement;
import java.util.function.BiFunction;
import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.JdbcException;
import org.seasar.doma.jdbc.NoResultException;
Expand Down Expand Up @@ -163,4 +164,14 @@
* @return the output format of SQL logs.
*/
SqlLogType sqlLog() default SqlLogType.FORMATTED;

/**
* Specifies a helper class used for aggregation operations.
*
* <p>The class specified here must contain at least one field of type {@link BiFunction}
* annotated with {@link AssociationLinker}.
*
* @return the class representing the aggregation helper, or {@code Void.class} if not specified
*/
Class<?> aggregateHelper() default Void.class;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@
package org.seasar.doma.jdbc;

import java.lang.reflect.Method;
import java.util.List;
import java.util.function.BiFunction;
import org.seasar.doma.jdbc.aggregate.AggregateCommand;
import org.seasar.doma.jdbc.aggregate.AssociationLinkerType;
import org.seasar.doma.jdbc.aggregate.StreamReducer;
import org.seasar.doma.jdbc.command.*;
import org.seasar.doma.jdbc.entity.EntityType;
import org.seasar.doma.jdbc.query.*;

/** A factory for the {@link Command} implementation classes. */
Expand All @@ -37,6 +42,15 @@ default <RESULT> SelectCommand<RESULT> createSelectCommand(
return new SelectCommand<>(query, resultSetHandler);
}

default <RESULT, ENTITY> AggregateCommand<RESULT, ENTITY> createAggregateCommand(
Method method,
SelectQuery query,
EntityType<ENTITY> entityType,
StreamReducer<RESULT, ENTITY> resultReducer,
List<AssociationLinkerType<?, ?>> associationLinkerTypes) {
return new AggregateCommand<>(query, entityType, resultReducer, associationLinkerTypes);
}

/**
* Creates a {@link DeleteCommand} object.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright Doma Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.seasar.doma.jdbc.aggregate;

import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import org.seasar.doma.internal.util.Combinations;
import org.seasar.doma.internal.util.Pair;
import org.seasar.doma.jdbc.command.Command;
import org.seasar.doma.jdbc.command.SelectCommand;
import org.seasar.doma.jdbc.entity.EntityType;
import org.seasar.doma.jdbc.query.Query;
import org.seasar.doma.jdbc.query.SelectQuery;

public class AggregateCommand<RESULT, ENTITY> implements Command<RESULT> {
private final SelectQuery query;
private final EntityType<ENTITY> entityType;
private final StreamReducer<RESULT, ENTITY> streamReducer;
private final List<AssociationLinkerType<?, ?>> associationLinkerTypes;

public AggregateCommand(
SelectQuery query,
EntityType<ENTITY> entityType,
StreamReducer<RESULT, ENTITY> streamReducer,
List<AssociationLinkerType<?, ?>> associationLinkerTypes) {
this.query = Objects.requireNonNull(query);
this.entityType = Objects.requireNonNull(entityType);
this.streamReducer = Objects.requireNonNull(streamReducer);
Objects.requireNonNull(associationLinkerTypes);
this.associationLinkerTypes = sortAssociationLinkerTypes(associationLinkerTypes);
}

private static List<AssociationLinkerType<?, ?>> sortAssociationLinkerTypes(
List<AssociationLinkerType<?, ?>> associationLinkerTypes) {
Comparator<AssociationLinkerType<?, ?>> reversedComparator =
Comparator.<AssociationLinkerType<?, ?>>comparingInt(AssociationLinkerType::getDepth)
.reversed();
return associationLinkerTypes.stream().sorted(reversedComparator).toList();
}

@Override
@SuppressWarnings("unchecked")
public RESULT execute() {
Map<LinkableEntityKey, Object> cache = new LinkedHashMap<>();
Combinations<LinkableEntityKey> combinations = new Combinations<>();
SelectCommand<List<LinkableEntityPool>> command =
new SelectCommand<>(
query,
new LinkableEntityPoolIterationHandler(
entityType, associationLinkerTypes, query.isResultMappingEnsured()));
List<LinkableEntityPool> entityPools = command.execute();
for (LinkableEntityPool entityPool : entityPools) {
Map<String, KeyAndEntity> associationCandidate = new LinkedHashMap<>();
for (Map.Entry<LinkableEntityKey, LinkableEntityData> e : entityPool.entrySet()) {
LinkableEntityKey key = e.getKey();
LinkableEntityData data = e.getValue();
Object entity =
cache.computeIfAbsent(
key,
k -> {
EntityType<Object> entityType = (EntityType<Object>) k.entityType();
Object newEntity = entityType.newEntity(data.getStates());
if (!entityType.isImmutable()) {
entityType.saveCurrentStates(newEntity);
}
return newEntity;
});
associationCandidate.put(key.propertyPath(), new KeyAndEntity(key, entity));
}
associate(cache, combinations, associationCandidate);
}
Stream<ENTITY> stream =
(Stream<ENTITY>)
cache.entrySet().stream()
.filter(e -> e.getKey().belongsToRootEntity())
.map(Map.Entry::getValue);
return streamReducer.reduce(stream);
}

private void associate(
Map<LinkableEntityKey, Object> cache,
Combinations<LinkableEntityKey> combinations,
Map<String, KeyAndEntity> associationCandidate) {
for (AssociationLinkerType<?, ?> associationLinkerType : associationLinkerTypes) {
KeyAndEntity source = associationCandidate.get(associationLinkerType.getSourceName());
KeyAndEntity target = associationCandidate.get(associationLinkerType.getTargetName());
if (source == null || target == null) {
continue;
}
Pair<LinkableEntityKey, LinkableEntityKey> keyPair = new Pair<>(source.key(), target.key());
if (combinations.contains(keyPair)) {
continue;
}
@SuppressWarnings("unchecked")
BiFunction<Object, Object, Object> linker =
(BiFunction<Object, Object, Object>) associationLinkerType.getLinker();
Object newEntity = linker.apply(source.entity(), target.entity());
if (newEntity != null) {
cache.replace(source.key(), newEntity);
associationCandidate.replace(
associationLinkerType.getSourceName(), new KeyAndEntity(source.key(), newEntity));
}
combinations.add(keyPair);
}
}

@Override
public Query getQuery() {
return query;
}

private record KeyAndEntity(LinkableEntityKey key, Object entity) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Doma Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.seasar.doma.jdbc.aggregate;

import java.util.Objects;
import org.seasar.doma.jdbc.entity.EntityType;

record AssociationIdentifier(String propertyPath, EntityType<?> entityType) {
AssociationIdentifier {
Objects.requireNonNull(propertyPath);
Objects.requireNonNull(entityType);
}
}
Loading

0 comments on commit e621d3e

Please sign in to comment.