refactor: revised class layout for JPA entities

This commit is contained in:
Zhongheng Liu 2025-12-22 02:58:50 +02:00
commit 88a85f2e04
6 changed files with 184 additions and 97 deletions

View file

@ -0,0 +1,53 @@
package commons;
import jakarta.persistence.Entity;
import java.util.Optional;
/**
* A formal ingredient inheriting from base {@link RecipeIngredient RecipeIngredient},
* holds an amount and a unit suffix in {@link String String} form.
* @see RecipeIngredient
*/
@Entity
public class FormalIngredient extends RecipeIngredient {
private double amount;
private String unitSuffix;
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
public String getUnitSuffix() {
return unitSuffix;
}
public void setUnitSuffix(String unitSuffix) {
this.unitSuffix = unitSuffix;
}
public FormalIngredient(Ingredient ingredient, double amount, String unitSuffix) {
super(ingredient);
this.amount = amount;
this.unitSuffix = unitSuffix;
}
public FormalIngredient() {
// ORM
}
public double amountInBaseUnit() {
Optional<Unit> unit = Unit.fromString(unitSuffix);
if (unit.isEmpty() || !unit.get().isFormal() || unit.get().conversionFactor <= 0) {
return 0.0;
}
return amount * unit.get().conversionFactor;
}
public String toString() {
return amount + unitSuffix + " of " + ingredient.name;
}
}

View file

@ -20,7 +20,8 @@ public class Ingredient {
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
public long id; public long id;
@Column(name = "name", nullable = false, unique = true) // FIXME Dec 22 2025::temporarily made this not a unique constraint because of weird JPA behaviour
@Column(name = "name", nullable = false, unique = false)
public String name; public String name;
@ -35,10 +36,24 @@ public class Ingredient {
public Ingredient() {} public Ingredient() {}
public Ingredient(String name, public Ingredient(
Long id,
String name,
double proteinPer100g, double proteinPer100g,
double fatPer100g, double fatPer100g,
double carbsPer100g) { double carbsPer100g) {
this.id = id;
this.name = name;
this.proteinPer100g = proteinPer100g;
this.fatPer100g = fatPer100g;
this.carbsPer100g = carbsPer100g;
}
public Ingredient(
String name,
double proteinPer100g,
double fatPer100g,
double carbsPer100g) {
this.name = name; this.name = name;
this.proteinPer100g = proteinPer100g; this.proteinPer100g = proteinPer100g;
this.fatPer100g = fatPer100g; this.fatPer100g = fatPer100g;

View file

@ -15,16 +15,17 @@
*/ */
package commons; package commons;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Entity;
import jakarta.persistence.CollectionTable; import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OrderColumn;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.ElementCollection; import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderColumn;
import jakarta.persistence.Table;
import java.util.ArrayList; import java.util.ArrayList;
@ -59,11 +60,11 @@ public class Recipe {
// | 1 (Steak) | 40g pepper | // | 1 (Steak) | 40g pepper |
// | 1 (Steak) | Meat | // | 1 (Steak) | Meat |
// |----------------------------------| // |----------------------------------|
@ElementCollection @OneToMany
@CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id")) @CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id"))
@Column(name = "ingredient") @Column(name = "ingredient")
// TODO: Replace String with Embeddable Ingredient Class // TODO: Replace String with Embeddable Ingredient Class
private List<String> ingredients = new ArrayList<>(); private List<RecipeIngredient> ingredients = new ArrayList<>();
// Creates another table named recipe_preparation which stores: // Creates another table named recipe_preparation which stores:
// recipe_preparation(recipe_id -> recipes(id), preparation_step, step_order). // recipe_preparation(recipe_id -> recipes(id), preparation_step, step_order).
@ -94,7 +95,7 @@ public class Recipe {
} }
// TODO: Replace String with Embeddable Ingredient Class for ingredients // TODO: Replace String with Embeddable Ingredient Class for ingredients
public Recipe(Long id, String name, List<String> ingredients, List<String> preparationSteps) { public Recipe(Long id, String name, List<RecipeIngredient> ingredients, List<String> preparationSteps) {
// Not used by JPA/Spring // Not used by JPA/Spring
this.id = id; this.id = id;
this.name = name; this.name = name;
@ -119,14 +120,14 @@ public class Recipe {
} }
// TODO: Replace String with Embeddable Ingredient Class // TODO: Replace String with Embeddable Ingredient Class
public List<String> getIngredients() { public List<RecipeIngredient> getIngredients() {
// Disallow modifying the returned list. // Disallow modifying the returned list.
// You can still copy it with List.copyOf(...) // You can still copy it with List.copyOf(...)
return Collections.unmodifiableList(ingredients); return Collections.unmodifiableList(ingredients);
} }
// TODO: Replace String with Embeddable Ingredient Class // TODO: Replace String with Embeddable Ingredient Class
public void setIngredients(List<String> ingredients) { public void setIngredients(List<RecipeIngredient> ingredients) {
this.ingredients = ingredients; this.ingredients = ingredients;
} }

View file

@ -1,77 +1,60 @@
package commons; package commons;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
/**
* The base RecipeIngredient class, holding a reference to the
* {@link Ingredient Ingredient} it has an (in)formal amount
* linked to.
* It uses {@link JsonSubTypes JsonSubTypes} such that the
* Jackson framework can conveniently decode JSON data sent
* by client, which could be either a formal or vague
* ingredient.
* @see Ingredient
*/
@Entity @Entity
public class RecipeIngredient { @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, // identifies subtype by logical name
include = JsonTypeInfo.As.PROPERTY,
property = "type" // JSON property carrying the subtype id
)
@JsonSubTypes({
@JsonSubTypes.Type(value = FormalIngredient.class, name = "formal"),
@JsonSubTypes.Type(value = VagueIngredient.class, name = "vague")
})
public abstract class RecipeIngredient {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.AUTO)
public Long id; public Long id;
// which recipe is used /**
@ManyToOne(optional = false) * Many-to-one: Many {@link RecipeIngredient RecipeIngredient}
@JoinColumn(name = "recipe_id") * can use the same {@link Ingredient Ingredient} with varying
public Recipe recipe; * amounts. This allows better ingredient collecting for
* nutrition view.
//which ingredient is used */
@ManyToOne(optional = false) @ManyToOne(optional = false)
@JoinColumn(name = "ingredient_id") @JoinColumn(name = "ingredient_id")
public Ingredient ingredient; public Ingredient ingredient;
public double amount;
// store the unit name in the database
public String unitName;
@SuppressWarnings("unused") @SuppressWarnings("unused")
protected RecipeIngredient() { protected RecipeIngredient() {
// for sebastian // for ORM
} }
public RecipeIngredient(Recipe recipe, //which recipe public RecipeIngredient(
Ingredient ingredient, // which ingredient Ingredient ingredient) {
double amount, // the amount //store it in the field
String unit) { //gram liter etc
//store it im tha field
this.recipe = recipe;
this.ingredient = ingredient; this.ingredient = ingredient;
this.amount = amount;
this.unitName = unit;
}
// Convert unitName to Unit object so java can read it
public Unit getUnit() {
return switch (unitName) {
case "GRAM" -> Unit.GRAM;
case "KILOGRAM" -> Unit.KILOGRAM;
case "MILLILITER" -> Unit.MILLILITER;
case "LITER" -> Unit.LITER;
case "TABLESPOON" -> Unit.TABLESPOON;
case "TEASPOON" -> Unit.TEASPOON;
case "CUP" -> Unit.CUP;
case "PIECE" -> Unit.PIECE;
case "PINCH" -> Unit.PINCH;
case "HANDFUL" -> Unit.HANDFUL;
case "TO_TASTE" -> Unit.TO_TASTE;
default -> null;
};
}
public double amountInBaseUnit() {
Unit unit = getUnit();
if (unit == null || !unit.isFormal() || unit.conversionFactor <= 0) {
return 0.0;
}
return amount * unit.conversionFactor;
} }
} }

View file

@ -1,33 +1,30 @@
package commons; package commons;
//what is a record class and why is it recommended import java.util.Optional;
/**
* A unit enum that holds some values.
* A {@link Unit Unit} enum holds primarily formal mass-units
* Except for an informal unit that always has a factor of zero
* is made for user input purposes.
* @see VagueIngredient
* @see FormalIngredient
*/
public enum Unit { public enum Unit {
//formal units // Mass units with their absolute scale
//weight units GRAMME("g", true, 1.0),
GRAM("GRAM", true, 1.0), KILOGRAMME("kg", true, 1_000.0 ),
KILOGRAM("KILOGRAM", true, 1000.0 ), TONNE("t", true, 1_000_000.0),
//volume units // A special informal unit
MILLILITER("MILLILITER",true, 1.0), INFORMAL("<NONE>", false, 0.0);
LITER("LITER", true, 1000.0),
TABLESPOON("TABLESPOON",true, 15.0),
TEASPOON("TEASPOON", true, 5),
CUP ("CUP", true, 240.0),
//piece should be a formal unit to converse for portions like 3eggs can become 1,5 eggs this way public final String suffix;
PIECE("PIECE", true, 1.0),
//informal units
PINCH("PINCH", false, 0.0),
HANDFUL("HANDFUL", false, 0.0),
TO_TASTE("TO_TASTE", false, 0.0);
public final String name;
public final boolean formal; public final boolean formal;
public final double conversionFactor; public final double conversionFactor;
Unit(String name, boolean formal, double conversionFactor) { Unit(String suffix, boolean formal, double conversionFactor) {
this.name = name; this.suffix = suffix;
this.formal = formal; this.formal = formal;
this.conversionFactor = conversionFactor; this.conversionFactor = conversionFactor;
} }
@ -36,12 +33,16 @@ public enum Unit {
return formal; return formal;
} }
public boolean isInformal() {
return !formal;
} //for oskar
@Override @Override
public String toString() { public String toString() {
return name; return suffix;
}
public static Optional<Unit> fromString(String suffix) {
for (Unit unit : Unit.values()) {
if (unit.suffix != null && unit.suffix.equals(suffix)) {
return Optional.of(unit);
}
}
return Optional.empty();
} }
} }

View file

@ -0,0 +1,34 @@
package commons;
import jakarta.persistence.Entity;
/**
* A vague ingredient inheriting from {@link RecipeIngredient RecipeIngredient}.
* It has a {@link String String} description that describes an informal quantity.
* The vague ingredient renders as its descriptor and its ingredient name
* conjugated by a space via {@link VagueIngredient#toString() toString()} method.
* @see RecipeIngredient
*/
@Entity
public class VagueIngredient extends RecipeIngredient {
private String description;
public VagueIngredient() {}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public VagueIngredient(Ingredient ingredient, String description) {
super(ingredient);
this.description = description;
}
@Override
public String toString() {
return description + " " + ingredient.name;
}
}