Obsługa relacji między tabelami w Hibernate i JPA - potrzebna pomoc

0

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

0

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.

0

(post usunięty - akurat te utile w sumie OK)

0

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ą...

0

@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).

1

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.

0

jaka jest przewaga takich utilsow do DAO nad @Transactional i ch*j ?

0
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.

1 użytkowników online, w tym zalogowanych: 0, gości: 1