
먼저 찾고 (SELECT 역할)
그 다음 지우고
commit()으로 반영
menu = db.query(Menu).filter(Menu.id == id).first()
if not menu:
# 404 같은 예외 처리
raise HTTPException(status_code=404, detail="Menu not found")
db.delete(menu)
db.commit()
조건에 맞는 레코드를 SELECT로 하나 꺼냄
필드를 수정
commit()으로 반영
menu = db.query(Menu).filter(Menu.id == id).first()
if not menu:
raise HTTPException(status_code=404, detail="Menu not found")
menu.name = new_name
menu.price = new_price
db.commit()
db.refresh(menu)
INSERT/UPDATE 후에는 보통 refresh() 한 번 해주는 패턴.
DB 세션 만들 때 보통 autocommit=False로 설정함.
이 경우 INSERT / UPDATE / DELETE 후에 commit()을 반드시 호출해야 실제 DB에 반영됨.
ORM 코드 예:
db.delete(obj)
db.commit() # 이게 있어야 실제로 삭제 반영
만약 오토커밋을 True로 하면:
commit()을 직접 안 써도 되지만
트랜잭션 제어(롤백 등)가 어려워지고
성능/안전성 측면에서 손해일 수 있음.
오토커밋이 아니면 commit()을 반드시 호출해야 한다.
ORM은 SELECT → 객체 가져오기 → 필드 수정/삭제 → commit() 이 기본 패턴
DELETE/UPDATE도 결국 “조건으로 찾고, 그 결과를 조작”하는 과정이다.
→ ORM마다 문법은 다르지만, 기본 개념은 “조건으로 검색 후, 결과를 조작”이라는 것.
예시
테이블에 이렇게 저장했다고 가정:
| Name | Club |
|---|---|
| 제라드 | 축구부 |
| 트라웃 | 야구부 |
| 르브론 | 농구부 |
축구부 --> 배구부로 이름을 바꾸고 싶으면?
UPDATE member
SET dept = '배구부'
WHERE dept = '축구부';
레코드가 수십만 개면 업데이트 부하 + 장애 발생 시 데이터 불일치 위험.
해결법: 테이블 분리 (정규화)
club,name 정보를 따로 분리:
Club 테이블
| ID | Club |
|---|---|
| 1 | 축구부 |
| 2 | 야구부 |
| 3 | 농구부 |
Name 테이블
| ID | Name |
|---|---|
| 1 | 제라드 |
| 2 | 트라웃 |
| 3 | 르브론 |
Member 테이블 (정규화 후)
| ID | Name | Club_ID |
|---|---|---|
| 1 | 제라드 | 1 |
| 2 | 트라웃 | 2 |
| 3 | 르브론 | 3 |
이제 축구부 이름만 바꾸고 싶으면:
UPDATE Club
SET Club = '배구부'
WHERE ID = 1;
멤버 테이블은 손댈 필요가 없고 성능과 일관성이 올라간다.
Club.ID → Primary Key
Name.Club_ID → Club.ID를 참조하는 Foreign Key
그렇다면 PK랑 FK는 무엇인가??
PK(프라이머리 키)는 데이터베이스의 각 행(row)을 고유하게 식별하는 데 사용되는 하나 이상의 컬럼(column)이다.
PK가 될수있는 조건은
유일함(Unique)
테이블 내 모든 행을 구별하는 유일한 기준이기때문
NULL 불가
반드시 값을 가져야 하며, NULL 값을 허용하지 않는다
테이블당 1개 (또는 복합키)
한 테이블에는 하나의 PK만 정의할 수 있습니다.
어떤 테이블의 PK가
다른 테이블에서 데이터처럼 사용될 때 그 컬럼을 FK라고 부른다.
정규화 이후에는
Name 테이블에는 club이름이 직접 없기 때문에
사람 이름 + Club 이름을 함께 보려면 JOIN이 필요하다.
SQL JOIN 예시
SELECT n.Name, c.Club
FROM Name n
JOIN Club c
ON n.ID = c.ID;
JOIN 결과표
| Name | Club |
|---|---|
| 제라드 | 축구부 |
| 트라웃 | 야구부 |
| 르브론 | 농구부 |
정규화는 업데이트,삭제 시 성능,일관성 문제를 해결하기 위해 테이블을 쪼개는 과정이다.테이블을 나눴기 때문에 다시 합쳐서 보고 싶을 때는 JOIN이 필요하다.
CRUD 기능 구성 요약
| 기능 | Method | URL | 설명 |
|---|---|---|---|
| 전체 조회 | GET | /users | 리스트 조회 |
| 단일 조회 | GET | /users/:id | Update 모드 진입 |
| 등록 | POST | /users | Insert |
| 수정 | PUT | /users/:id | Update |
| 삭제 | DELETE | /users/:id | Delete |
React에서는 Insert 화면과 Update 화면을 따로 만들기보다 하나의 컴포넌트로 통합하는 것이 일반적이다.
id 값이 있으면 Update 모드, id 값이 없으면 Insert 모드로 동작하도록 설계한다.
예시
/write 새 회원 등록
/write/:id 기존 회원 수정
페이지 이동은 document.location 대신 useNavigate() 훅을 사용해야 한다.
React Router가 내부 history를 관리하므로 강제 이동 방식은 권장되지 않는다.
입력 필드가 여러 개 있을 때, 각각 useState를 여러 번 선언하지 않고 하나의 객체로 묶어서 관리한다.
const initValue = {
name: "",
gender: "",
mobile: ""
};
const [user, setUser] = useState(initValue);
React에서는 이벤트 객체에 name, value가 포함된다. 이를 이용해 공용 onChange 핸들러를 작성한다.
const onChange = (e) => {
const { name, value } = e.target;
setUser(prev => ({
...prev,
[name]: value
}));
};
이 방식의 장점
입력 필드가 10개든 20개든 onChange 한 개로 모두 처리 가능
확장성과 유지보수성이 높음
<input
type="radio"
name="gender"
value="male"
checked={user.gender === "male"}
onChange={onChange}
/>
<input
type="radio"
name="gender"
value="female"
checked={user.gender === "female"}
onChange={onChange}
/>
id 값이 있으면 Update 요청을 보내고, 없으면 Insert 요청을 보낸다.
if (!user.id) {
await axios.post("/users", user); // Insert
} else {
await axios.put(`/users/${user.id}`, user); // Update
}
Update 모드일 때 기존 데이터를 불러오는 흐름
const { id } = useParams();
useEffect(() => {
if (id) {
fetchUser();
}
}, []);
빈 배열을 넣으면 최초 렌더링 시 한 번만 실행된다.
const navigate = useNavigate();
navigate("/users");
document.location.href 사용은 React Router 내부 제어 구조를 깨뜨리므로 비권장.
--------파이썬-----------
main.py
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from database import engine, Base, get_db
from models import User
app = FastAPI()
origins = [
"http://localhost:5173",
"http://127.0.0.1:5173"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Base.metadata.create_all(bind=engine)
@app.get("/user")
async def getAll(db: Session = Depends(get_db)):
return db.query(User).all()
@app.get("/user/{id}")
async def getOne(id: int, db: Session = Depends(get_db)):
return db.query(User).filter(User.id == id).first()
@app.post("/user/{name}/{gender}/{mobile}")
async def insert(name: str, gender: str, mobile: str, db: Session = Depends(get_db)):
user = User(name=name, gender=gender, mobile=mobile)
db.add(user)
db.commit()
db.refresh(user)
return {"message": "ok"}
@app.put("/user/{id}/{name}/{gender}/{mobile}")
async def update(id: int, name: str, gender: str, mobile: str, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == id).first()
user.name = name
user.gender = gender
user.mobile = mobile
db.commit()
return user
@app.delete("/user/{id}")
async def delete(id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == id).first()
db.delete(user)
db.commit()
return {"message": f"deleted id={id}"}
models.py
from sqlalchemy import Column, Integer, String
from database import Base
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True) # Auto_increment 기본 설정
name = Column(String(48), nullable=False)
gender = Column(String(6))
mobile = Column(String(20))
database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
DATABASE_URL = "mysql+pymysql://root:00000000@localhost:3306/kakao3"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
--------리액트-----------
Userlist.jsx
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import './mystyle.css'
axios.defaults.baseURL = "http://127.0.0.1:8000";
export default function UserList() {
const [users, setUsers] = useState([])
const navi = useNavigate()
const doUpdate = (e) => {
navi(`/write/${e.target.id}`)
}
const doDelete = async (e) => {
if (!confirm("정말로 삭제할까요?")) return
try {
await axios.delete(`/user/${e.target.id}`)
} catch (error) {
console.log(error)
}
doGet()
}
const doGet = async () => {
try {
const res = await axios.get('/user')
setUsers(res.data)
} catch (error) {
console.log(error)
}
}
useEffect(() => { doGet() }, [])
return (
<>
<button className='btn' onClick={() => navi("/write")}>추가</button>
<br /><br />
<table className="list-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Gender</th>
<th>Mobile</th>
<th>수정 / 삭제</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.gender}</td>
<td>{user.mobile}</td>
<td>
<button id={user.id} onClick={doUpdate}>수정</button>
<button id={user.id} onClick={doDelete}>삭제</button>
</td>
</tr>
))}
</tbody>
</table>
</>
)
}
Write.jsx
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import axios from 'axios'
import './mystyle.css'
export default function Write() {
const initValue = { name: '', gender: '', mobile: '' }
const [user, setUser] = useState(initValue)
const navi = useNavigate()
const { id } = useParams()
// -----------------------------
// 입력 변경
// -----------------------------
const doChange = (e) => {
const { name, value } = e.target
setUser(x => ({ ...x, [name]: value }))
}
// -----------------------------
// 등록
// -----------------------------
const doInsert = async () => {
if (user.name === '') {
alert("이름을 입력해야 합니다.")
return
}
try {
await axios.post(`/user/${user.name}/${user.gender}/${user.mobile}`)
alert("등록 완료!")
setUser(initValue)
navi("/user")
} catch (error) {
console.log(error)
}
}
// -----------------------------
// 수정
// -----------------------------
const doUpdate = async () => {
try {
await axios.put(`/user/${id}/${user.name}/${user.gender}/${user.mobile}`)
alert("수정 완료!")
navi("/user")
} catch (error) {
console.log(error)
}
}
// -----------------------------
// 조회 (수정 모드일 때만 사용)
// -----------------------------
const fetchUser = async (uid) => {
try {
const res = await axios.get(`/user/${uid}`)
setUser(res.data)
} catch (error) {
console.log(error)
}
}
// -----------------------------
// 화면 로딩 시 id 있으면 데이터 조회
// -----------------------------
useEffect(() => {
if (!id) return
// effect 내부에서 setState 사용 → ESLint 경고 방지용 async wrapper
(async () => {
await fetchUser(id)
})()
}, [id])
return (
<div>
<h2>{id ? "회원 수정" : "회원 등록"}</h2>
<table className="write-table">
<tbody>
<tr>
<td>Name</td>
<td>
<input
type="text"
name="name"
value={user.name}
onChange={doChange}
/>
</td>
</tr>
<tr>
<td>Gender</td>
<td>
<input
type="radio"
name="gender"
value="male"
checked={user.gender === 'male'}
onChange={doChange}
/> Male
<input
type="radio"
name="gender"
value="female"
checked={user.gender === 'female'}
onChange={doChange}
/> Female
</td>
</tr>
<tr>
<td>Mobile</td>
<td>
<input
type="text"
name="mobile"
value={user.mobile}
onChange={doChange}
/>
</td>
</tr>
<tr>
<td colSpan="2" style={{ textAlign: "center" }}>
{id
? <button onClick={doUpdate}>수정</button>
: <button onClick={doInsert}>등록</button>}
<button onClick={() => navi("/user")}>취소</button>
</td>
</tr>
</tbody>
</table>
</div>
)
}
App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import UserList from "./Userlist";
import Write from "./Write";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/user" element={<UserList />} />
<Route path="/write" element={<Write />} />
<Route path="/write/:id" element={<Write />} />
</Routes>
</BrowserRouter>
);
}

http://localhost:5173/user 화면

추가번튼을 누르면 이쪽으로 이동http://localhost:5173/write

등록하면 등록완료! 라고 메세지

삭제하면 정말로 삭제할까요! 메세지 나오고 확인 누르면

삭제된다

3번 훌리오 마르티네즈를 수정하고싶으면 수정버튼을 눌러서

수정칸에 들어가서 수정을하면

수정을 누르면 수정완료가 뜨고

수정된것을 볼수있다