Cześć.
Chcę za pomocą Hibernate i JPA (adnotacji) stworzyć zależność taką, jaką w uproszczonej wersji zaprezentowałem w diagramie w załączniku. Póki co, moje nieudolne próby doprowadziły mnie jedynie do sytuacji w której żeby dodać listę np "ingredientsów" do bazy należałoby najpierw dodać ingredient, następnie dodać recipe bez ingredientsów, następnie zapisać recipe we wszystkich ingredienstach, następnie dodać listę ingredientsów do recipe. Niestety w zamyśle miało to działać tak że dodaje do bazy parę ingredientów a następnie podaję ich listę przy tworzeniu recipe. Ktoś mógłby rzucić okiem na mój kod, paczka "dao" i przeanalizować adnotacje oraz podpowiedzieć jak to poprawić aby uzyskać pożądany efekt? Poniżej link do repo. Dzięki ;)
https://github.com/JavaHussar/seekncook/tree/master/src/main/java
Przywraca twój kod wiarę w Jave :-)
Jeśli chcesz zrobić tak jak mówisz to w Ingredient zrób
@ManyToOne(optional=true)
private Recipe recipe;
Tylko będziesz miał ingredienty z nullami (do czasu aż podstawić recipe) głupie, ale takie są bazy SQL.
Ale tak moim zdaniem to ty raczej chcesz relacji @ManyToMany i wtedy problem Ci się rozwiązuje.
(post usunięty - akurat te utile w sumie OK)
Pojęcia klucz na dziś:
- Operacje kaskadowe
- Właściciel relacji
Jeżeli tworzysz relację @OneToMany
/@ManyToOne
, to właścicielem relacji jest strona Many
. Ona zarządza relacją i operacja wywołana na tej stronie może być agregowana do encji, które są z nią związane. Co to oznacza?
Zacznijmy od encji:
@Entity
public class Recipe {
@Id
private UUID id;
private String name;
@OneToMany(mappedBy = "recipe", fetch = FetchType.EAGER)
private List<Step> steps;
@OneToMany(mappedBy = "recipe", fetch = FetchType.EAGER)
private Set<Ingredient> ingredients;
public Recipe(String name) {
this();
this.name = name;
}
Recipe() {
this.id = UUID.randomUUID();
}
public List<Step> getSteps() {
return steps;
}
public Set<Ingredient> getIngredients() {
return ingredients;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Recipe recipe = (Recipe) o;
return Objects.equals(id, recipe.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "Recipe{" +
"id=" + id +
", name='" + name + '\'' +
", steps=" + steps +
", ingredients=" + ingredients +
'}';
}
}
//-----
@Entity
public class Ingredient {
@Id
private UUID id;
@Column(unique = true)
private String name;
@ManyToOne(cascade = CascadeType.PERSIST)
private Recipe recipe;
public Ingredient(String name, Recipe recipe) {
this();
this.name = name;
this.recipe = recipe;
}
Ingredient() {
this.id = UUID.randomUUID();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Ingredient that = (Ingredient) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public String getName() {
return name;
}
public Recipe getRecipe() {
return recipe;
}
@Override
public String toString() {
return "Ingredient{" +
"id=" + id +
", name='" + name + '\'' +
", recipe=" + recipe.getName() +
'}';
}
}
//----
@Entity
public class Step {
@Id
private UUID id;
private String name;
@ManyToOne(cascade = CascadeType.PERSIST)
private Recipe recipe;
public Step() {
this.id = UUID.randomUUID();
}
public Step(String name, Recipe recipe) {
this();
this.name = name;
this.recipe = recipe;
}
public String getName() {
return name;
}
public Recipe getRecipe() {
return recipe;
}
@Override
public String toString() {
return "Step{" +
"id=" + id +
", name='" + name + '\'' +
", recipe=" + recipe.getName() +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Step step = (Step) o;
return Objects.equals(id, step.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Przyjrzyj się, że Ingridient
i Step
ma ustawiony atrybut cascade
dla relacji. A dla Recipe
jest ustawiony fetch
na eager. Teraz prosty test:
@Test
public void recipeTest() throws Exception {
Recipe recipe = new Recipe("Simple Recipt");
Step s1 = new Step("s1", recipe);
Step s2 = new Step("s2", recipe);
Ingredient i1 = new Ingredient("1", recipe);
Ingredient i2 = new Ingredient("2", recipe);
Ingredient i3 = new Ingredient("3", recipe);
em.persist(i1);
em.persist(i2);
em.persist(i3);
em.persist(s1);
em.persist(s2);
em.flush(); // wymuszam zapisanie wszystkiego do bazy
System.out.println(recipe);
em.refresh(recipe); // odświeżam stan recipe
System.out.println(recipe);
}
Nigdzie nie zapisuje recipe
, a jedynie utrwalam Ingredient
i Step
. Porównaj teraz co wypisze test przed refresh
i po refresh
. To co entityManager
/session
, to zarządzanie stanem obiektów. Zatem dajmy mu robić jego robotę.
Co do utilsów... yyy nie... Co jeżeli jeden z serwisów wywoła metodę innego serwisu i w obu metodach otwieramy sesję? Jedna z sesji wycieka i nie będzie można jej zamknąć. Ot uroki manualnego zarządzania sesją...
@Koziołek: Sprostowanie: właścicielem relacji @OneToMany/@ManyToOne może być każda strona. Jak nie podasz mappedBy to jesteś właścicielem (a jest jeszcze updatable insertable .. masakra). To co podałeś to najwygodniejsze i najbardziej zgodne z intuicją SQL podejście do OnToMany w JPA.
A Fetch.EAGER to smell : https://vladmihalcea.com/2014/12/15/eager-fetching-is-a-code-smell/
Co do zarządzania sesją to polecam coś w tym stylu:
<T> T runInTransaction(Function<Session, T> dbCommand) {
final Session session = sessionFactory.openSession();
session.beginTransaction();
try {
final T result = dbCommand.apply(session);
session.getTransaction().commit();
session.close();
return result;
} catch (Exception t) {
throw new IllegalStateException(t);
}
}
// a tak korzystamy
runInTransaction( session -> session.persist(a));
JOOQ ma takie rzeczy wbudowane (tylko to nie JPA).
Tyle cudowania z transakcjami a wystarczyłoby @Transactional
:P Albo jak ktoś się lubuje w pisaniu wszystkiego ręcznie to można założyć jakieś własne AOP na to ewentualnie. Niemniej faktycznie sensowniej przynajmniej wrzucić to w jedno miejsce a nie powielać n-razy zabawy z otwieranie/zamykanie.
@vpiotr to ze w tutorialach tak jest, to jeszcze nie znaczy ze jest to dobry pomysł ;) Szczególnie że często coś co działa dla tutorialowego generic cruda na 100 linijek, niekoniecznie sprawdza się w prawdziwym życiu.
jaka jest przewaga takich utilsow do DAO nad @Transactional i ch*j ?
Shalom napisał(a):
@vpiotr to ze w tutorialach tak jest, to jeszcze nie znaczy ze jest to dobry pomysł ;) Szczególnie że często coś co działa dla tutorialowego generic cruda na 100 linijek, niekoniecznie sprawdza się w prawdziwym życiu.
Aaa, a ja myślałem że to kod edukacyjny.