Sesja i zduplikowana wartość klucza w bazie danych.

0

Cześć, mam dwa modele; User i Company

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "company")
public class Company {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToMany(mappedBy = "followedCompany")
    private Set<User> followedUsers = new HashSet<>();
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH}, fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_company_follow",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "company_id")
    )
    private Set<Company> followedCompany = new HashSet<>();

    public boolean addFollowedCompany(Company company){
        if(followedCompany.contains(company)){
            return false;
        }

        company.getFollowedUsers().add(this);
        followedCompany.add(company);
        return true;
    }
}

Kod mocno uprościłem do najważniejszych elementów, aby był bardziej czytelny.

Jak widać, stworzona też jest relacja ManyToMany.

Problem pojawia się w momencie korzystania z endpointa:

    @ResponseBody
    @RequestMapping(value = "/follow/{id}", method = RequestMethod.POST)
    public ResponseEntity<String> followCompany(@PathVariable("id") Long id) {
        User user = userSession.getUser();
        if (user == null) {
            //TODO Obsługa braku zalogowanego użytkownika
            return ResponseEntity.badRequest().body("User not logged in");
        }
        //user = userRepository.findById(user.getId()).get();

        Optional<Company> companyOptional = repository.findById(id);
        if (companyOptional.isPresent()) {
            Company company = companyOptional.get();

            boolean added = user.addFollowedCompany(company);
            if (added) {
                userRepository.save(user);
                repository.save(company);
                return ResponseEntity.ok("Company followed successfully.");
            } else {
                return ResponseEntity.badRequest().body("User already follows this company.");
            }
        } else {
            return ResponseEntity.notFound().build();
        }
    }

Na początku wyciągam użytkownika z sesji, oraz sprawdzam czy obiekt user nie jest null - Czyli czy użytkownik jest zalogowany.
Sesja:

@Data
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
    private User user;
}

Problem pojawia się, gdy dam obserwacje najpierw jednej firmie, a próbuje dać drugiej. Gdy w między czasie się wyloguje (usunę użytkownika z sesji) i zaloguje ponownie (dodam użytkownika do sesji wyciągniętego z bazy danych), to wszystko działa.
Dostaje taki błąd:

org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "user_company_follow_pkey"
Detail: Key (user_id, company_id)=(252, 1) already exists.

252 - ID użytkownika
1 - ID firmy.
Problemem jest to, że identyfikator firmy, którą właśnie zaobserwowałem jest równy 2. Aplikacja próbuje dodać ponownie firmę, którą wcześniej zaobserwowałem (z identyfikatorem = 1) i to powoduje błąd.
Aplikacja tak jakby nie jest świadoma tego, że umieściła już ten rekord wcześniej w tabeli i próbuje umieścić go ponownie.

Co ciekawe, jeśli pobiorę użytkownika ponownie z bazy danych, na podstawie ID użytkownika z sesji:
user = userRepository.findById(user.getId()).get();
to kod działa prawidłowo i nie wyrzuca wspomnianego błędu.

Moje pytanie brzmi. Czy muszę za każdym razem pobierać użytkownika z bazy danych i na nim operować, zamiast korzystać z obiektu wyciągniętego z sesji? Czy musze jakoś odświeżać obiekt w sesji, aby to działało?

0

Powinieneś chyba wyciągnąć set i dodać do niej id 2 , a nie nadpisywac

0

Dawno już nie pisałem w Hibernate ale wydaje mi się że followedCompany trzeba zastąpić getterem, zwłaszcza jak masz włączony lazy loading.
Taka rada od wujka który już z SQL niewiele ma wspólnego.

0
bbzzyyczczeek napisał(a):

Powinieneś chyba wyciągnąć set i dodać do niej id 2 , a nie nadpisywac

Nie bardzo wiem, o co chodzi 😛

0xmarcin napisał(a):

Dawno już nie pisałem w Hibernate ale wydaje mi się że followedCompany trzeba zastąpić getterem, zwłaszcza jak masz włączony lazy loading.
Taka rada od wujka który już z SQL niewiele ma wspólnego.

Jeśli chodzi Tobie o zamianę followedCompany.add(company) na getFollowedCompany().add(company) w funkcji addFollowedCompany() to nie rozwiązało to problemu

0

Zobacz jeszcze zamiast @Data
normalnie equals(), hashCode()

0
bbzzyyczczeek napisał(a):

Zobacz jeszcze zamiast @Data
normalnie equals(), hashCode()

Jestem nowy w Spring Boot i wiem za bardzo co te dwie funkcje mają wspólnego z moim problemem. Może to było istotne i powinienem o tym wspomnieć, ale w klasach User oraz Company nadpisałem te funkcje:
User:

  @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       User user = (User) o;
       return Objects.equals(id, user.id);
   }

   @Override
   public int hashCode() {
       return Objects.hash(id, login, password, email, role);
   }

Company

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Company company = (Company) o;
        return Objects.equals(id, company.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, description, logoUrl, backgroundUrl, nameUrl);
    }

Problem rozwiązuje pobierania za każdym razem użytkownika z repozytorium zamiast korzystać z obiektu zapisanego w UserSession. Czy podejście pobierania użytkownika za każdym razem z repo jest bardzo nieefektywne i nie powinno się tego stosować, czy nie ma to dużego wpływu na szybkość działania aplikacji?

0

Ref...

Problem rozwiązuje pobierania za każdym razem użytkownika z repozytorium zamiast korzystać z obiektu zapisanego w UserSession. Czy podejście pobierania użytkownika za każdym razem z repo jest bardzo nieefektywne i nie powinno się tego stosować, czy nie ma to dużego wpływu na szybkość działania aplikacji?

Mam jeszcze inne, krótkie pytanie, niezwiązane z tym tematem.
Jeśli mam tabele miast (id (long), name (String)) oraz ~1000 rekordów, to czy warto załadować całą tabelę do Listy, aby nie odwoływać się do bazy danych za każdym razem, gdy użytkownik wyszukuje dane dla lokalizacji w mieście? Tabela ta jest niemodyfikowalna, więc nie pojawią się problemy z rozbieżnością danych w tabeli i w liście zapisanej w pamięci.

0
Treektus napisał(a):

Problem rozwiązuje pobierania za każdym razem użytkownika z repozytorium zamiast korzystać z obiektu zapisanego w UserSession. Czy podejście pobierania użytkownika za każdym razem z repo jest bardzo nieefektywne i nie powinno się tego stosować, czy nie ma to dużego wpływu na szybkość działania aplikacji?

Jak strzelasz z repozytorium, to ponieważ to wszystko i tak jest opakowane Hibernetem, to i tak on sobie sam ogarnie czy może użyć cache (1 poziomu automatycznie, możesz samemu skonfigurować też cache 2 poziomu, np ehcache).

Mam jeszcze inne, krótkie pytanie, niezwiązane z tym tematem.
Jeśli mam tabele miast (id (long), name (String)) oraz ~1000 rekordów, to czy warto załadować całą tabelę do Listy, aby nie odwoływać się do bazy danych za każdym razem, gdy użytkownik wyszukuje dane dla lokalizacji w mieście? Tabela ta jest niemodyfikowalna, więc nie pojawią się problemy z rozbieżnością danych w tabeli i w liście zapisanej w pamięci.

Jak ~~1000 rekordów i niemodyfikowalne, to na luzie możesz trzymać sobie w pamięci aplikacji jako swój cache.

1

Trzymanie obiektów JPA w stanie detached i próba używania ich w innej sesji JPA to proszenie się o problemy. Przeładowywuj obiekty przy każdym requeście - EntityManager.refresh, albo repository.findById. Jak się boisz o wydajność, spróbuj dodać second-level cache, ale nadal przeladowywuj w kodzie.

Nie należy też mylić sesji JPA i sesji HTTP.
Nie widzę, gdzie masz granice transakcji w twoim kodzie. Mam nadzieję, że nie mieszasz koncepcyjnie sesji http („@Scope(value = "session"”) z transakcjami bazodanowymi.

—-

Jeśli mam tabele miast (id (long), name (String)) oraz ~1000 rekordów, to czy warto załadować całą tabelę do Listy, aby nie odwoływać się do bazy danych za każdym razem, gdy użytkownik wyszukuje dane dla lokalizacji w mieście? Tabela ta jest niemodyfikowalna, więc nie pojawią się problemy z rozbieżnością danych w tabeli i w liście zapisanej w pamięci.

Jeszcze jak.

Ale też, to zależy, co rozumiesz pod „wyszukiwaniem danych dla lokalizacji w mieście”.

—-

Przy okazji, narzekania starego dziada ;)

Jestem nowy w Spring Boot

Nic w tym kodzie, nie wskazuje na Spring-Boota.

Czy to twoje pierwsze kroki z bazami danych w Javie? Jak mnie to wnerwia, że nowi ludzie zaczynają od spring-data-jpa+Spring-Boot, a przecież po drodze jest kilka frameworków i koncepcji, które trzeba najpierw zrozumieć, aby dobrze używać tych zaawansowanych narzędzi.

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