Skip to content

Commit b405798

Browse files
committed
Implemented support for enum foreign keys.
1 parent ff435b0 commit b405798

File tree

6 files changed

+136
-25
lines changed

6 files changed

+136
-25
lines changed

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Include the Maven artifact:
2020
<dependency>
2121
<groupId>com.github.collinalpert</groupId>
2222
<artifactId>java2db</artifactId>
23-
<version>4.0</version>
23+
<version>4.1</version>
2424
</dependency>
2525
```
2626
Or include the [JAR](https://github.com/CollinAlpert/Java2DB/releases/latest) in your project.
@@ -156,6 +156,50 @@ The asynchronous versions of methods which have a return value, e.g. `create`, `
156156
If you do not wish to use the computed value, e.g. for the `create` method, the `CallbackUtils` class offers an `empty()` method, which returns an empty `Consumer` that just does nothing.
157157
Use this as an argument in the asynchronous methods, when needed.
158158

159+
### Enums for static values
160+
Lets suppose you are using a "mood" in which you have certain moods (happy, sad, mad, etc.) stored. Now, to describe the mood
161+
of a person you would obviously reference this table via a foreign key and add the `@ForeignKeyEntity` attribute to a POJO field you have defined for the "mood" table.
162+
For static tables, meaning tables which just contain informational values e.g. statuses which do not change, you can define an enum to keep track of the values.
163+
That way a complete entity is not always needed. Using the above example, it would look something like this:
164+
165+
```java
166+
// An enum representing used in combination with the @ForeignKeyEntity must implement IdentifiableEnum.
167+
public enum MoodTypes implements IdentifiableEnum {
168+
169+
HAPPY(1), SAD(2), MAD(3);
170+
171+
private final int id;
172+
173+
MoodTypes(int id) {
174+
this.id = id;
175+
}
176+
177+
@Override
178+
public long getId() {
179+
return this.id;
180+
}
181+
}
182+
```
183+
184+
```java
185+
@TableName("person")
186+
public class Person extends BaseEntity {
187+
188+
// All of the above fields.
189+
190+
private int moodId;
191+
192+
// You can still use the foreign key entity if you like,
193+
// but it becomes sort of redundant as soon as you use the enum.
194+
@ForeignKeyEntity("moodId")
195+
private Mood mood;
196+
197+
// The enum value will be set to the value corresponding with the "moodId".
198+
@ForeignKeyEntity("moodId")
199+
private MoodTypes moodType;
200+
}
201+
```
202+
159203
### Column name deviations
160204
To be able to target any column naming conventions, it is possible to explicitly tell Java2DB which table column a POJO field targets with the `@ColumnName` attribute. Simply apply the attribute to a field.
161205
Note that when supplying a different name for a foreign key, the `@ForeignKeyEntity` annotation must still point to the name of the actual table's foreign key column. Here's an example in which the gender foreign key is not suffixed with "id" on the database:

pom.xml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.github.collinalpert</groupId>
88
<artifactId>java2db</artifactId>
9-
<version>4.0</version>
9+
<version>4.1</version>
1010
<packaging>jar</packaging>
1111

1212
<name>Java2DB</name>
@@ -73,7 +73,7 @@
7373
<dependency>
7474
<groupId>org.junit.jupiter</groupId>
7575
<artifactId>junit-jupiter-api</artifactId>
76-
<version>5.4.0</version>
76+
<version>5.4.1</version>
7777
<scope>test</scope>
7878
</dependency>
7979
</dependencies>
@@ -136,8 +136,9 @@
136136
</executions>
137137
</plugin>
138138
<plugin>
139+
<groupId>org.apache.maven.plugins</groupId>
139140
<artifactId>maven-assembly-plugin</artifactId>
140-
<version>3.1.0</version>
141+
<version>3.1.1</version>
141142
<executions>
142143
<execution>
143144
<phase>package</phase>

src/main/java/com/github/collinalpert/java2db/annotations/ForeignKeyEntity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
* This property does not have to exist on the database.
1111
* Its parameter is the name of the foreign key column.
1212
*
13+
* This annotation can be used on two types of objects:
14+
* 1) Entities which represent real objects of foreign key tables. In this case the entity has to extend
15+
* {@link com.github.collinalpert.java2db.entities.BaseEntity}. Because this entity represents a table it should extend {@code BaseEntity} anyway.
16+
*
17+
* 2) Foreign keys to a table with static values that can be represented by an enum because they don't change.
18+
* In that case the enum must extend {@link com.github.collinalpert.java2db.contracts.IdentifiableEnum} and map the ids from the table.
19+
*
1320
* @author Collin Alpert
1421
*/
1522
@Target(ElementType.FIELD)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.github.collinalpert.java2db.contracts;
2+
3+
/**
4+
* A contract for enums which represent foreign keys.
5+
* Every enum used to represent a foreign key must implement this interface.
6+
*
7+
* @author Collin Alpert
8+
*/
9+
public interface IdentifiableEnum {
10+
long getId();
11+
}

src/main/java/com/github/collinalpert/java2db/mappers/BaseMapper.java

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
import com.github.collinalpert.java2db.annotations.ColumnName;
44
import com.github.collinalpert.java2db.annotations.ForeignKeyEntity;
5+
import com.github.collinalpert.java2db.contracts.IdentifiableEnum;
56
import com.github.collinalpert.java2db.entities.BaseEntity;
67
import com.github.collinalpert.java2db.modules.ArrayModule;
78
import com.github.collinalpert.java2db.utilities.IoC;
89
import com.github.collinalpert.java2db.utilities.UniqueIdentifier;
910
import com.github.collinalpert.java2db.utilities.Utilities;
1011

12+
import java.lang.reflect.Field;
1113
import java.sql.ResultSet;
1214
import java.sql.SQLException;
1315
import java.time.LocalDate;
1416
import java.time.LocalDateTime;
1517
import java.time.LocalTime;
18+
import java.util.Arrays;
1619
import java.util.Calendar;
1720
import java.util.LinkedList;
1821
import java.util.List;
@@ -22,6 +25,7 @@
2225
import java.util.stream.Stream;
2326

2427
import static com.github.collinalpert.java2db.utilities.Utilities.tryAction;
28+
import static com.github.collinalpert.java2db.utilities.Utilities.tryGetValue;
2529

2630
/**
2731
* Default mapper for converting a {@link ResultSet} to the respective Java entity.
@@ -138,32 +142,31 @@ private <E extends BaseEntity> void setFields(ResultSet set, E entity, String id
138142
var fields = Utilities.getEntityFields(entity.getClass(), true);
139143
for (var field : fields) {
140144
field.setAccessible(true);
145+
141146
if (field.getAnnotation(ForeignKeyEntity.class) != null) {
142-
var foreignKeyColumnName = field.getAnnotation(ForeignKeyEntity.class).value();
143-
if (!BaseEntity.class.isAssignableFrom(field.getType())) {
144-
throw new IllegalArgumentException(String.format("Type %s, which is annotated as a foreign key, does not extend BaseEntity.", field.getType().getSimpleName()));
145-
}
146147

147-
// This block is for checking if the foreign key is null.
148-
// That means that the corresponding foreign key entity must be set to null.
149-
var foreignKeyName = "";
150-
try {
151-
var foreignKeyField = field.getDeclaringClass().getDeclaredField(foreignKeyColumnName);
152-
foreignKeyName = foreignKeyField.getName();
153-
if (foreignKeyField.getAnnotation(ColumnName.class) != null) {
154-
foreignKeyName = foreignKeyField.getAnnotation(ColumnName.class).value();
155-
}
156-
} catch (NoSuchFieldException e) {
157-
//Oh boi, you've done it now!
158-
for (var declaredField : field.getDeclaringClass().getDeclaredFields()) {
159-
if (declaredField.getAnnotation(ColumnName.class) != null && declaredField.getAnnotation(ColumnName.class).value().equals(foreignKeyColumnName)) {
160-
foreignKeyName = declaredField.getAnnotation(ColumnName.class).value();
161-
break;
162-
}
148+
if (field.getType().isEnum()) {
149+
if (!IdentifiableEnum.class.isAssignableFrom(field.getType())) {
150+
throw new IllegalArgumentException(String.format("The enum %s used in %s was annotated with a ForeignKeyEntity attribute but does not extend IdentifiableEnum.", field.getType().getSimpleName(), field.getDeclaringClass().getSimpleName()));
163151
}
152+
153+
var foreignKeyName = getForeignKeyName(field);
154+
var foundEnum = Arrays.stream(field.getType().getEnumConstants())
155+
.map(x -> (IdentifiableEnum) x)
156+
.filter(x -> Long.valueOf(tryGetValue(() -> getAccessibleField(field.getDeclaringClass(), foreignKeyName).get(entity)).toString()) == x.getId())
157+
.findFirst();
158+
159+
foundEnum.ifPresent(identifiableEnum -> tryAction(() -> field.set(entity, field.getType().cast(identifiableEnum))));
160+
161+
continue;
164162
}
165163

166-
if (set.getObject((identifier == null ? Utilities.getTableName(entity.getClass()) : identifier) + "_" + foreignKeyName) == null) {
164+
if (!BaseEntity.class.isAssignableFrom(field.getType())) {
165+
throw new IllegalArgumentException(String.format("Type %s, which is annotated as a foreign key, does not extend BaseEntity.", field.getType().getSimpleName()));
166+
}
167+
168+
// If foreign key is null, the corresponding entity must also be null.
169+
if (set.getObject((identifier == null ? Utilities.getTableName(entity.getClass()) : identifier) + "_" + getForeignKeyName(field)) == null) {
167170
continue;
168171
}
169172

@@ -196,4 +199,29 @@ private <E extends BaseEntity> void setFields(ResultSet set, E entity, String id
196199
tryAction(() -> field.set(entity, value));
197200
}
198201
}
202+
203+
private String getForeignKeyName(Field field) {
204+
var foreignKeyColumnName = field.getAnnotation(ForeignKeyEntity.class).value();
205+
206+
try {
207+
var foreignKeyField = field.getDeclaringClass().getDeclaredField(foreignKeyColumnName);
208+
return Utilities.getColumnName(foreignKeyField);
209+
} catch (NoSuchFieldException e) {
210+
//Oh boi, you've done it now! This case occurs when a foreign key field gets altered by a ColumnName attribute.
211+
for (var declaredField : field.getDeclaringClass().getDeclaredFields()) {
212+
ColumnName columnName;
213+
if ((columnName = declaredField.getAnnotation(ColumnName.class)) != null && columnName.value().equals(foreignKeyColumnName)) {
214+
return columnName.value();
215+
}
216+
}
217+
}
218+
219+
return "";
220+
}
221+
222+
private Field getAccessibleField(Class<?> clazz, String name) throws NoSuchFieldException {
223+
var field = clazz.getDeclaredField(name);
224+
field.setAccessible(true);
225+
return field;
226+
}
199227
}

src/main/java/com/github/collinalpert/java2db/utilities/Utilities.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public static List<TableColumnReference> getAllFields(Class<? extends BaseEntity
7676
public static List<TableColumnReference> getAllFields(Class<? extends BaseEntity> instanceClass, String alias) {
7777
var fields = new LinkedList<TableColumnReference>();
7878
for (var field : getEntityFields(instanceClass, true)) {
79+
if (field.getType().isEnum()) {
80+
continue;
81+
}
82+
7983
if (field.getAnnotation(ForeignKeyEntity.class) != null) {
8084
var tempAlias = UniqueIdentifier.generate(getTableName(field.getType()).substring(0, 1), field.getName());
8185
fields.add(new TableColumnReference(getTableName(instanceClass), field, tempAlias, alias));
@@ -197,4 +201,20 @@ public static <E extends Throwable> void tryAction(ThrowableRunnable<E> runnable
197201
e.printStackTrace();
198202
}
199203
}
204+
205+
/**
206+
* Tries to perform a certain supplier and retrieve its value while considering a checked exception that could occur.
207+
*
208+
* @param supplier The {@code Supplier} to try to execute.
209+
* @param <T> The type of value to return.
210+
* @param <E> The type of checked exception.
211+
* @return The value returned by the supplier, assuming it can be excecuted without throwing an exception.
212+
*/
213+
public static <T, E extends Throwable> T tryGetValue(ThrowableSupplier<T, E> supplier) {
214+
try {
215+
return supplier.fetch();
216+
} catch (Throwable e) {
217+
throw new RuntimeException("Underlying method threw an exception.", e);
218+
}
219+
}
200220
}

0 commit comments

Comments
 (0)