Spring Boot + React でアプリを作成してみた【完全なエンドツーエンドのWebアプリケーション】


## spring initializrでプロジェクトを作成する

Maven

Group: com.dailycodework
Artifact: sbr-demo
Name : sbr-demo
Description: Demo project for Spring Boot
Package name: com.dailycodework.sbr-demo

jar
Java 17

lombok
Spring Data JPA
Spring Web
MySQL Driver

## モデル、サービス、リポジトリ、例外、コントローラ、および設定ファイルの作成

①src/main/java/dailycodework/sbrdemoにmodel/Student.javaを作成する

```model/Student.java
package com.dailycodework.sbrdemo.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    @NuturalId(mutable = true)
    private String email;
    private String department;

}
```

②src/main/java/dailycodework/sbrdemoにservice/IStudentService.javaを作成する

```service/IStudentService.java
package com.dailycodework.sbrdemo.service;

import com.dailycodework.sbrdemo.model.Student;
import java.util.List;

public interface IStudentService {
    Student addStudent(Student student);

    List<Student> getStudents();

    Student updateStudent(Student student, Long id);

    Student getStudentById(Long id);

    void deleteStudent(Long id);
}
```

③src/main/java/dailycodework/sbrdemoにservice/StudentService.javaを作成する

```service/StudentService.java
package com.dailycodework.sbrdemo.service;

import org.springframework.stereotype.Service;

import com.dailycodework.sbrdemo.exception.StudentNotFoundException;
import com.dailycodework.sbrdemo.model.Student;
import com.dailycodework.sbrdemo.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentService extends JpaRepository<Student, Long>

@Service
@RequiredArgsConstructor
public class StudentService implements IStudentService {
        private final StudentRepository studentRepository;

        @Override
        public List<Student> getStudents() {
            return studentRepository.findAll();
        }

    @Override
    public Student addStudent(Student student) {
        if (studentAlreadyExists(student.getEmail())){
            throw new StudentAlreadyExistsException(student.getEmail()+"already exsits!")
        }
            return studentRepository.save(student);
    }

        @Override
        public Student updateStudent(Student student, Long id) {
            return studentRepository.findById(id).map(st -> {
                st.setFirstName(student.getFirstName());
                st.setLastName(student.getLastName());
                st.setEmail(student.getEmail());
                st.setDepartment(student.getDepartment());
                return studentRepository.save(st);
            }).orElseThrow(() -> new StudentNotFoundException("Sorry, this student could not be found"));
        }

        @Override
        public Student getStudentById(Long id) {
            return studentRepository.findById(id)
                    .orElseThrow(() -> new StudentNotFoundException("Sorry,no student found with the Id:" + id));
        }

        @Override
        public void deleteStudent(Long id) {
            if (!studentRepository.existsById(id)) {
                throw new StudentNotFoundException("Sorry, student not found");
            }
            studentRepository.deleteById(id);
        }

        private boolean studentAlreadyExists(String email) {
            return studentRepository.findByEmail(email).isPresent();
        }
}
```
④src/main/java/dailycodework/sbrdemoにrepository/StudentRrepository.javaを作成する


```repository/StudentRrepository.java
package com.dailycodework.sbrdemo.repository;

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

public interface StudentRepository extends JpaRepository<Student, Long> {

    Optional<Student> findByEmail(String email);

}
```

⑤src/main/java/dailycodework/sbrdemoにexception/StudentAlreadyExistsException.javaを作成する

```exception/StudentAlreadyExistsException.java
package com.dailycodework.sbrdemo.exception;

public class StudentAlreadyExistsException extends RuntimeException {

    public StudentAlreadyExistsException(String message) {
        super(message);
    }
}
```

⑥src/main/java/dailycodework/sbrdemoにexception/StudentNotFoundException.javaを作成する

```exception/StudentNotFoundException.java
package com.dailycodework.sbrdemo.exception;

public class StudentNotFoundException extends RuntimeException {

    public StudentNotFoundException(String message) {
        super(message);
    }
}
```

⑦src/main/java/dailycodework/sbrdemoにcontroller/StudentController.javaを作成する
```controller/StudentController.java
package com.dailycodework.sbrdemo.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/students")
@RequiredArgsConstructor
public class StudentController {
    private final IStudentService studentService;

    @GetMapping
    public ResponseEntity<List<Student>> getStudents() {
        return new ResponseEntity<>(studentService.getStudents(), HttpStatus.FOUND);
    }

    @PostMapping
    public Student addStudent(@RequestBody Student student) {
        return studentService.addStudent(student);
    }

    @PutMapping("/update/{id}")
    public Student updateStudent(@RequestBody Student student, @PathVariable Long id) {
        return studentService.updateStudent(student, id);
    }

    @DeleteMapping("/delete/{id}")
    public void deleteStudent(@PathVariable Long id) {
        studentService.deleteStudent(id);
    }

    @GetMapping("/student/{id}")
    public void getStudentById(@PathVariable Long id) {
        return studentService.getStudentById(id);
    }
}
```

⑧resources/application.ymlを編集する

```resources/application.yml
server:
port: 9192

spring:
datasource:
username: root
password: admin
url: jdbc:mysql://localhost:3306/sbr_db
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: create
format_sql :true
```

⑨src/main/java/dailycodework/sbrdemoにexception/ CustomExceptionHandler.javaを作成する

```exception/CustomExceptionHandler.java
package com.dailycodework.sbrdemo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.*;

@RestControllerAdvice
public class CustomExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, String> handleException(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        BindingResult bindingResult = ex.getBindingResult();
        bindingResult.getFieldErrors()
                .forEach(fieldError -> errors.put(fieldError.getField(), fieldError.getDefaultMessage()));
        return errors;
    }

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(StudentNotFoundException.class)
    public Map<String, String> userNotFound(StudentNotFoundException ex) {
        Map<String, String> errors = new HashMap<>();
        errors.put("error", ex.getMessage());
        return errors;
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(StudentAlreadyExistsException.class)
    public Map<String, String> userAlreadyExists(StudentAlreadyExistsException ex) {
        Map<String, String> errors = new HashMap<>();
        errors.put("error", ex.getMessage());
        return errors;
    }
}
```

## Postman

①New Collectionにて名前を入力　⇨ 　『...』をクリック ⇨ 『Add Folder』⇨ studentフォルダを作成

②　『＋』タブをクリック ⇨ GETに　http://localhost:9192/studentsを入力 ⇨ 『Send』

③ 『Save』の右『Save As』⇨ 『名前を入力したのを選択』⇨ 『studentフォルダ』⇨
『Request name』を入力 ⇨『Save』

④　『Post』⇨ 『Body』 ⇨ 『raw』 ⇨ textをJsonに選択　⇨ 

```
{
 "firstName":"Simpson",
 "lastName":"Alfred",
 "email":"simpson@email.com",
 "deparment":"Software Engineering"

}
```
⇨ 『Send』

⑤ 『Save』の右『Save As』⇨ 『名前を入力したのを選択』⇨ 『studentフォルダ』⇨
『Request name』を入力 ⇨『Save』

⑥　『Get』⇨ 『Body』 ⇨ 『raw』 ⇨ textをJsonに選択　⇨ 

```
{
 "firstName":"Juliet",
 "lastName":"Markson",
 "email":"juliet@email.com",
 "deparment":"Accounting"

}
```
⇨ 『Send』

## Frontendを作成する

①プロジェクトを作成する
```
npx i create-react-app <プロジェクト名>
npx i axios
npx i boostap --sava
```
②src/index.jsを編集する

```src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

③src/App.jsを編集する

```src/App.js
import AddStudent from './components/student/AddStudent.js';
import EditStudent from './components/student/EditStudent.js';
import StudentProfile from './components/student/StudentProfile.js';
import StudentsView from './components/student/StudentsView.js';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <main className='container mt-5'>
      <Router>
        <NavBar />
        <Routes>
          <Route exact path='/' element={<Home />}></Route>
          <Route exact path='/view-students' element={<StudentsView />}></Route>
          <Route exact path='/add-students' element={<AddStudent />}></Route>
          <Route
            exact
            path='/edit-students/:id'
            element={<EditStudent />}
          ></Route>
          <Route
            exact
            path='/student-prfile/:id'
            element={<StudentProfile />}
          ></Route>
        </Routes>
      </Router>
    </main>
  );
}

export default App;
```

④src/Home.jsを作成する

```src/Home.js
import React from 'react';

const Home = () => {
  return <div>Home</div>;
};

export default Home;
```

⑤src/componets/student/StudentsView.jsを作成する

```src/componets/student/StudentsView.js
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { FaTrashAlt, FaEdit, FaEye } from 'react-icons/fa';
import { Link } from 'react-router-dom';
import Search from '../common/Search';

const StudentsView = () => {
  const [students, setStudents] = useState([]);
  const [search, setSearch] = useState('');

  useEffect(() => {
    loadStudents();
  }, []);

  const loadStudents = async () => {
    const result = await axios.get('http://localhost:9192/students', {
      validateStatus: () => {
        return true;
      },
    });
    if (result.status === 302) {
      setStudents(result.data);
    }
  };

  const handleDelete = async (id) => {
    await axios.delete(`http://localhost:9192/students/delete/${id}`);
    loadStudents();
  };

  return (
    <section>
      <Search search={search} setSearch={setSearch} />
      <table className='table table-bordered table-hover shadow'>
        <thead>
          <tr className='text-center'>
            <th>ID</th>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>
            <th>Depatment</th>
            <th colSpan='3'>Actions</th>
          </tr>
        </thead>

        <tbody className='text-center'>
          {students
            .fill((st) => st.firstName.toLowerCase().includes(search))
            .map((student, index) => (
              <tr key={student.id}>
                <th scope='row' key={index}>
                  {index + 1}
                </th>
                <td>{student.firstName}</td>
                <td>{student.lastName}</td>
                <td>{student.email}</td>
                <td>{student.deparment}</td>
                <td className='mx-2'>
                  <Link
                    to={`/student-profile/${student.id}`}
                    className='btn btn-info'
                  >
                    <FaEye />
                  </Link>
                </td>
                <td className='mx-2'>
                  <Link
                    to={`/edit-student/${student.id}`}
                    className='btn btn-warning'
                  >
                    <FaEdit />
                  </Link>
                </td>
                <td className='mx-2'>
                  <button
                    className='btn btn-danger'
                    onChange={() => handleDelete(student.id)}
                  >
                    <FaTrashAlt />
                  </button>
                </td>
              </tr>
            ))}
        </tbody>
      </table>
    </section>
  );
};

export default StudentsView;
```


⑦src/componets/common/Navbar.jsを作成する

```src/componets/common/Navbar.js
import React from 'react';
import { Link } from 'react-router-dom';

const NavBar = () => {
  return (
    <nav className='navbar navbar-expand-lg navbar-dark bg-dark mb-5'>
      <div className='container-fluid'>
        <Link className='navbar-brand' to={'/'}>
          SBR demo
        </Link>
        <button
          className='navbar-toggler'
          type='button'
          data-bs-toggle='collapse'
          data-bs-target='#navbarNav'
          aria-controls='navbarNav'
          aria-expanded='false'
          aria-label='Toggle navigation'
        >
          <span className='navbar-toggler-icon'></span>
        </button>
        <div className='collapse navbar-collapse' id='navbarNav'>
          <ul className='navbar-nav'>
            <li className='nav-item'>
              <Link
                className='nav-link active'
                aria-current='page'
                to={'view-students'}
              >
                View All Student
              </Link>
            </li>
            <li className='nav-item'>
              <Link className='nav-link' to={'/add-student'}>
                Add new Student
              </Link>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  );
};

export default NavBar;
```
⑨src/componets/student/AddStudent.jsを作成する


```src/componets/student/AddStudent.js
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';

const AddStudent = () => {
  let navigate = useNavigate();
  const [student, setStudent] = useState({
    firstName: '',
    lastName: '',
    email: '',
    department: '',
  });
  const { firstName, lastName, email, department } = student;

  const handleInputChange = (e) => {
    setStudent({ ...student, [e.target.name]: e.target.value });
  };

  const savaStudent = async (e) => {
    e.preventDefault();
    await axios.post('http://localhost:9192/students', student);
    navigate('/view-students');
  };

  return (
    <div className='col-sm-8 py-2 px-5'>
      <from onChange={(e) => savaStudent(e)}>
        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='FirstName'>
            First Name
          </label>
          <input
            className='form-controll col-sm-6'
            type='text'
            name='firstName'
            id='firstName'
            required
            value={firstName}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='lastName'>
            Last Name
          </label>
          <input
            className='form-controll col-sm-6'
            type='text'
            name='lastName'
            id='lastName'
            required
            value={lastName}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='email'>
            Email
          </label>
          <input
            className='form-controll col-sm-6'
            type='email'
            name='email'
            id='email'
            required
            value={email}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='department'>
            Department
          </label>
          <input
            className='form-controll col-sm-6'
            type='text'
            name='department'
            id='department'
            required
            value={department}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='col-sm-2'>
          <button type='submit' className='btn btn-outline-warning btn-lg'>
            Save
          </button>
        </div>
        <div className='col-sm-2'>
          <Link
            to={'/view-students'}
            type='submit'
            className='btn btn-outline-warning btn-lg'
          >
            Cancel
          </Link>
        </div>
      </from>
    </div>
  );
};

export default AddStudent;
```
⑩src/componets/student/EditStudent.jsを作成する

```src/componets/student/EditStudent.js
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Link, useNavigate, useParams } from 'react-router-dom';

const EditStudent = () => {
  let navigate = useNavigate();
  const { id } = useParams();
  const [student, setStudent] = useState({
    firstName: '',
    lastName: '',
    email: '',
    department: '',
  });
  const { firstName, lastName, email, department } = student;

  useEffect(() => {
    loadStudent();
  }, []);

  const loadStudent = async () => {
    const result = await axios.get(
      `http://localhost:9192/students/student/${id}`
    );
    setStudent(result.data);
  };

  const handleInputChange = (e) => {
    setStudent({ ...student, [e.target.name]: e.target.value });
  };

  const updateStudent = async (e) => {
    e.preventDefault();
    await axios.put(`http://localhost:9192/students/update/${id}`, student);
    navigate('/view-students');
  };

  return (
    <div className='col-sm-8 py-2 px-5'>
      <h2 className='mt-5'>Edit Student</h2>
      <from onChange={(e) => updateStudent(e)}>
        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='FirstName'>
            First Name
          </label>
          <input
            className='form-controll col-sm-6'
            type='text'
            name='firstName'
            id='firstName'
            required
            value={firstName}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='lastName'>
            Last Name
          </label>
          <input
            className='form-controll col-sm-6'
            type='text'
            name='lastName'
            id='lastName'
            required
            value={lastName}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='email'>
            Email
          </label>
          <input
            className='form-controll col-sm-6'
            type='email'
            name='email'
            id='email'
            required
            value={email}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='input-group mb-5'>
          <label className='input-group-text' htmlFor='department'>
            Department
          </label>
          <input
            className='form-controll col-sm-6'
            type='text'
            name='department'
            id='department'
            required
            value={department}
            onChange={(e) => handleInputChange(e)}
          />
        </div>

        <div className='col-sm-2'>
          <button type='submit' className='btn btn-outline-warning btn-lg'>
            Save
          </button>
        </div>
        <div className='col-sm-2'>
          <Link
            to={'/view-students'}
            type='submit'
            className='btn btn-outline-warning btn-lg'
          >
            Cancel
          </Link>
        </div>
      </from>
    </div>
  );
};

export default EditStudent;
```

⑪src/componets/student/StudentProfile.jsを作成する

```src/componets/student/StudentProfile.js
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';

const StudentProfile = () => {
  const { id } = useParams();
  const [student, setStudent] = useState({
    firstName: '',
    lastName: '',
    email: '',
    department: '',
  });

  useEffect(() => {
    loadStudent();
  }, []);

  const loadStudent = async () => {
    const result = await axios.get(
      `http://localhost:9192/students/student/${id}`
    );
    setStudent(result.data);
  };

  return (
    <section className='shadow' style={{ backgroudColor: 'whitesmoke' }}>
      <div className='container py-5'>
        <div className='row'>
          <div className='col-lg-3'>
            <div className='card mb-4'>
              <div className='card-body text-center'>
                <img
                  src='https://mdbcdn.b-cdn.net/img/Photos/new-template/boostrap-chat/avatar'
                  alt='avatar'
                  className='rounded-circle img-fluid'
                  style={{ width: 150 }}
                />
                <h5 className='my-3'>
                  {`${student.firstName} ${student.lastName}`}
                </h5>
                <div className='d-flex justify-content-center mb-2'>
                  <button type='button' className='btn btn-outline-primary'>
                    Call
                  </button>
                  <button
                    type='button'
                    className='btn btn-outline-warning ms-1'
                  >
                    Message
                  </button>
                </div>
              </div>
            </div>
          </div>

          <div className='col-lg-9'>
            <div className='card mb-4'>
              <div className='card-body'>
                <hr />

                <div className='row'>
                  <div className='col-sm-3'>
                    <h5 className='mb-0'>First Name</h5>
                  </div>

                  <div className='col-sm-9'>
                    <p className='text-muted mb-0'>{student.firstName}</p>
                  </div>
                </div>
                <hr />

                <div className='row'>
                  <div className='col-sm-3'>
                    <h5 className='mb-0'>Last Name</h5>
                  </div>

                  <div className='col-sm-9'>
                    <p className='text-muted mb-0'>{student.lastName}</p>
                  </div>
                </div>
              </div>
              <hr />

              <div className='row'>
                <div className='col-sm-3'>
                  <h5 className='mb-0'>Email</h5>
                </div>
                <div className='col-sm-9'>
                  <p className='text-muted mb-0'>{student.email}</p>
                </div>
              </div>
              <hr />

              <div className='row'>
                <div className='col-sm-9'>
                  <h5 className='mb-0'>Department</h5>
                </div>

                <div className='col-sm-9'>
                  <p className='text-muted mb-0'>{student.department}</p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

export default StudentProfile;
```

⑫src/componets/common/Search.jsを作成する

```src/componets/common/Search.js
import React from 'react';

const Search = ({ search, setSearch }) => {
  return (
    <div className='col-sm-6 mb-4'>
      <form onSubmit={(e) => e.preventDefault()}>
        <input
          className='form-controll'
          type='search'
          role='searchbox'
          placeholder='Search students...'
          value={search}
          onChange={(e) => setSearch(e.target.value)}
        ></input>
      </form>
    </div>
  );
};

export default Search;
```


## 参考サイト
[Master Java Full Stack Development with Spring Boot and ReactJS](https://www.youtube.com/watch?v=MTj7vk9F02A&t=358s)