Skip to content

Commit

Permalink
feat(core): Infer one-to-one and one-to-many fk by field name too
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseLion committed Feb 26, 2024
1 parent 2a42bda commit 041c737
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 55 deletions.
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);
}
}

0 comments on commit 041c737

Please sign in to comment.