Hibernate, Relacje pomiędzy encjami

0

Cześć. Jestem po kursie Hibernate w którym omówione zostały relacje pomiędzy encjami. Wykładowca omawiając relację, powiedzmy OneToOne BI-Directional robił to tak, że w miejsce klucza obcego wrzucał cały obiekt innej klasy. Dopóki pracowaliśmy w czystym Hibernacie i wywoływaliśmy wszystko w main(), to wszystko grało, jednak przenosząc te zasady na REST Api pojawiają się pewne problemy.

Jako, że obiekt ActivationCode posiada w sobie obiekt User, a User posiada w sobie obiekt ActivationCode, próbując wyciągnąć z bazy dane dochodzi do nieskończonej rekurencji i Stackoverflow Error.

title

title

ActivationCode.class

@Entity
@Table(name="activation_code")
public class ActivationCode {
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="id_activation_code")
	private int id_activation_code;
	
	@Column(name="serial_number")
	private String serial_number;
	
	@OneToOne(mappedBy="id_activation_code", cascade= {CascadeType.DETACH,
			 										   CascadeType.MERGE,
			 										   CascadeType.PERSIST,
			 										   CascadeType.REFRESH })
	private User user;
	
	
	
	public ActivationCode() {
		
	}
}

User.class

@Entity
@Table(name="user")
public class User {
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="id_user")
	private int id_user;
	
	@OneToOne(cascade={CascadeType.DETACH,
						CascadeType.MERGE,
						CascadeType.PERSIST,
						CascadeType.REFRESH})
	@JoinColumn(name="id_activation_code")
	private ActivationCode id_activation_code;

	@Column(name="user_name")
	private String user_name;
	
	@Column(name="user_surname")
	private String user_surname;
	
	@Column(name="user_email")
	private String user_email;
	
	@Column(name="user_password")
	private String user_password;
	
	@Column(name="user_phone_number")
	private String user_phone_number;
	
	@Column(name="user_car")
	private String user_car;
	
	@Column(name="user_description")
	private String user_description;
	
	@Column(name="is_active")
	private boolean is_active;
	
	@OneToMany(mappedBy="id_user",
			cascade= {CascadeType.PERSIST, CascadeType.MERGE,
					  CascadeType.DETACH, CascadeType.REFRESH})
	private List<Seat> listOfUserSeats;
	
	public User() {
		
	}
}

Pobawiłem się trochę JsonIgnore i wynik jest już ciekawszy, jednak chciałbym w swoim wyniku mieć otrzymać JSON'a, w którym zamiast obiektu User dostałbym jego ID. Czy ktoś wie jak to zrobić ?

{
    "serverResponse": {
        "msg": "Code exist in database",
        "succesful": true
    },
    "activationCode": {
        "id_activation_code": 1,
        "serial_number": "abcd",
	"id_user" : 3
    }
}
3

Stack Overflow przy restowaniu encji JPA jest znany.
Byłbyś wolny od problemu, gdyby encje JPA zamykały się w niższej warstwie (jak zwał tak zwał: repo, service, DAO) a w górę szły DTO już nie mające związków z JPA, o związkach (jakby relacjach) wysokopoziomowych a nie specyficznych dla bazy.

0

Nie wiem z jakiego kursu korzystałeś ale masz zkiepszczone mapowanie. Zobacz tutaj jak się to robi i od razu zauważysz co zrobiłeś źle :)
https://www.baeldung.com/jpa-one-to-one

1

Jest też inne rozwiązanie. Odpowiedz sobie na pytanie czy Encja User musi wiedzieć o ActivationCode. Jeśli usuniesz z User relacje do ActivationCode to nie wpadniesz w stackoverflow.

2

Jest kilka możliwych odpowiedzi. Zakładam, że wykorzystujesz Jacksona.

  1. Jak pisał @AnyKtokolwiek nie używaj encji, ale przepakuj swoje obiekty w DTOsy. Jednak przy mniejszych projektach nie ma to większego sensu. Dodajesz warstwy, bez widocznych korzyści.
  2. Użyj na polu w jednej z encji adnotacji @JsonIgnore. Zaletą tego podejścia jest prostota, a wadą „sztywne” wyłączenie pola.
  3. Użyj adnotacji @JsonView i następnie steruj odpowiednio wygenerowanymi obiektami:

@Entity
@Table(name="activation_code")
public class ActivationCode {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id_activation_code")
    @JsonView({View.Activation.class, View.User.class})
    private int id_activation_code;

    @Column(name="serial_number")
    @JsonView({View.Activation.class, View.User.class})
    private String serial_number;

    @OneToOne(mappedBy="id_activation_code", cascade= {CascadeType.DETACH,
                                                       CascadeType.MERGE,
                                                       CascadeType.PERSIST,
                                                       CascadeType.REFRESH })
    @JsonView({View.Activation.class})
    private User user;

    public ActivationCode() {

    }
}

//---

@Entity
@Table(name="user")
public class User {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id_user")
    private int id_user;

    @OneToOne(cascade={CascadeType.DETACH,
                        CascadeType.MERGE,
                        CascadeType.PERSIST,
                        CascadeType.REFRESH})
    @JoinColumn(name="id_activation_code")
    @JsonView({View.User.class})
    private ActivationCode id_activation_code;

    @Column(name="user_name")
    @JsonView({View.Activation.class, View.User.class})
    private String user_name;

    @Column(name="user_surname")
    @JsonView({View.Activation.class, View.User.class})
    private String user_surname;

    @Column(name="user_email")
    @JsonView({View.Activation.class, View.User.class})
    private String user_email;

    @Column(name="user_password")
    @JsonView({View.Activation.class, View.User.class})
    private String user_password;

    @Column(name="user_phone_number")
    @JsonView({View.Activation.class, View.User.class})
    private String user_phone_number;

    @Column(name="user_car")
    @JsonView({View.Activation.class, View.User.class})
    private String user_car;

    @Column(name="user_description")
    @JsonView({View.Activation.class, View.User.class})
    private String user_description;

    @Column(name="is_active")
    @JsonView({View.Activation.class, View.User.class})
    private boolean is_active;

    @OneToMany(mappedBy="id_user",
            cascade= {CascadeType.PERSIST, CascadeType.MERGE,
                      CascadeType.DETACH, CascadeType.REFRESH})
    @JsonView({View.Activation.class, View.User.class})
    private List<Seat> listOfUserSeats;

    public User() {

    }
}

Gdzie View.Activation.class i View.User.class to są twoje definicje widoków (przykładowo):

interface View {
    interface Activation{}
    interface User{}
}

I później już wystarczy odpowiednio skonfigurować sobie mapper:

public class JsonViewSerializeUtils {

  public static String serializeObjectToString(Object object, Class<?> serializationView) throws JsonProcessingException{
      ObjectMapper mapper = new ObjectMapper();
      mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
    String result = mapper.writerWithView(serializationView).writeValueAsString(object);
    return result;
  }
  
  public static Object deserializeStringToObject(String jsonString, Class<?> view, Class<?> valueType) throws JsonProcessingException, IOException {
    ObjectMapper mapper = new ObjectMapper();
    Object object = mapper.readerWithView(view).forType(valueType).readValue(jsonString);
    return object;
  }
}

i go użyć:

User user =…
ActivationCode activationCode =…
// wiązanie setterami
// i później
JsonViewSerializeUtils.serializeObjectToString(user, View.User.class);
JsonViewSerializeUtils.serializeObjectToString(activationCode, View.Activation.class);
0

Cześć raz jeszcze. Wszystko fajnie działa wg metody która przedstawił tu @Koziołek za co ogromne dzięki.

Jednym minusem jest to, że ten JSON który jest wypluwany przez serializer czasem jest nieczytelny i w takiej formie

"{\"id_activation_code\":3,\"serial_number\":\"asdf\",\"user\":{\"id_user\":5,\"user_name\":\"testowanie update u\",\"user_surname\":\"testowanie update u\",\"user_email\":\"test\",\"user_password\":\"test\",\"user_phone_number\":\"test\",\"user_car\":\"test\",\"user_description\":\"test\",\"is_active\":true}}"

Co nie jest nawet formatowane przez przeglądarki do JSON'a tylko wyświetla się jako zwykły tekst. Zauważyłem, że można takiego JSONa znowu zdeserializować do postaci obiektu i nie będzie nieskończonej rekurencji, problem jest tylko jeden

title

Jest jakaś możliwość, żeby po deserializacji to pole było olewane ? Bądź to dobry sposób, żeby zamiast obiektu zapisanego w JSONIe w takim stylu jak na zdjęciu wysyłać tekst, taki jak kilka linijek wyżej i formatować go w programie który go odbiera ? Który styl lepszy ? Taki dziwny null, czy taki dziwny response ?

1
RezyserKinaAkcji napisał(a):

Cześć raz jeszcze. Wszystko fajnie działa wg metody która przedstawił tu @Koziołek za co ogromne dzięki.

Jednym minusem jest to, że ten JSON który jest wypluwany przez serializer czasem jest nieczytelny i w takiej formie

"{\"id_activation_code\":3,\"serial_number\":\"asdf\",\"user\":{\"id_user\":5,\"user_name\":\"testowanie update u\",\"user_surname\":\"testowanie update u\",\"user_email\":\"test\",\"user_password\":\"test\",\"user_phone_number\":\"test\",\"user_car\":\"test\",\"user_description\":\"test\",\"is_active\":true}}"
mapper.enable(SerializationFeature.INDENT_OUTPUT);

albo

 mapper.writerWithDefaultPrettyPrinter()
0

@crejk:

	public static String serializeObjectToString(Object object, Class<?> serializationView)
			throws JsonProcessingException {
		ObjectMapper mapper = new ObjectMapper();
		mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
		String result = mapper.writerWithView(serializationView).writeValueAsString(object);
		return result;
	}

	public static Object deserializeStringToObject(String jsonString, Class<?> view, Class<?> valueType)
			throws JsonProcessingException, IOException {
		ObjectMapper mapper = new ObjectMapper();
		Object object = mapper.readerWithView(view).forType(valueType).readValue(jsonString);
		return object;
	}

Tylko własnie tutaj mam już zajęty writer :P

1
RezyserKinaAkcji napisał(a):

@crejk:

	public static String serializeObjectToString(Object object, Class<?> serializationView)
			throws JsonProcessingException {
		ObjectMapper mapper = new ObjectMapper();
		mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
		String result = mapper.writerWithView(serializationView).writeValueAsString(object);
		return result;
	}

	public static Object deserializeStringToObject(String jsonString, Class<?> view, Class<?> valueType)
			throws JsonProcessingException, IOException {
		ObjectMapper mapper = new ObjectMapper();
		Object object = mapper.readerWithView(view).forType(valueType).readValue(jsonString);
		return object;
	}

Tylko własnie tutaj mam już zajęty writer :P

    public static String serializeObjectToString(Object object, Class<?> serializationView) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper()
                .enable(SerializationFeature.INDENT_OUTPUT)
                .disable(MapperFeature.DEFAULT_VIEW_INCLUSION);

        return mapper.writerWithView(serializationView).writeValueAsString(object);
    }
0

@crejk:

title

Sprawa jest jeszcze poważniejsza xd

0
RezyserKinaAkcji napisał(a):

Jednym minusem jest to, że ten JSON który jest wypluwany przez serializer czasem jest nieczytelny i w takiej formie

oprogramowanie które CZASEM daje backslashe byłoby dziwne.
A przypadkiem nie jest tak, że CZASEM oceniasz JSONa na podstawie wyświetlania go w debugerze?

0
RezyserKinaAkcji napisał(a):

@crejk:

title

Sprawa jest jeszcze poważniejsza xd

Mógłbyś wrzucić kod kontrolera, który to zwraca?

0

@crejk:

package borgwarner.com.pickmeup.rest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.core.JsonProcessingException;

import borgwarner.com.pickmeup.entity.ActivationCode;
import borgwarner.com.pickmeup.jsonhelper.JsonViewSerializeUtils;
import borgwarner.com.pickmeup.jsonhelper.View;
import borgwarner.com.pickmeup.responses.ActivationCodeResponse;
import borgwarner.com.pickmeup.service.ActivationCodeService;


@RestController
@RequestMapping("/api")
public class ActivationCodeRestController {

	private ActivationCodeService activationCodeService;
	
	@Autowired
	public ActivationCodeRestController(ActivationCodeService activationCodeService) {
		this.activationCodeService = activationCodeService;
	}
	
	@GetMapping("/activationcodes")
	public List<String> findAll(){
		List<ActivationCode> listOfActivationCodes = activationCodeService.findAll();
		List<String> listOfAllActivationCodesAfterSerializing = new ArrayList<>();
		String result = "";	
		for (ActivationCode activationCode : listOfActivationCodes) {
			try {
				result = JsonViewSerializeUtils.serializeObjectToString(activationCode, View.ActivationCode.class);
			} catch (JsonProcessingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			listOfAllActivationCodesAfterSerializing.add(result);
		}
		return listOfAllActivationCodesAfterSerializing;
	}

	@GetMapping("/activationcodes/serialnumber")
	@ResponseBody
	public ActivationCodeResponse findActivationCodeBySerialNumber(@RequestParam String serialNumber) {
		return activationCodeService.findActivationCodeBySerialNumber(serialNumber);	
	}
	
	@GetMapping("/activationcodes/{theID}")
	public ActivationCodeResponse findActivationCodeByID(@PathVariable int theID) {
		return activationCodeService.findActivationCodeByID(theID);
	}	
	
}
1
RezyserKinaAkcji napisał(a):

@crejk:

@RestController
@RequestMapping("/api")
public class ActivationCodeRestController {

	private ActivationCodeService activationCodeService;
	
	@Autowired
	public ActivationCodeRestController(ActivationCodeService activationCodeService) {
		this.activationCodeService = activationCodeService;
	}
	
	@GetMapping("/activationcodes")
	public List<String> findAll(){
		List<ActivationCode> listOfActivationCodes = activationCodeService.findAll();
		List<String> listOfAllActivationCodesAfterSerializing = new ArrayList<>();
		String result = "";	
		for (ActivationCode activationCode : listOfActivationCodes) {
			try {
				result = JsonViewSerializeUtils.serializeObjectToString(activationCode, View.ActivationCode.class);
			} catch (JsonProcessingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			listOfAllActivationCodesAfterSerializing.add(result);
		}
		return listOfAllActivationCodesAfterSerializing;
	}

W sumie to nie potrzebujesz nawet tego JsonViewSerializeUtils. Zwracasz po prostu List<ActivationCode> i dodajesz do metody adnotacje @JsonView({View.Activation.class, View.User.class}).

    @GetMapping("/activationcodes")
    @JsonView(View.ActivationCode.class)
    public List<ActivationCode> findAll() {
        return activationCodeService.findAll();
    }
0

@crejk: Wow. Faktycznie.

A wiesz dlaczego ucina akurat te dwa elementy ? Czegoś mi brakuje ?

title

0

@RezyserKinaAkcji: sprawdź czy nie zgubiłeś adnotacji @JsonView w klasie OfferedRide nad polem private List<Seat> listOfUserSeats; listOfSeats.

0

@crejk: W klasie User nie mam listOfUserSeats. Mam tam liste OfferedRide, a listOfUserSeats jest polem w klasie OfferedRide

@Entity
@Table(name="offered_ride")
public class OfferedRide {
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="id_offered_ride")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private int id_offered_ride;

	@Column(name="date_of_ride")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private Date date_of_ride;
	
	@Column(name="time_of_ride")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private Time time_of_ride;
	
	@Column(name="number_of_free_seats")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private int number_of_free_seats;
	
	@Column(name="ride_category")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private int ride_category;
	
	@Column(name="from_where")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String from_where;
	
	@Column(name="to_where")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String to_where;
	
	@Column(name="user_comment")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_comment;
	
	@ManyToOne(cascade= {CascadeType.PERSIST, CascadeType.MERGE,
			CascadeType.DETACH, CascadeType.REFRESH})
	@JoinColumn(name="id_user")
	@JsonView({View.OfferedRide.class})
	private User user;
	
	@OneToMany(mappedBy="offeredRide",
			cascade= CascadeType.ALL)
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private List<Seat> listOfSeats;
	
	public OfferedRide() {
		
	}
@Entity
@Table(name="user")
public class User {
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="id_user")
	@JsonView({View.User.class, View.ActivationCode.class})
	private int id_user;
	
	@OneToOne(cascade={CascadeType.DETACH,
			 CascadeType.MERGE,
			 CascadeType.PERSIST,
			 CascadeType.REFRESH })
	@JoinColumn(name="id_activation_code")
	@JsonView({View.User.class})
	private ActivationCode activationCode;

	@Column(name="user_name")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_name;
	
	@Column(name="user_surname")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_surname;
	
	@Column(name="user_email")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_email;
	
	@Column(name="user_password")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_password;
	
	@Column(name="user_phone_number")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_phone_number;
	
	@Column(name="user_car")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_car;
	
	@Column(name="user_description")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private String user_description;
	
	@Column(name="is_active")
	@JsonView({View.User.class, View.ActivationCode.class, View.OfferedRide.class})
	private boolean is_active;
	
	@OneToMany(mappedBy="user",
			cascade= {CascadeType.PERSIST, CascadeType.MERGE,
					  CascadeType.DETACH, CascadeType.REFRESH})
	@JsonView({View.User.class, View.ActivationCode.class})
	private List<OfferedRide> listOfOfferedRides;
	
	public User() {
		
	}
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="id_seat")
	private int id_seat;
	
	@ManyToOne(cascade= {CascadeType.PERSIST, CascadeType.MERGE,
			CascadeType.DETACH, CascadeType.REFRESH})
	@JoinColumn(name="id_offered_ride")
	@JsonView({View.Seat.class})
	private OfferedRide offeredRide;
	
	@ManyToOne(cascade= {CascadeType.PERSIST, CascadeType.MERGE,
			CascadeType.DETACH, CascadeType.REFRESH})
	@JoinColumn(name="id_user")
	@JsonView({View.Seat.class})
	private User id_user;
	
	public Seat() {
		
	}
1
RezyserKinaAkcji napisał(a):
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="id_seat")
	private int id_seat;
	
	@ManyToOne(cascade= {CascadeType.PERSIST, CascadeType.MERGE,
			CascadeType.DETACH, CascadeType.REFRESH})
	@JoinColumn(name="id_offered_ride")
	@JsonView({View.Seat.class})
	private OfferedRide offeredRide;
	
	@ManyToOne(cascade= {CascadeType.PERSIST, CascadeType.MERGE,
			CascadeType.DETACH, CascadeType.REFRESH})
	@JoinColumn(name="id_user")
	@JsonView({View.Seat.class})
	private User id_user;
	
	public Seat() {
		
	}

Dodaj JsonView View.ActivationCode.class do pól.

    @ManyToOne(cascade= {CascadeType.PERSIST, CascadeType.MERGE,
            CascadeType.DETACH, CascadeType.REFRESH})
    @JoinColumn(name="id_offered_ride")
    @JsonView({View.Seat.class, View.ActivationCode.class})
    private OfferedRide offeredRide;

Edit: Mój 2 pomysł jednak nie przejdzie w Springu

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