Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Infer one-to-one and one-to-many fk by field name too #14

Merged
merged 1 commit into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,20 @@

/**
* Used to specify the name of the "foreign key" column in the current
* entity's table. This is usually optional if the name of the column matches
* the name of the parent table followed by an {@code _id} suffix.
* entity's table. This is usually optional because the anotation can infer
* the "foreign key" column name in two different ways:
*
* <p>For example, given the parent table is {@code country} and the child
* table is {@code city}. By default, the annotation will use {@code country_id}
* as the "foreign key" column of the {@code city} table.
* <p>1. The name of the column matches the name of the parent table
* followed by an {@code _id} suffix.
*
* <p>2. The name of the column matches the name of the annotated field
* followed by an {@code _id} suffix.
*
* <p>For example, given a parent table {@code country}, a child table
* {@code city}, and the annotated field {@code @OneToMany Country originCountry;}.
* By default, the "foreign key" column of the {@code city} table will be
* inferred as {@code country_id} using option (1). If that fails, it will try
* {@code origin_country_id} using option (2).
*
* @return the name of the "foreign key" column of the entity table
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,21 @@
boolean backReference() default false;

/**
* Used to specify the name of the "foreign key" column on the child table.
* This is usually optional if the name of the column matches the name of the
* parent table followed by an {@code _id} suffix.
* Used to specify the name of the "foreign key" column in the child table.
* This is usually optional because the anotation can infer the "foreign key"
* column name in two different ways:
*
* <p>1. The name of the column matches the name of the parent table
* followed by an {@code _id} suffix.
*
* <p>2. The name of the column matches the name of the annotated field
* followed by an {@code _id} suffix.
*
* <p>For example, given the parent table is {@code phone} and the child
* table is {@code phone_details}. By default, the annotation will look for
* the "foreign key" column {@code phone_id} in the {@code phone_details} table.
* <p>For example, given a parent table {@code phone}, a child table
* {@code phone_details}, and the annotated field {@code @OneToOne PhoneDetails details;}.
* By default, the "foreign key" column of the {@code phone_details} table
* will be inferred as {@code phone_id} using option (1). If that fails, it
* will try {@code details_id} using option (2).
*
* @return the name of the "foreign key" column
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,34 @@ public record ManyToOneProcessor(
public Mono<Object> populate(final ManyToOne annotation, final Field field) {
final var fieldProjection = field.getType();
final var fieldType = this.domainFor(fieldProjection);
final var foreignKey = Optional.of(annotation)
final var byTable = this.tableNameOf(fieldType).concat("_id");
final var byField = Commons.toSnakeCase(field.getName()).concat("_id");
final var foreignField = Optional.of(annotation)
.map(ManyToOne::foreignKey)
.map(Commons::toCamelCase)
.filter(not(String::isBlank))
.orElseGet(() -> this.tableNameOf(fieldType).concat("_id"));
final var foreignField = Commons.toCamelCase(foreignKey);
final var parentId = this.idColumnOf(fieldType);
final var keyValue = Optional.of(this.entity)
.map(Reflect.getter(foreignField))
.or(() -> this.inferForeignField(byTable).map(Field::getName))
.or(() -> this.inferForeignField(byField).map(Field::getName))
.orElseThrow(() -> {
final var entityType = this.domainFor(this.entity.getClass());
final var message = "Entity <%s> is missing foreign key in field: %s".formatted(
entityType.getName(),
foreignField
);

final var message = """
Unable to infer foreign key for "%s" entity. Neither "%s" nor "%s"
associated fields could be found
"""
.formatted(entityType.getSimpleName(), byTable, byField);
return RelationshipException.of(message);
});
final var parentId = this.idColumnOf(fieldType);

return this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(parentId).is(keyValue)))
.one()
.map(Commons::cast);
return Mono.just(this.entity)
.mapNotNull(Reflect.getter(foreignField))
.flatMap(fkValue ->
this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(parentId).is(fkValue)))
.one()
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,47 +38,60 @@ public Mono<Object> populate(final OneToOne annotation, final Field field) {
.isPresent();
final var mappedBy = Optional.of(annotation)
.map(OneToOne::mappedBy)
.filter(not(String::isBlank))
.orElseGet(() -> {
final var prefix = isBackReference
? this.tableNameOf(fieldType)
: this.table.getReference();

return prefix.concat("_id");
});
.filter(not(String::isBlank));

if (isBackReference) {
final var parentId = this.idColumnOf(fieldType);
final var mappedField = Commons.toCamelCase(mappedBy);
final var fkValue = Optional.of(this.entity)
.map(Reflect.getter(mappedField))
final var byTable = this.tableNameOf(fieldType).concat("_id");
final var byField = Commons.toSnakeCase(field.getName()).concat("_id");
final var mappedField = mappedBy
.map(Commons::toCamelCase)
.or(() -> this.inferForeignField(byTable).map(Field::getName))
.or(() -> this.inferForeignField(byField).map(Field::getName))
.orElseThrow(() -> {
final var entityType = this.domainFor(this.entity.getClass());
final var message = "Entity <%s> is missing foreign key in field: %s".formatted(
entityType.getName(),
mappedField
);

final var message = """
Unable to infer foreign key for "%s" entity. Neither "%s" nor "%s"
associated fields could be found
"""
.formatted(entityType.getSimpleName(), byTable, byField);
return RelationshipException.of(message);
});

return this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(parentId).is(fkValue)))
.one()
.map(Commons::cast);
return Mono.just(this.entity)
.mapNotNull(Reflect.getter(mappedField))
.flatMap(fkValue ->
this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(parentId).is(fkValue)))
.one()
);
}

return Mono.just(this.entity)
.mapNotNull(this::idValueOf)
.flatMap(entityId ->
this.template
.flatMap(entityId -> {
final var byTable = this.table.getReference().concat("_id");
final var byField = Commons.toSnakeCase(field.getName()).concat("_id");
final var mappedField = mappedBy
.or(() -> this.inferForeignField(byTable, fieldType).map(this::columnNameOf))
.or(() -> this.inferForeignField(byField, fieldType).map(this::columnNameOf))
.orElseThrow(() -> {
final var message = """
Unable to infer foreign key for "%s" entity. Neither "%s" nor "%s"
associated fields could be found
"""
.formatted(fieldType.getSimpleName(), byTable, byField);
return RelationshipException.of(message);
});

return this.template
.select(fieldType)
.as(fieldProjection)
.matching(query(where(mappedBy).is(entityId)))
.one()
);
.matching(query(where(mappedField).is(entityId)))
.one();
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,30 @@ default String columnNameOrNull(final Field field) {
.map(SqlIdentifier::getReference)
.orElse(null);
}

/**
* Tries to infer the foreign key field in the specified entity type.
*
* @param foreignKey the foreign key column name
* @param entityType the entity type to find the field from
* @return the foreign key field
*/
default Optional<Field> inferForeignField(final String foreignKey, final Class<?> entityType) {
return Maybe.of(foreignKey)
.map(Commons::toCamelCase)
.solve(entityType::getDeclaredField)
.toOptional();
}

/**
* Tries to infer the foreign key field in the current entity type.
*
* @param foreignKey the foreign key column name
* @return the foreign key field
*/
default Optional<Field> inferForeignField(final String foreignKey) {
final var entityType = this.domainFor(this.entity().getClass());

return this.inferForeignField(foreignKey, entityType);
}
}
Loading