"Object-Relational-Mapping"은 객체 지향 프로그래밍 언어를 사용하여 호환되지 않는 유형의 시스템 간에(Django - SQL) 데이터를 변환하는 프로그래밍 기술이다. 이것은 프로그래밍 언어에서 사용할 수 있는 '가상 객체 데이터베이스'를 만들어 사용한다.
우리는 DB를 객체(object)로 조작하기 위해 ORM을 사용한다.
from django.db import models
class Article(models.Model): # 장고에서 미리 정의한 Model을 상속받는다.
# ORM을 이용해서 id, title, content 컬럼을 가진 DB 모델을 생성해보자.
# id 컬럼은 자동으로 생성된다.
# CharField - 길이의 제한이 있는 varchar형 필드. (man_length가 필수 인자임)
title = models.CharField(max_length=10)
# TextField - 길이의 제한이 없는 text형 필드.
content = models.TextField()
# DateTimeFIeld - DateTime 기록 필드.
created_at = models.DateTimeField(
auto_now_add=True) # auto_now_add - 첫 저장 시 자동 기록
updated_at = models.DateTimeField(
auto_now=True) # auto_now - 저장될 때마다 자동 기록
makemigrationsmigratesqlmigrateshowmigrationsmakemigrationspython manage.py makemigrationsmigratepython manage.py migratesqlmigraatepython manage.py sqlmigrate 앱이름 마이그레이션번호python manage.py sqlmigrate articles 0001showmigrationspython manage.py showmigrationspython manage.py makemigrationspython manage.py migrateSQLite extension 설치 - django 폴더 내 db.sqlite3 파일을 우클릭하면 open database 선택지가 생성됨 - 왼쪽 EXPLOLER에 SQLITE EXPLOLER 탭이 생겨서 그 안에서 DB 테이블 조회 가능!
장고 내장 기본 셸은 python manage.py shell
쿼리 조작을 실습하기 위해서 django third-party 라이브러리 설치 - 셸을 실행할 때 DB API 조작을 위해 필요한 것들을 자동으로 import 해줘서 편리함
django-extensions 설치
pip install ipython - > cf) 파이썬 셸 업그레이드
pip install django-extensions → 장고 third-party shell extension
settings.py 안의 INSTALLED_APPS 에 추가
INSTALLED_APPS = (
...
'django_extensions',
)
python manage.py shell_plus로 설치된 extension 실행
아래에서 살펴볼 CRUD 코드들은
python manage.py shell_plus 를 통해 실행된 django third-party shell 환경 내에서 실행된 것들이다.
# 1. 빈 게시물 인스턴스 생성
a = Ariticle() # 게시물 인스턴스 생성
a.title = 'title1' # 게시물 title 필드 값 입력
a.content = 'content1' # 게시물 content 값 입력
a.save() # 게시물을 DB에 저장
a
# <Article: Article object (None)>
# save를 하기 전에는 아직 DB에 저장된 상태가 아니며, pk값이 주어지지 않아 None으로 나타난다.
# 2. 생성자를 이용해 필드에 값이 있는 게시물 인스턴스 생성
# django에서 제공하는 Model 클래스에 정의되어 있는 생성자를 이용해 인스턴스 생성하는 방법
b = Article(title='second', content'django!!')
b
# <Article: Article object (None)>
b.save()
b
# <Article: Article object (2)>
# 3. create 이용해서 인스턴스 생성 후 바로 저장
# 클래스.objects.create() -> 인스턴스를 생성하지 않고 저장하는 방법
Article.objects.create(title='third', content='django!!!')
# <Article: Article object (3)>
# 1. all - 모든 데이터 조회
Article.objects.all()
# <QuerySet [<Article: Article object (1)>, <Article: Article object (2)>, <Article: Article object (3)>]>
# 2. first - 가장 첫번째 데이터 조회
a = Article.objects.first() # articles_article 테이블의 첫 번째 레코드 read
a.title
# 'title1'
a.content
# 'content1'
a
# <Article: Article object (1)>
a.pk
# 1
a.id
# 1
# 3. get - unique한 값(pk)을 통해 조회할 떄 사용
b = Article.objects.get(pk=2)
b
# <Article: Article object (2)>
# 해당하는 객체가 없을 경우 에러 발생
# DoesNotExist: Article matching query does not exist.
# 해당하는 객체가 여러 개일 경우 에러 발생
# ultipleObjectsReturned: get() returned more than one Article -- it returned 2!
# 4. filter - 지정된 매개변수와 일치하는 객체를 모두 포함하는 'QuerySet' 반환
# (해당하는 레코드가 단 하나일 경우에도 QuerySet 형태로 반환됨에 주의!)
# 1번 레코드와 같은 content 값을 가지는 새로운 레코드 저장
# DB에 저장과 동시에, 생성된 인스턴스를 반환한다.
Article.objects.create(title='title4', content='django!')
# <Article: Article object (4)>
Article.objects.filter(content='django!')
# <QuerySet [<Article: Article object (1)>, <Article: Article object (4)>]>
# content 필드에 '!'를 포함하는 레코드 조회
Article.objects.filter(content__contains='!')
# <QuerySet [<Article: Article object (1)>, <Article: Article object (2)>, <Article: Article object (3)>, <Article: Article object (4)>]>
`
article = Article.objects.get(pk=1)
article
# <Article: Article object (1)>
article.title
# 'first'
article.title = 'byebye'
article.title
# 'byebye'
a.save() # Update (반드시 save해야 함. 그래야만 DB에 수정사항이 반영됨)
article = Article.objects.get(pk=1)
article.delete() # 삭제
# (1, {'articles.Article': 1})
article
# <Article: Article object (None)> pk가 사라져 None으로 표시되는 것을 확인할 수 있다.
Article.objects.get(pk=1) # 실제 DB에서 pk가 1인 레코드가 사라져 오류가 남
# DoesNotExist: Article matching query does not exist.
# 이후 새로운 레코드를 저장하면?
Article.objects.create(title='test', content='test!!!')
# <Article: Article object (5)>
# 삭제된 pk값(1)을 재사용 하지 않는다. pk값 5로 저장됨
articles 앱 폴더 내의 admin.py 파일에 다음과 같이 작성한다.
from django.contrib import admin
from .models import Article
# 'admin, site에, 등록한다' 로 외우자.
admin.site.register(Article)
콘솔에서 python manage.py createsuperuser 를 통해 admin 계정을 하나 생성한다.
만든 계정이 들어갈 테이블은 어디에?? - 프로젝트에서 처음 migrate 할 때 auth_user 테이블이 생성됨. (settings.py에서 INSTALLED_APP에 기본적으로 'django.contrib.auth' 앱이 등록되어 있기 때문에 가능)
굉장히 편리하게도, 장고는 기본적으로 관리자 페이지 기능을 제공한다.
이에 필요한 'django.contrib.admin' 도 프로젝트 생성 시 자동으로 INSTALLED_APP에 등록되어 있다.
서버를 실행하고 'adimn/' url로 접속하면 다음과 같은 관리자 페이지가 나타난다.
from django.contrib import admin
from .models import Article
class ArticleAdmin(admin.ModelAdmin):
list_display = ('pk', 'title', 'content', 'created_at', 'updated_at',)
# 'admin, site에, 등록한다' 로 외우자.
admin.site.register(Article, ArticleAdmin)
현재까지 배운 Template, View, Model의 개념을 연계하여 활용해보자.
from django.urls import path
from . import views
app_name = 'articles'
urlpatterns = [
path('index/', views.index, name='index'),
]
from django.db import models
# Create your models here.
class Article(models.Model):
title = models.CharField(max_length=10)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
from django.shortcuts import render
from .models import Article
# Create your views here.
def index(request):
# 모든 게시글을 조회
articles = Article.objects.all()
context = {
'articles': articles,
}
return render(request, 'index.html', context)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
{% extends 'base.html' %}
{% block content %}
<h1>INDEX</h1>
{% for article in articles %}
<p>글 번호: {{ article.pk }}</p>
<p>글 제목: {{ article.title }}</p>
<p>글 내용: {{ article.content }}</p>
<hr>
{% endfor%}
{% endblock %}
form에서 다음과 같이 GET 메소드를 사용하고 있을 때 사용자가 submit 버튼을 누르면 새로 간 주소 맨 뒤에 '?title=입력된제목&content=입력된내용'이 표시되는 것을 확인할 수 있다.
GET 방식은 사용자가 입력한 데이터를 url에 넣어서 보낸다.
<form action="{% url 'articles:create' %}" method="GET">
<div class="mb-3">
<label for="title" class="form-label">제목</label>
<input name="title" type="text" class="form-control" id="title" placeholder="제목을 입력해주세요">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea name="content" class="form-control" id="content" rows="3" placeholder="내용 입력"></textarea>
</div>
<input type="submit" class="btn btn-primary"></input>
</form>
하지만 form 태그에서 POST 메소드를 사용할 경우 아래와 같은 오류가 발생한다. (인증 실패)
CSRF 검증이 실패되었다고 나온다.
CSRF는 Cross-Site Request Fordery의 약자로, 사이트 간 요청 위조를 뜻한다.
장고에서는 이러한 공격을 막기 위해 믿을 수 있는 사이트에서 보낸 POST 요청만 처리할 수 있도록 클라이언트에게 csrf token이라는 것을 발급한다.
form 태그 안에 DTL을 활용하여 csrf_token을 발급하고 렌더링하면 form 태그 안에 hidden 타입을 가진 input 태그가 하나 생기는데, 이 안에 장고가 발급한 csrf_token이 들어있다. 이는 새로고침할 때마다 다른 값이 나타난다.
장고는 페이지를 요청받을 때마다 페이지에 각각 다른 도장을 찍어 보내는 것과 같다.
즉, 이 도장이 없거나 잘못된 도장을 가지고 있는 요청이 서버로 들어오게 될 경우 403 Forbiden 에러가 뜨면서 접근이 금지되는 것이다.