JPA Dwukierunkowa relacja (one to many) nie widzi dodania obiektu

0

Cześć!

Mam sobie 2 encje (kod poniżej): Author i Message. Każdy autor ma wiele wiadomości. Chciałbym, aby po dodaniu wiadomości do bazy była ona widoczna z poziomu autora. Po wykonaniu kodu z klasy Main otrzymuję:

Messages
Message{id=1, author=1, content='New Message', sendingTime=Mon Nov 12 01:07:28 CET 2018}
Authors
Author(id=1, username=Username, password=Password, messages=[Message{id=1, author=1, content='New Message', sendingTime=Mon Nov 12 01:07:28 CET 2018}])
[main] INFO com.burdzi0.SimpleChat.listener.Slf4jMessageLogListener - Saved message: Message{id=2, author=1, content='Second message', sendingTime=Mon Nov 12 01:07:28 CET 2018}
Authors
Author(id=1, username=Username, password=Password, messages=[Message{id=1, author=1, content='New Message', sendingTime=Mon Nov 12 01:07:28 CET 2018}])

Dlaczego po drugim wykonaniu zapytania do bazy o autorów nie są widoczne wszystkie jego wiadomości?

Kod

@Data
@NoArgsConstructor
@Entity
@EntityListeners(Slf4jAuthorLogListener.class)
@NamedQueries({
		@NamedQuery(
				name = "Author.ByUsername",
				query = "SELECT a FROM Author a WHERE a.username LIKE :username"
		),
		@NamedQuery(
				name = "Author.All",
				query = "SELECT a FROM Author a"
		)
})
public class Author {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String username;
	private String password;

	@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	private List<Message> messages;

	public Author(String username, String password) {
		this.username = username;
		this.password = password;
	}

	public Author(String username, String password, List<Message> messages) {
		this.username = username;
		this.password = password;
		this.messages = messages;
	}
}

oraz

@Data
@NoArgsConstructor
@Entity
@EntityListeners(Slf4jMessageLogListener.class)
@NamedQueries({
		@NamedQuery(
				name = "Message.All",
				query = "SELECT m FROM Message m"
		),
		@NamedQuery(
				name = "Message.FindByContent",
				query = "SELECT m FROM Message m WHERE m.content like :content"
		)
})
public class Message {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@ManyToOne(cascade = CascadeType.ALL)
	@JoinColumn(name = "author_fk")
	private Author author;
	private String content;

	@Column(name = "sending_timestamp")
	@Temporal(TemporalType.TIMESTAMP)
	private Date sendingTime;

	public Message(Author author, String content, Date sendingTime) {
		this.author = author;
		this.content = content;
		this.sendingTime = sendingTime;
	}

	@Override
	public String toString() {
		return "Message{" +
				"id=" + id +
				", author=" + author.getId() +
				", content='" + content + '\'' +
				", sendingTime=" + sendingTime +
				'}';
	}
}

W klasie Main sobie testuję co udało mi się utworzyć:

public class Main {

	public static void main(String[] args) {
		EntityManagerFactory factory = Persistence.createEntityManagerFactory("IRC");
		EntityManager manager = factory.createEntityManager();
		EntityTransaction transaction = manager.getTransaction();

		AuthorRepository authorRepository = new InMemoryAuthorRepository(manager);
		MessageRepository messageRepository = new InMemoryMessageRepository(manager);

		Author author = new Author("Username", "Password");

		Message message = new Message(author, "New Message", new Date());

		author.setMessages(new ArrayList<>() {{add(message);}});

		authorRepository.saveAuthor(author);


		System.out.println("Messages");
		messageRepository.getAllMessages().forEach(System.out::println);

		System.out.println("Authors");
		authorRepository.getAllAuthors().forEach(System.out::println);

		Message message1 = new Message(author, "Second message", new Date());
		messageRepository.saveMessage(message1);

		System.out.println("Authors");
		authorRepository.getAllAuthors().forEach(System.out::println);

		manager.close();
		factory.close();
	}
}

Plik persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="IRC" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <class>com.burdzi0.SimpleChat.model.Message</class>
        <class>com.burdzi0.SimpleChat.model.Author</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:file:/home/Burdzi0/IdeaProjects/SimpleChat/src/DATABASE" />
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- <property name="hibernate.show_sql" value="true" /> -->
        </properties>
    </persistence-unit>
</persistence>

Dzięki!

0

Czyżby first level cache?

0

Co można z tym zrobić? Szukałem, czytałem trochę o lvlach cache'u i jedyne rozwiązania jakie przychodza mi do głowy to:

  • ręczna aktualizacja obiektu Author z użyciem adnotacji @PostPersist
  • zmiana relacji z dwukierunkowej na jednokierunkową (czego robić nie chcę)

Są lepsze rozwiązania?

1

Spróbuj zrobić przed wypisaniem

EntityManager.flush()
0

Wywołanie

manager.flush();

poza transakcją daje mi

Exception in thread "main" javax.persistence.TransactionRequiredException: no transaction is in progress

Natomiast w transakcji, gdzie zapisuję wiadomość do bazy,

	@Override
	public void saveMessage(Message message) {
		transaction.begin();
		manager.persist(message);
		manager.flush();
		transaction.commit();
	}

nic nie zmienia.
Help?

1

Spróbuj zrobić "refresh" na obiekcie - powinno pobrać świeże dane z bazy danych.

0

@Seti87: Zadziałało ;) dzięki!

1

Co do tego refresha, to nie jestem pewien czy to poprawne rozwiązanie i czy rzeczywiści jest on tutaj potrzebny? Trzeba pamiętać że taki refresh do dodatkowy strzał do bazy danych.
Pewnie lepiej by było zapisywać wiadomość kaskadowo poprzez dodanie do autora, albo w ogóle nie potzrebujemy odświeżać autora bo i tak go nigdzie nie zwracamy.
No chyba że to tylko w formie zabawy z hibernate :)

1

A nie lepiej zapisywać obie strony relacji zamiast czekać aż zrobi to za nas Hibernate? Z tym refreshem łatwo sobie wyobrazić sytuację gdy robimy kilkanaście zmian w encji i zanim transakcja zostanie skommitowana robimy refresh(). Z dokumentacji wynika, że wtedy wszystkie nie zkommitowane zmiany w encji zostaną nadpisane obecnym stanem bazy.

0

Wszystko chyba tak naprawdę zależy od tego jak z bazy będziemy korzystać. Tworzę prosty czat, więc na dobrą sprawę najważniejsze są tutaj wiadomości.
Pobieranie listy wiadomości danego usera będzie rzadszym przypadkiem, ale może się przydać (do jakiegoś wyszukiwania treści na przykład). Refresh w takim przypadku też raczej nie będzie potrzebny, bo encja Author nie będzie używana wcześniej, czyli pobrany z bazy danych obiekt będzie świeży. To był taki raczej PoC z pierwszą relacją dwukierunkową.

0

Z ciekawostek, znam projekty ( a może znałem, bo ostatnio coraz mnie spotykam tego JPA) , gdzie relacje dwukierunkowe są zabronione w kodzie Javy. Przy grubszej logice i jakichś obliczeniach zbyt łatwo popełnić błąd, a @PostPersist i inne sztuczki jeszcze tylko pogarszają.
Jak dla mnie to jest to tylko jedna z wielu pułapek JPA i najlepiej w ogóle ten framework olać, chyba, że masz mały, zgrany zespół lub projekt jednoosobowy.

0

Projekt jednoosobowy i raczej się nie rozrośnie. Dlaczego JPA jest złe? Zawsze patrzyłem na JPA jako na standard, a chyba lubimy standardy bo są wszędzie takie same? Mogę sobie podpiąć dowolnego ORMa i dowolną bazę i powinno działać (po drobnych zmianach ofc), czy zbyt naiwnie do tego podchodzę?

1

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