Objekt in OneToOne Beziehung über http request anzeigen lassen

tom.j85

Mitglied
Hallo liebe Community,

wir sollen eine art "tripadvisor" nachprogrammieren, ich verzeweifle gerade an der Berechnung der durchschnittlichen Ratings. Meine Architektur sieht folgendermaßen aus:

Es gibt eine Mutterklasse Root (der Name muss dringend geändert werden, wir konnten uns in der Gruppe nicht einigen worauf):

Java:
import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.itf.Review.Review;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "Entity_Type")
public abstract class Root {

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

    @NotBlank
    @Column(nullable = false)
    @Size(min = 1, max = 40)
    private String name;

    @NotBlank
    @Column(nullable = false)
    private String address;

    @NotBlank
    @Column(nullable = false)
    @Size(min = 1, max = 20)
    private String telephone;

    @NotBlank
    @Column(nullable = false)
    @Size(min = 1, max = 20)
    private String city;

    @NotBlank
    @Column(nullable = false)
    @Size(min = 1, max = 500)
    private String description;

    private String website;

    private Double rating;


    
    
    @JsonManagedReference
    @OneToMany(mappedBy = "rootEntity")
    private List<Review> reviewList = new ArrayList<Review>();
    
}

Dann gibt es eine Klasse Hotel, die von Root erbt

Java:
package com.itf.Entity.Hotel;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.OneToOne;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.itf.Entity.Root;
import com.itf.Review.AvgRating;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Entity
@DiscriminatorValue("Hotel")
@AllArgsConstructor
@NoArgsConstructor
public class Hotel extends Root {

    @NotBlank
    private Double price;
    
    private Integer stars;
    
    @NotNull
    private Integer rooms;
    
    private Boolean swimmingPool;


    private Boolean restaurant;

    private String breakfast;

    private Boolean wifi;

    private String parking;
    
    private String email;
    
    @JsonBackReference
    @OneToOne(mappedBy = "matchedReview")
    private AvgRating avgRatingStats;

}

eine Klasse Review

Java:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.itf.Entity.Root;
import com.sun.istack.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name="reviews")
public class Review implements Comparable< Review > {


    
    @Id
    @NotNull
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Integer id;
    
    private Double averageRating;

    
    @Min(0)
    @Max(5)
    private Double cleanness;

    @Min(0)
    @Max(5)
    private Double friendliness;

    @Min(0)
    @Max(5)
    private Double foodQuality;

    @Min(0)
    @Max(5)
    private Double ambience;

    @Min(0)
    @Max(5)
    private Double familyFriendly;

    @Size(min= 0, max = 255)
    private String comment;
    
    @NotBlank
    private String author;
    
    
    public Double getAverageRating() {
        
        return (cleanness+friendliness+foodQuality+ambience+familyFriendly)/5;
    }

    

    @JsonBackReference
    @ManyToOne
    @JoinColumn(name = "establishment_id")
    private Root rootEntity;
    
    }
:
und eine Klasse AvgRating, die ein Objekt sein soll, dass später die durchschnittlichen Werte der einzelnen Reviews speichert:

Code:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class AvgRating {

    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Integer id;
    
    
    Double avgAmbience = 0.0;
    Double avgFriendly = 0.0;
    Double avgClean = 0.0;
    Double avgFood = 0.0;
    Double avgFamily = 0.0;
    Double avgTotalRating = 0.0;
    
    
    @OneToOne
    @JoinColumn(name = "review_match")
    Review matchedReview;
    
}

Das Repository ist relativ straighfoward:

Code:
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

public interface HotelRepository extends JpaRepository<Hotel, Integer> {

}

und der Knackpunkt ist der Controller. Ich würde gerne das Objekt private AvgRating avgRatingStats in der Klasse Hotel mit den Durchschnittswerten der einzelnen Reviews versehen, sobald es aus der Datenbank gezogen wird...


Java:
import java.util.List;
import java.util.Optional;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.itf.Review.AvgRating;
import com.itf.Review.Review;




@RestController
@RequestMapping("/smart-trip/hotels/")
public class HotelController {

    @Autowired
    HotelRepository hotelRepository;

    
    Hotel hotel;
    List <Hotel> hotelList;
    List <Review> reviews;
    AvgRating avgRating = new AvgRating();
    
    
    
    
    @GetMapping
    public List<Hotel> fetchAll(){
        
        hotelList = hotelRepository.findAll();
        

        for (Hotel hotel : hotelList) {
            reviews = hotel.getReviewList();
            
            
            for (Review review : reviews) {
                avgRating.setAvgAmbience(+review.getAmbience());
                avgRating.setAvgClean(+review.getCleanness());
                avgRating.setAvgFamily(+review.getFamilyFriendly());
                avgRating.setAvgFood(+review.getFoodQuality());
                avgRating.setAvgFriendly(+review.getFriendliness());
                avgRating.setAvgTotalRating(+review.getAverageRating());
                
            }
            hotel.setAvgRatingStats(avgRating);
            
        }
        
        
        return hotelList;
        
    }
    
    }

Um die ganze Sache noch zu verkomplizieren gibt es auch Mock-Daten:

SQL:
--Add Hotels
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Hotel am Main','Olof-Staße 19','017630222','Frankfurt','Gemütliche Hotel am Main','www.hotelmain.com',4,20.0,4,90,true,'false',false,true,'true','hotelmain@mail.de');
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Wald Hotel','Waldstr.13','017645665','Frankfurt','Günstiges Hotel am Wald','www.hotelWald.com',3,30.0,2,40,true,'false',true,true,'false','waldhotel@mail.de');
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Hotel Blankenese','Hauptstraße 19','017630222','Hamburg','Gemütliches Hotel an der Elbe','www.hotelblankenese.com',3.0,40.0,3,90,false,'false',false,true,'false','hotelmain@mail.de');
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Kreuzberg Inn','Wiesenstr.13','017645665','Berlin','Hotel im Szeneviertel','www.Kreuzberginn.com',4.5,60.0,5,40,true,'false',true,true,'false','waldhotel@mail.de');
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Schlosshotel Dresden','Hauptstaße 19','017630222','Dresden','Gemütliche Hotel an der Elbe','www.hotelmain.com',3.4,70.0,5,90,false,'false',false,true,'false','schlosshotel@mail.de');
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Berghotel Frankfurt','Waldstr.13','017645665','Frankfurt','Günstiges Hotel am Wald','www.hotelWald.com',2.7,40.0,3,40,false,'true',false,false,'true','waldhotel@mail.de');
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Gala Hotel','Hauptstraße 19','017630222','Frankfurt','Hochklassiges Domizil der Elbe','www.hotel-am-berg.com',3.0,100.0,5,90,false,'false',true,true,'false','hotelmain@mail.de');
INSERT INTO root (entity_type,name,address,telephone,city,description,website,rating,price,stars,rooms,swimming_pool,restaurant,breakfast,wifi,parking,email)VALUES('Hotel', 'Stadthotel Frankfurt','Wiesenstr.13','017645665','Frankfurt','Hotel im Szeneviertel','www.stadthotel.com',4.5,60.0,5,40,true,'true',true,true,'true','waldhotel@mail.de');


--Add Reviews
insert into reviews( cleanness, friendliness, food_quality, ambience, family_friendly, comment, author, establishment_id ) values
(3,5,4,1,5,'Tolles Ding', 'das Bo', 1);
insert into reviews( cleanness, friendliness, food_quality, ambience, family_friendly, comment, author, establishment_id ) values
(3,5,2,3,1,'Tolles Ding', 'der letzte Mohikaner',2);
insert into reviews( cleanness, friendliness, food_quality, ambience, family_friendly, comment, author, establishment_id ) values
(3,4,1,1,4,'Tolles Ding', 'Janette',3);
insert into reviews( cleanness, friendliness, food_quality, ambience, family_friendly, comment, author, establishment_id ) values
(5,2,2,1,2,'Tolles Ding', 'biba-mausi', 4);
insert into reviews( cleanness, friendliness, food_quality, ambience, family_friendly, comment, author, establishment_id ) values
(1,5,2,1,3,'Tolles Ding','Kalle',1);
insert into reviews( cleanness, friendliness, food_quality, ambience, family_friendly, comment, author, establishment_id ) values
(4,5,2,2,1,'Tolles Ding','Mareike E.',3);

Das Problem scheint, dass das AvgRating Objekt nicht weitergegeben wird....wenn es vom Frontend abgerufen wird.
Zur Info noch den http-Service in Angular:

Javascript:
const apiHotelRoot = "http://localhost:8080/smart-trip/hotels/";


@Injectable({
  providedIn: 'root'
})
export class HotelService {


public getHotels(): Observable<Hotel[]> {
    return this.http.get<Hotel[]>(apiHotelRoot);

  }

und in der entsprechenden hotel-list.ts datei steht dann

Javascript:
import { Component, OnInit, Input, SimpleChange } from '@angular/core';
import { Hotel } from '../hotel/model/hotel';
import { HotelService } from '../hotel/hotel.service';
import { Pipe, PipeTransform } from "@angular/core";
import { map } from 'rxjs/operators';
import { Observable, of } from 'rxjs';


@Component({
  selector: 'app-hotel-list',
  templateUrl: './hotel-list.component.html',
  styleUrls: ['./hotel-list.component.css']
})
export class HotelListComponent implements OnInit {

  constructor(private hotelService: HotelService) { }

  ngOnInit(): void {

    this.hotelService.getHotels()
      .subscribe(fetchedHotels => (this.filteredHotelList = fetchedHotels));
      
  }
  }

Wie gesagt, das funktioniert auch alles, die filteredHotelList gibt per json-pipe auch jede menge Infos (die Mock Daten eben) ....aaaber: Das im Controller in Java generierte AvgRating Objekt, das eigentlich per oneToOne in jedem Hotel stecken sollte kommt nicht an! Direkt hinter der reviewList müsste es eigetlich stehen:

JSON:
 [ { "id": 3, "name": "Hotel am Main", "address": "Olof-Staße 19", "telephone": "017630222", "city": "Frankfurt", "description": "Gemütliche Hotel am Main", "website": "www.hotelmain.com", "rating": 4, "reviewList": [ { "id": 3, "averageRating": 2.6, "cleanness": 3, "friendliness": 4, "foodQuality": 1, "ambience": 1, "familyFriendly": 4, "comment": "Tolles Ding", "author": "Janette" }, { "id": 6, "averageRating": 2.8, "cleanness": 4, "friendliness": 5, "foodQuality": 2, "ambience": 2, "familyFriendly": 1, "comment": "Tolles Ding", "author": "Mareike E." } ], "price": 20, "stars": 4, "rooms": 90, "swimmingPool": true, "restaurant": false, "breakfast": "FALSE", "wifi": true, "parking": "true", "email": "hotelmain@mail.de" }

Ich vermute irgendwo habe ich einen Denkfehler..ist mein erstes Projekt mit Datenbanken..bin für jeden Tipp und jede Hilfe dankbar!
 
Die eine Seite der Relation wird mit @JsonManagedReference , die andere mit @JsonBackReference annotiert.
An
Java:
    @OneToOne
    @JoinColumn(name = "review_match")
    Review matchedReview;
fehlt eine Annotation.
Statt dem Frontend Code hättest du einen Test zeigen können, der das Mapping zu JSON testet und fehlschlägt.
 
Jaaa, das war das Problem! Drei Stunden Suche und da wars. Vielen Dank! Jetzt muss ich nur noch die Berechnung vernünftig hinbekommen.

Besten Dank! Ich werde mich auch mal mit JSON tests beschäftigen. JSONAssert habe ich gerade gefunden. Denke das ist was Du meinst.

Beste Grüße
Tom
 
So in etwa (ungetestet):
Java:
HotelTest

@Test
public void givenBidirectionRelation_whenSerializingAsJSON_hasAvgRatingStats() throws JsonProcessingException, IOException {
    Hotel hotel = new Hotel(...);

    String result = new ObjectMapper().writeValueAsString(hotel);

    assertThat(result, containsString("\"AvgRatingStats\":"));
}
Geht mit JSONAssert und anderen Libs wohl auch sicher filigraner, wenn man das möchte.
 

Zurück
Oben