2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-05-23

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?