Najprostsza aplikacja Spring JPA mająca front w React

0

Hej. Próbowałem jakiś czas temu stworzyć jak najprostszą aplikację, w której backend byłby w Springu, a front w React. Program ma tylko jedną funkcję: dla wprowadzonego w pole tekstowe miasta szuka w bazie danych "world" danych dotyczących populacji i wyświetla je.
W React zrobiłem najprostszy formularz zawierający pole tekstowe i miejsce na rezultat. Teraz nie wiem jak to połączyć tak, żeby tekst wprowadzany w pole tekstowe był argumentem dla metody w Javie "findByPopulation" i żeby w polu "Result" był wyświetlany wynik tej metody.
Byłbym bardzo wdzięczny za pomoc. Jeśli dobrze to rozumiem, to powinno być kilka linijek w JavaScripcie w komponentach 'result' dodać coś do '<dev>' i w 'Form' do '<form>, ale ciężko mi się było połapać jak to dokładnie zrobić. Poniżej linki. To jest siedem klas, ale chyba bardzo prostych.

Jakby ktoś sie zastanawiał po co w ogóle mi to. Przejrzałem kilka kursów i jest trochę tak, że kurs Spring Java w momencie, w którym jest omawiany temat komunikacji z frontem, podaje po prostu dosyć złożony (jak dla kogoś kto nigdy nic nie napisał w JavaScript) przykład widoku, który należy sobie wkleić i nie głowić się za bardzo nad tym co tam się dzieje. No ja jednak wolałbym na prostym przykładzie wiedzieć jak pola z klas w JS są identyfikowane z tymi w Javie, a poza tym stopniowo sobie wszystko rozbudowywać.

    class App extends Component {
  render (){
  return(
    <div className='App'>
  <Form />
 <Result />
 </div>
  );
}
}
const Form = () => {
    return (
        <form>
            <input type='text'/>
            <button>Wyszukaj miasto</button>
        </form>

    )
}

export default Form;
const Result = () => {

    return (
        <div>Populacja</div>
    );
}

export default Result;
mport lombok.*;
import javax.persistence.*;

@Entity
@Table(name = "city")
@Getter
@Setter
@ToString


public class City {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    int ID;
    String name;
    @Transient
    String CountryCode;
    String District;
    int Population;

    public City()
    {

    }

    public City(int ID, String name, /*String countryCode,*/ String District, int Population) {
        this.ID = ID;
        this.name = name;
        /*this.CountryCode = countryCode;*/
        this.District = District;
        this.Population = Population;
    }
}
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CityRepository extends JpaRepository<City, Long> {

    List<City> findByName(String name);
    List<City> findByPopulation(int population);

}
@RestController
public class CityService {

    @Autowired
    CityRepository cityRepository;

    @Autowired
    ObjectMapper objectMapper;

     @PostMapping("/cities")
    public ResponseEntity findCity(@RequestBody City city){
        List <City> foundedCity = cityRepository.findByName(city.getName());
        return ResponseEntity.ok(foundedCity);
    }

}
@SpringBootApplication
public class Main {


    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);

    }
}
1

Na pierwszy rzut oka widać, że to nie zagra. Napisz na jakim jesteś etapie - odpaliłeś to i jaki błąd dostajesz?

Ogólna uwaga - podczas nauki skup się na jednej rzeczy. W tym przypadku najpierw nauczyłbym się wystawiać Rest API w Springu i potestował to curlem/Postmanem, a dopiero potem zaczął naukę Reacta. Albo odwrotnie. Jedno i drugie trochę Ci zajmie, więc po kolei.

1

Zdefiniowałeś sobie endpoint (klasa CityService), który odpowiada na żądania http. Teraz z aplikacji React musisz wysłać żądanie na ten endpoint, a w odpowiedzi dostaniesz listę, którą sobie pobierasz z repozytorium. Parametry metody findCity z klasy CityService stanowią ciało żądania które wysyłasz, pomijając fakt, że używasz do tego celu encji w żądaniu musisz umieścić name za pomocą którego wyszukujesz w repozytorium. Żądanie możesz wysłać np. za pomocą Fetch API.

dodam jeszcze, że pewnie używasz serwera deweloperskiego Reacta, więc będziesz musiał sobie skonfigurować proxy w pliku package.json, wystarczy dodać linijkę: "proxy": "http://{host}:{port}", np. "proxy": "http://localhost:8080"

0
eziomou napisał(a):

Zdefiniowałeś sobie endpoint (klasa CityService), który odpowiada na żądania http. Teraz z aplikacji React musisz wysłać żądanie na ten endpoint, a w odpowiedzi dostaniesz listę, którą sobie pobierasz z repozytorium. Parametry metody findCity z klasy CityService stanowią ciało żądania które wysyłasz, pomijając fakt, że używasz do tego celu encji w żądaniu musisz umieścić name za pomocą którego wyszukujesz w repozytorium. Żądanie możesz wysłać np. za pomocą Fetch API.

dodam jeszcze, że pewnie używasz serwera deweloperskiego Reacta, więc będziesz musiał sobie skonfigurować proxy w pliku package.json, wystarczy dodać linijkę: "proxy": "http://{host}:{port}", np. "proxy": "http://localhost:8080"

Do tej pory coś podobnego robiłem w Javie EE i HTML-u, no i miałem cichą nadzieję, że jednak jest jakieś podobne rozwiązanie, tj. w klamrach formularza napisanego w React mam identifikator "name", któremu nadaję nazwę "city" i potem w metodzie Springa odbiorę to mniej więcej tak

String searchedCity = request.getParameter("city");

Potem w Springu metoda wyszukuje mi dla danego miasta populację, zapisuję je jako String "popution" i wysyłam coś w stylu:

Response resp = new Resp();
resp.send(population);

Potem na podobnej zasadzie pobieram do widoku w React, to co wysłałem z kodu Javy. No jednak jak czytam tutoriale z Reacta, to chociaż niby jest zalecany dla osób, które nie chcą się wgłębiać w JavaScript i chcą tylko sobie zrobić prosty front, żeby coś na nim przetestować, to próbując znaleźć jakieś najprostsze odpowiedzi łapię się na tym, że najlepiej byłoby przerabiać wszystko od podstaw. :(

1

To co chcesz zrobić jest osiągalne po przerobieniu tutoriala ze strony Reacta, który zahacza o komunikacje z API, a wiec niezbędne podstawy możesz nabyć w 1-2 wieczory.

0

dodam jeszcze, że pewnie używasz serwera deweloperskiego Reacta, więc będziesz musiał sobie skonfigurować proxy w pliku package.json, wystarczy dodać linijkę: "proxy": "http://{host}:{port}", np. "proxy": "http://localhost:8080"

Ale jak? Jak wklejam przed wszystkim na samym początku to od razu mi podkreśla jako błąd kompilacji. Jeśli chcę to wpakować w którąś klamrę też. Z resztą czy aby nie jest tak, że sobie w SpringBoocie mogę ustawić w Properties localhost na dowolny, tyle żeby był identyczny z tym, do którego odwołuje się kod w React i wszystko będzie działać?

0

To nie to samo. Nie jestem frontendowcem, ale serwer developerski daje Ci np. hot reload assetów. https://create-react-app.dev/docs/proxying-api-requests-in-development/

1

A tak w ogóle jeszcze, wpisałem w Google „spring react” i wyskoczył mi tutorial do Spring Data Rest ze słabej jakości frontem w React: https://spring.io/guides/tutorials/react-and-spring-data-rest/ Może Cię usatysfakcjonuje, przynajmniej miałbyś jakikolwiek front stukający do API.

0

No ok. Dzięki wszystkim. Zaraz się biorą za Reacta od podstaw, ale mam jeszcze pytanie - jak Spring odbiera zapytanie? Jak to się robi, że np. Front w React stoi na localhost:3000, backend w Springu na 8080 i to razem działa? Jeśli dam w Springu server.port=3000, a w React to jest port domyślny to z oczywistych przyczyn projekt nie ruszy. Próbowałem w ten sposób, że skoro jeśli wpiszę w formularzu i wyśle "Poznań" to w adresie będę miał: "http://localhost:3000/?city=Poznań", to żeby odebrać te słowo i wydrukować je w konsoli, powinienem zrobić coś takiego

@RestController
public class CityService {

    @Autowired
    CityRepository cityRepository;

    @Autowired
    ObjectMapper objectMapper;
    @GetMapping("/?city={name}")
    public void getCity(@PathVariable String name) 
    {
        System.out.println(name);
    }
}

Tyle, że wtedy Spring będzie to interpretował jako "localhost:8080/?city={name}". Podejrzewam, że albo trzeba coś ustawić w konfiguracji, albo po prostu nad metodą można dać jakąś adnotację wskazującą na port 3000.

0

Masz ustawione proxy na port 8080, ot cała magia. Przeczytałeś link, który podałem, czy olałeś sprawę? :)
https://create-react-app.dev/docs/proxying-api-requests-in-development/

0
paranoise napisał(a):

No ok. Dzięki wszystkim. Zaraz się biorą za Reacta od podstaw, ale mam jeszcze pytanie - jak Spring odbiera zapytanie? Jak to się robi, że np. Front w React stoi na localhost:3000, backend w Springu na 8080 i to razem działa? Jeśli dam w Springu server.port=3000, a w React to jest port domyślny to z oczywistych przyczyn projekt nie ruszy. Próbowałem w ten sposób, że skoro jeśli wpiszę w formularzu i wyśle "Poznań" to w adresie będę miał: "http://localhost:3000/?city=Poznań", to żeby odebrać te słowo i wydrukować je w konsoli, powinienem zrobić coś takiego

@RestController
public class CityService {

    @Autowired
    CityRepository cityRepository;

    @Autowired
    ObjectMapper objectMapper;
    @GetMapping("/?city={name}")
    public void getCity(@PathVariable String name) 
    {
        System.out.println(name);
    }
}

Tyle, że wtedy Spring będzie to interpretował jako "localhost:8080/?city={name}". Podejrzewam, że albo trzeba coś ustawić w konfiguracji, albo po prostu nad metodą można dać jakąś adnotację wskazującą na port 3000.

Jeszcze dodam, zrób zależności private final i dodaj konstruktor można wtedy też usunać @Autowired.
I Zamiast ?city=name użyj @RequestParam String city

0

No ok. Jest postęp. @Charles_Ray przeglądałem ten projekt, tylko, że nie zjarzyłem w jaki sposób tam jest wszystko na jednym localhoście. Koniec końców zrobiłem to przez @CrossOrigin w Springu i chyba to jest najbardziej typowe rozwiązanie. Zrobiłem jeszcze stejta w React, ale nie mogę zapisać w nim danych przekazanych przez Springa. Tak wygląda formularz i klasa startowa w React:
App.js

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
       population: '',
       district: ''
    };
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleCitySubmit = this.handleCitySubmit.bind(this);
 }

state = {
  district: '',
  population: '',
}

handleInputChange = (e) => {
  this.setState({
    value: e.target.value
  })
}

handleCitySubmit = e => {
  e.preventDefault()
  const API = `http://localhost:8080/cities`;

  fetch(API)
  .then(response => {
    if(response.ok){
  return response
}
throw Error("It doesn't works")
  })
  .then(response => response.json())
  .then(data => {
    this.setState({
      district: data.district,
      population: data.population

    })
  })
console.log('District info: '+this.state.district)
console.log('Population info: '+this.state.population)
}


  render (){
    return (
    <div className="App">
      <Form 
      value = {this.state.value}
      submit = {this.handleCitySubmit}
      />
      <Result/>
    </div>
  );
    }
}

export default App

Form.js

import React from 'react';

const Form = props => {
return (
    <form onSubmit={props.submit}>
        <input 
        type ="text" 
        value = {props.value}
        placeholder="Enter city name"
        />
        <button>Find city </button>
    
    </form>
)
}

export default Form

A tak kontroler Springa:

@RestController
public class CityService {

    @Autowired
    CityRepository cityRepository;

    @Autowired
    ObjectMapper objectMapper;

    @CrossOrigin(origins = "http://localhost:3000")
    @GetMapping("/cities")
    public ResponseEntity getCity ()throws JsonProcessingException
    {
        List <City> cities = cityRepository.findByName("New York");
        System.out.println(cities);
        return ResponseEntity.ok(objectMapper.writeValueAsString(cities));
    }
}

Tego @RequestParam użyję jak opanuję w jaki sposób dane z formularza wysyłać do metody w Springu. Na razie będę szczęśliwy jak dane ze Springa zostaną odebrane poprawnie. Trochę to dla mnie dziwne, bo jak wchodzę na > http://localhost:8080/cities
to wyświetla mi poprawnie: [{"name":"New York","countryCode":null,"population":8008278,"id":3793,"district":"New York"}], ale kiedy odbieram to przez formularz React na localhost:3000 i robię w przeglądarce "inspect" to dostaję informację, że district i population są niezdefiniowane. Ktoś wie czemu i jak to naprawić?

0
paranoise napisał(a):

No ok. Jest postęp. @Charles_Ray przeglądałem ten projekt, tylko, że nie zjarzyłem w jaki sposób tam jest wszystko na jednym localhoście. Koniec końców zrobiłem to przez @CrossOrigin w Springu i chyba to jest najbardziej typowe rozwiązanie. Zrobiłem jeszcze stejta w React, ale nie mogę zapisać w nim danych przekazanych przez Springa. Tak wygląda formularz i klasa startowa w React:
App.js

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
       population: '',
       district: ''
    };
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleCitySubmit = this.handleCitySubmit.bind(this);
 }

state = {
  district: '',
  population: '',
}

handleInputChange = (e) => {
  this.setState({
    value: e.target.value
  })
}

handleCitySubmit = e => {
  e.preventDefault()
  const API = `http://localhost:8080/cities`;

  fetch(API)
  .then(response => {
    if(response.ok){
  return response
}
throw Error("It doesn't works")
  })
  .then(response => response.json())
  .then(data => {
    this.setState({
      district: data.district,
      population: data.population

    })
  })
console.log('District info: '+this.state.district)
console.log('Population info: '+this.state.population)
}


  render (){
    return (
    <div className="App">
      <Form 
      value = {this.state.value}
      submit = {this.handleCitySubmit}
      />
      <Result/>
    </div>
  );
    }
}

export default App

Form.js

import React from 'react';

const Form = props => {
return (
    <form onSubmit={props.submit}>
        <input 
        type ="text" 
        value = {props.value}
        placeholder="Enter city name"
        />
        <button>Find city </button>
    
    </form>
)
}

export default Form

A tak kontroler Springa:

@RestController
public class CityService {

    @Autowired
    CityRepository cityRepository;

    @Autowired
    ObjectMapper objectMapper;

    @CrossOrigin(origins = "http://localhost:3000")
    @GetMapping("/cities")
    public ResponseEntity getCity ()throws JsonProcessingException
    {
        List <City> cities = cityRepository.findByName("New York");
        System.out.println(cities);
        return ResponseEntity.ok(objectMapper.writeValueAsString(cities));
    }
}

Tego @RequestParam użyję jak opanuję w jaki sposób dane z formularza wysyłać do metody w Springu. Na razie będę szczęśliwy jak dane ze Springa zostaną odebrane poprawnie. Trochę to dla mnie dziwne, bo jak wchodzę na > http://localhost:8080/cities
to wyświetla mi poprawnie: [{"name":"New York","countryCode":null,"population":8008278,"id":3793,"district":"New York"}], ale kiedy odbieram to przez formularz React na localhost:3000 i robię w przeglądarce "inspect" to dostaję informację, że district i population są niezdefiniowane. Ktoś wie czemu i jak to naprawić?

@RestController
public class CityController {
    private final CityRepository cityRepository;

    public CityController(final CityRepository repo) {
        cityRepository=repo;
   }

    @GetMapping("/cities")
    ResponseEntity<List<Cities>> getCities () {
       var cities = cityRepository.findByName("New York"); 
       return ResponseEntity.ok(cities);
   }
}

To jest poprawne, a jak chcesz parama na nazwę miasta to:

@RestController
public class CityController {
    private final CityRepository cityRepository;

    public CityController(final CityRepository repo) {
        cityRepository=repo;
   }

    @GetMapping("/cities")
    ResponseEntity<List<Cities>> getCities (@RequestParam String name) {
       var cities = cityRepository.findByName(name); 
       return ResponseEntity.ok(cities);
   }
}

A request wygląda tak GET http://{nazwaHosta}:{port}/cities?name={nazwaMiasta}

Jeszcze jedno nigdy, na prawde nigdy tej adnotacji na corsa nie uzywaj poza testami jakimis i szybkim prototypowaniem. To jest niesamowity syf w kodzie potem. Corsa sie robi w konfiguracji WebMvcConfigurer .

0

@nowyworek: No ale co to zmienia? W state dalej nie mam zapisanych district i population.

0

@nowyworek: Jeśli dobrze Cię rozumiem, to muszę albo zrobić tak, żeby ResponseEntity zawierał pojedynczy obiekt albo w metodzie Reacta wskazać najpierw, do którego elementu tablicy chcę się odwołać, a dopiero później pobrać wartość z kluczy population i district. No w React próbowałem tak:

  .then(data => {
    this.setState({
      district: data[0].district,
      population: data[0].population

    })

no i nie działa. Z drugiej strony próbowałem przekazać pojedynczy obiekt, zamiast listy w ResponseEntity o tak:

    ResponseEntity<City> getFirstCity()
    {
        Long number = Long.valueOf(3793);
        Optional<City> city = cityRepository.findById(number);
        return ResponseEntity.ok(city);
    }

No i też nie działa już na etapie kompilacji i nie kumam tego błędu, który mi wskazuje kompilator. :(

1

Dajesz nam za mało danych. Czy możesz pobrać dane z backendu za pomocą np curla albo postmana? Sprawdz w konsoli Chrome/FF (zakładka Network) co się dzieje na froncie, przychodzą tam te dane?

0

@Charles_Ray:

no instance(s) of type variable(s) exist so that Optional<City> conforms to City inference variable T has incompatible bounds: equality constraints: City lower bounds: Optional<City>
0
Charles_Ray napisał(a):

Dajesz nam za mało danych. Czy możesz pobrać dane z backendu za pomocą np curla albo postmana? Sprawdz w konsoli Chrome/FF (zakładka Network) co się dzieje na froncie, przychodzą tam te dane?

No tak. Tak to wygląda w Postmanie:
body:

[
    {
        "name": "New York",
        "countryCode": null,
        "population": 8008278,
        "id": 3793,
        "district": "New York"
    }
]

Nagłówków jest osiem. Pierwsze trzy nazywające się "vary" mają wartość: origin, Access-Control-Request-Method, Access-Control-Request-Method. Następnie Content-Type: application/json, Transfer-Encoding: chunked, Date - no wiadomo, data godzina, Keep-Alive: timout=60, Connection: keep-Alive.

A no i status 200 ok. A wiem, że w ogóle się komunikuje front z backendem przez to, że kiedy klikam ten przycisk w formularzu, to jeśli mam w tej metodzie w Springu coś w stylu:

System.out.println("Wyświetl coś");

No to wyświetla w konsoli Intellij.

Oooo! Zadziałało. Ale nie wiem czemu za którymś tam kliknięciem. Czyli szanowny kolega @nowyworek miał rację z tym, że wystarczy w React zaznaczyć, do którego elementu listy się odwołuję.

3
        Optional<City> city = cityRepository.findById(number);
        return ResponseEntity.ok(city);

Tak to sie nie da. ResponseEntity.of(city) a nie ok(). Skąd wiesz że ok skoro optional może być pusty?!

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