루나의 TIL 기술 블로그

장고 CRUD2 주인과 강아지

|

위코드에서 CRUD 두번째 세션을 마치고 과제인 주인과 강아지를 하면서 진행 과정을 써보았다.

장고 CRUD2 과제 주인과 강아지

주인과 강아지 ERD

POST

각 기능을 서로 다른 클래스로 구현해주세요.

  1. 신규 주인 등록
  2. 강아지 등록 (주인정보 필요)

위 기능을 구현 후 직접 httpie 를 활용하여 주인 2명의 정보와 각 주인 당 2~3마리의 강아지 정보를 데이터베이스에 저장해주세요.

GET

각 기능을 서로 다른 클래스로 구현해주세요.

  1. 주인 리스트
    • 이름, 이메일, 나이 포함
  2. 강아지 리스트
    • 이름, 나이, 주인 이름 포함
  3. 주인 리스트 (1번 코드에 추가)
    • 이름, 나이 포함, 키우는 강아지 리스트 (이름, 나이 포함)

진행 과정

초기 세팅 진행

  • Miniconda 가상환경 생성 및 가상환경 activate
    conda create -n dog python=3.8
    conda activate dog
    
  • Database 생성
    mysql -u root -p #마이sql시작
    
    mysql> create database dog character set utf8mb4 collate utf8mb4_general_ci;
    
  • 프로젝트 시작을 위한 python package 설치
    pip install Django #장고 설치
    pip install mysqlclient # mysqlclient설치 (mysql 먼저 설치 필요!!!)
    pip install django-cors-headers #CORS 해결을 위한 패키지
    
  • 장고 프로젝트 생성
    django-admin startproject dog_owner #프로젝트 생성
    cd dog #프로젝트 디렉토리로 들어감
    python manage.py startapp dog #앱 생성(manage.py가 존재하는 디렉토리에서)
    

프로젝트 폴더(dog_owner)의 settings.py 설정

setting.py파일 펼치기/접기
from pathlib        import Path #기존에 settings.py 에 있는 코드
from my_settings   import DATABASES, SECRET_KEY

SECRET_KEY = SECRET_KEY
DATABASES = DATABASES
ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
    # 'django.contrib.admin', #admin도 
    # 'django.contrib.auth', #login auth도 직접 만들어쓸 예정이다
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders', # 추가
    'products', #새로 만든 앱 이름 추가
]
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    # 'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware', #추가함
]
##CORS
CORS_ORIGIN_ALLOW_ALL=True
CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_METHODS = (
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
)

CORS_ALLOW_HEADERS = (
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
	#만약 허용해야할 추가적인 헤더키가 있다면?(사용자정의 키) 여기에 추가하면 됩니다.
)

위와 같이 settings.py를 추가 및 수정한다. 미들웨어에서 csrf와 auth를 주석으로 만들었어야되는데 잘못해서 그 전 줄을 주석화했다가
RuntimeError: Model class django.contrib.auth.models.Permission doesn’t declare an explicit app_label and isn’t in an application in INSTALLED_APPS.
에러가 계속 났었다. 수업을 들으면서 어떤 코드를 쓰거나 지울 때 그걸 왜 쓰는지 알고 써야될 것 같다.

my_setting.py 파일 추가

시크릿 키와 디비정보가 깃에 공개적으로 노출되지 않도록 빼기 위해서
manage.py있는 디렉토리에 my_setting.py라는 새 파일을 만들어서 추가한다.

#클라우드에 올리지 않는, 키를 보관하는 파일
DATABASES = {
    'default' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'dog',
        'USER': 'root',
        'PASSWORD': '',
        'HOST': '127.0.0.1', #데이터베이스의 IP주소, 이건 각자의 컴퓨터
        'PORT': '3306',
    }
}
SECRET_KEY ='_9^=58g2r*r(6@q7ugn!fxg-!fo48$7af9i4yn9-1$+...'
#setting.py에 있던 시크릿 키를 붙여넣음

urls.py수정

from django.urls import path

urlpatterns = [
]

gitignore추가 (manage.py가 존재하는 디렉토리에)

git ignore 펼쳐서 보기/접기

# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,vim,macos,linux,zsh
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,visualstudiocode,vim,macos,linux,zsh

### Linux

\*~

# temporary files which can be created if a process still has a handle open of a deleted file

.fuse_hidden\*

# KDE directory preferences

.directory

# Linux trash folder which might appear on any partition or disk

.Trash-\*

# .nfs files are created when an open file is removed but is still being accessed

.nfs\*

### macOS

# General

.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r

Icon

# Thumbnails

.\_\*

# Files that might appear in the root of a volume

.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share

.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### PyCharm

# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider

# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff

.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/\*\*/shelf

# AWS User-specific

.idea/\*\*/aws.xml

# Generated files

.idea/\*\*/contentModel.xml

# Sensitive or high-churn files

.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/\*\*/dbnavigator.xml

# Gradle

.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import

# When using Gradle or Maven with auto-import, you should exclude module files,

# since they will be recreated, and may cause churn. Uncomment if using

# auto-import.

# .idea/artifacts

# .idea/compiler.xml

# .idea/jarRepositories.xml

# .idea/modules.xml

# .idea/\*.iml

# .idea/modules

# \*.iml

# \*.ipr

# CMake

cmake-build-\*/

# Mongo Explorer plugin

.idea/\*\*/mongoSettings.xml

# File-based project format

\*.iws

# IntelliJ

out/

# mpeltonen/sbt-idea plugin

.idea_modules/

# JIRA plugin

atlassian-ide-plugin.xml

# Cursive Clojure plugin

.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)

com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client

.idea/httpRequests

# Android studio 3.1+ serialized cache file

.idea/caches/build_file_checksums.ser

### PyCharm Patch

# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# \*.iml

# modules.xml

# .idea/misc.xml

# \*.ipr

# Sonarlint plugin

# https://plugins.jetbrains.com/plugin/7973-sonarlint

.idea/\*\*/sonarlint/

# SonarQube Plugin

# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin

.idea/\*\*/sonarIssues.xml

# Markdown Navigator plugin

# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced

.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/\*\*/markdown-navigator/

# Cache file creation bug

# See https://youtrack.jetbrains.com/issue/JBR-2257

.idea/$CACHE_FILE$

# CodeStream plugin

# https://plugins.jetbrains.com/plugin/12206-codestream

.idea/codestream.xml

### Python

# Byte-compiled / optimized / DLL files

**pycache**/
_.py[cod]
_$py.class

# C extensions

\*.so

# Distribution / packaging

.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
_.egg-info/
.installed.cfg
_.egg
MANIFEST

# PyInstaller

# Usually these files are written by a python script from a template

# before PyInstaller builds the exe, so as to inject date/other infos into it.

_.manifest
_.spec

# Installer logs

pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports

htmlcov/
.tox/
.nox/
.coverage
.coverage._
.cache
nosetests.xml
coverage.xml
_.cover
\*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations

_.mo
_.pot

# Django stuff:

\*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:

instance/
.webassets-cache

# Scrapy stuff:

.scrapy

# Sphinx documentation

docs/\_build/

# PyBuilder

.pybuilder/
target/

# Jupyter Notebook

.ipynb_checkpoints

# IPython

profile_default/
ipython_config.py

# pyenv

# For a library or package, you might want to ignore these files since the code is

# intended to run in multiple environments; otherwise, check them in:

# .python-version

# pipenv

# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

# However, in case of collaboration, if having platform-specific dependencies or dependencies

# having no cross-platform support, pipenv may install dependencies that don't work, or not

# install all needed dependencies.

#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow

**pypackages**/

# Celery stuff

celerybeat-schedule
celerybeat.pid

# SageMath parsed files

\*.sage.py

# Environments

.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings

.spyderproject
.spyproject

# Rope project settings

.ropeproject

# mkdocs documentation

/site

# mypy

.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker

.pyre/

# pytype static type analyzer

.pytype/

# Cython debug symbols

cython_debug/

### Vim

# Swap

[._]_.s[a-v][a-z]
!_.svg # comment out if you don't need vector files
[._]\*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]

# Session

Session.vim
Sessionx.vim

# Temporary

.netrwhist

# Auto-generated tag files

tags

# Persistent undo

[._]\*.un~

### VisualStudioCode

.vscode/_
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
_.code-workspace

# Local History for Visual Studio Code

.history/

### VisualStudioCode Patch

# Ignore all local history of files

.history
.ionide

### Zsh

# Zsh compiled script + zrecompile backup

_.zwc
_.zwc.old

# Zsh completion-optimization dumpfile

_zcompdump_

# Zsh zcalc history

.zcalc_history

# A popular plugin manager's files

.\_zinit
.zinit_lstupd

# zdharma/zshelldoc tool's files

zsdoc/data

# robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files

# (when set-up to store the history in the local directory)

.directory_history

# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files

# (for Zsh plugins using Python)

# Zunit tests' output

/tests/\_output/\*
!/tests/\_output/.gitkeep

# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,vim,macos,linux,zsh

#보안관련파일과 크롤링파일
my_setting.py
*.csv 

실행

pythong manage.py runserver

migration을 위한 models.py작성

from django.db import models

class Owner(models.Model):
    name = models.CharField(max_length=45)
    email = models.EmailField(max_length=254)
    age = models.BigIntegerField
    class Meta: 
        db_table = 'owners' 
    def __str__(self):
        return self.name

class Dog(models.Model):
    name = models.CharField(max_length=45)
    owner = models.ForeignKey('Owner', on_delete=models.CASCADE, default='')
    age = models.BigIntegerField
    class Meta:
        db_table='dogs'  
    def __str__(self):
        return self.name

migration 만들고 migrate 실행하기

python manage.py makemigrations 
#모델의 변경사항을 파악, 설계도 작성(하고나서 만들어진 파일을 잘 살펴본다)
python manage.py showmigrations
#현재 migrations가 어떤 상태인지 살펴보기
python manage.py sqlmigrate dog 0001
#실제 데이터베이스에 전달되는 SQL 쿼리문을 확인
python manage.py migrate 
#자동으로 migration을 실행

sqlmigrate했을 때 나오는 구문은 아래와 같다.

BEGIN;
--
-- Create model Owner
--
CREATE TABLE "owners" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "name" varchar(45) NOT NULL, 
    "email" varchar(254) NOT NULL
    );
--
-- Create model Dog
--
CREATE TABLE "dogs" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    "name" varchar(45) NOT NULL, 
    "owner_id" bigint NOT NULL REFERENCES "owners" ("id") DEFERRABLE INITIALLY DEFERRED
    );
CREATE INDEX "dogs_owner_id_b381d414" ON "dogs" ("owner_id");
COMMIT;

DEFERRABLE INITIALLY DEFERRED는 제약조건을 커밋할 때까지 연기하는거라고 하는데 아직 몰라도 될 것 같다.

에러

장고에는 원래 sqllite3가 있는데 위코드 커리큘럼에서 이걸 제거하고 mysql에 연결해주는 과정이 있었다. 그걸 하면서 settings.py에서 아래 코드를 지우고 DATABASES = DATABASES로 바꿨었는데 dog 장고앱을 만들면서 아래부분을 안 지웠더니 마이그레이션 이후에도 계속 mysql에 테이블이 생기지 않는 에러가 나서 2시간정도 고생하다가 다행히 학우분께서 이걸 찾아주셨다.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
#settings.py에서 이걸 지워야된다

쿼리셋을 이용한 shell에서의 CREATE

python manage.py shell 
>>>from dog.models import Dog, Owner
>>> Owner.objects.create(name='명주', email='abc@gmail.com')
<Owner: 명주>
>>> Owner.objects.create(name='경훈', email='123@gmail.com')
<Owner: 경훈>
>>> Owner.objects.create(name='은영', email='567@gmail.com')
<Owner: 은영>
>>> Dog.objects.create(name = '삼월', owner_id=1)
<Dog: 삼월>
>>> Dog.objects.create(name = '허순이', owner_id=2)
<Dog: 허순이>
>>> Dog.objects.create(name = '맹구', owner_id=2)
<Dog: 맹구>

dog 테이블

쿼리셋을 이용한 shell에서의 READ

>>> Dog.objects.all()
<QuerySet [<Dog: 삼월>, <Dog: 허순이>, <Dog: 맹구>]>
>>> Owner.objects.values()
<QuerySet [{'id': 1, 'name': '명주', 'email': 'abc@gmail.com'}, {'id': 2, 'name': '경훈', 'email': '123@gmail.com'}, {'id': 3, 'name': '은영', 'email': '567@gmail.com'}]>
>>> Dog.objects.values_list()
<QuerySet [(1, '삼월', 1), (2, '허순이', 2), (3, '맹구', 2)]>

CRUD2

brew install httpie

httpie는 파이썬으로 개발된 http 클라이언트 유틸리티로 개발 및 디버깅 용도로 사용 가능하다.
curl에 비해서 사용이 쉽고 가독성이 좋다.

View 작성하기

READ를 위한 get method

# dog>views.py
import json

from django.http import JsonResponse
from django.views import View

from dog.models import Dog, Owner
class DogView(View):
    def get(self, request):
        dogs = Dog.objects.all()
        results=[]
        for dog in dogs:
            results.append(
            {
                "name" : dog.name,
                "owner" : dog.owner.name
            }
        )
        return JsonResponse({'result':results}, status=200)

class OwnerView(View):
    def get(self, request):
        owners = Owner.objects.all()
        results=[]
        for owner in owners:
            results.append(
            {
                "name" : owner.name,
                "email" : owner.email,
                "dogs" : owner.dog.name
            }
        )
        return JsonResponse({'result':results}, status=200)

client로 부터 데이터 요청을 받으면 우선 제일 부모 폴더에 있는 urls.py로 받는다.

#dog_owner(상위 프로젝트 폴더) > urls.py
#상위 프로젝트 폴더 dog_owner가 dog앱 안의 urls을 바라보게 만들어줌
from django.urls import path, include

urlpatterns = [
    path('dog', include('dog.urls'))
]

여기가 부모 urls.py 이다. 여기는 server로 오는 요청을 경로를 처리하는 파일이라고 생각하면 된다.

localhost:8000/dog

위의 경로로 dog app을 사용하겠다는 요청이 오면 다시 상세 app으로 보내주는 역할을 한다.
그 역할을 하는 것이 바로 include 이다. 그러면 그 해당 app 의 urls.py로 보내준다.
근데 처음 startapp 으로 app 을 생성했을 때 urls.py는 존재하지 않는다. 그렇기 때문에 dog app 안에 새로 생성해줘여한다.

#dog(하위 앱 폴더, urls.py새로 만들어주어야함) > urls.py
#하위 앱 폴더 dog에서 DogView를 연결
from django.urls   import path
from dog.views     import OwnerView, DogView
#OwnerView, DogView 는 view.py로부터 get, post 기능을 수행하는 클래스를 import해온다.

urlpatterns = [
    path('/dog', DogView.as_view()),
    path('/owner', OwnerView.as_view()),
    #as_view는 해당 경로까지 온 요청이 어떤 method인지 판단해주는 함수이다.
]

둘을 바꿔 넣어서 에러가 났었는데 동료분께서 찾아주셨다..!!

#url의 위치
.
├── manage.py
├── products
│   ├── models.py
│   ├── urls.py (없음, 새로 생성해야함)
│   └── views.py
└── westarbucks
    └── urls.py : main urls.py (부모, 요청 url 분석을 가장 먼저 하는 위치)

CREATE를 위한 post method

POST방식으로 개와 주인 데이터를 추가해보자.

# dog>views.py
import json

from django.http import JsonResponse
from django.views import View

from .models import Dog, Owner

class OwnerView(View):
    def post(self, request):
        data  = json.loads(request.body)
        # view는 받아온 json 데이터를 template의 html 파일로 전달하고 
        # 템플릿으로 전달된 json 데이터는 자바스크립트에 의해 활용할 수 있는 
        # 형태로 다시 받아온다. 자바스크립트로 json데이터를 자유롭게 사용할 수 있다.
        name = data['name']
        # 받아온 json데이터 중 변수 name의 키값을 name변수에 저장한다 
        email= data['email']
        Owner.objects.create(name = name, email=email)        
        # Owner모델을 이용해서 받아온 name과 email의 값을 가진 데이터를 추가한다.
        return JsonResponse({'Message':'Created'}, status=201)

    def get(self, request):
        owners = Owner.objects.all()
        results=[]
        for owner in owners:
            dogs = 
            [
                {"이름":dog.name} for dog in Dog.objects.filter(owner_id=owner.id)
            ]
            # for문부터 읽어서, model에서 owner의 포린키 아이디(owner_id)가 
            # 오너의 아이디(owner.id)와 일치하는 개들을 filter로 불러와서 
            # "이름": 값 형태로 dogs라는 변수에 저장한다.
            results.append(
            {
                "name" : owner.name,
                "email" : owner.email,
                "dog_list" : dogs
            }
        )
        return JsonResponse({'result':results}, status=200)

class DogView(View):
    def post(self, request):
        data = json.loads(request.body)
        # owner = Owner.objects.get(name = data['owner']) 이렇게 해서 owner = owner라고 써줘도 됨
        # 프론트에서 준 데이터인 오너의 이름을 가진 데이터들을 불러와서 owner변수에 저장
        dog = Dog.objects.create(
            name = data['name'], 
            owner_id = data['owner_id']
        )
        # Dog모델을 이용해서 받아온 name과 owner의 값을 가진 데이터를 추가한다.

            if not Owner.objects.filter(id=data["owner_id"].exists():
                return JsonResponse({"message": "Owner_does_not_exist", status=404})
            
        return JsonResponse({'Message':'Created'}, status=201)

    def get(self, request):
        dogs = Dog.objects.all()
        results=[]
        for dog in dogs:
            results.append(
            {
                "name" : dog.name,
                "owner" : dog.owner.name
            }
        )
        return JsonResponse({'result':results}, status=200)

/dog/dog 해야되는걸 모르고 한시간정도 헤메였는데 한성봉님의 블로그를 보고 알게되었다. 한성봉님의 블로그

POST 로 주인과 강아지 추가

http -v POST 127.0.0.1:8000/dog/dog name="땅콩" owner="경훈"

owner=”경훈” 이렇게 이름으로 찾아도 되지만 owner_id=3 해서 pk으로 주고받는것이 정석이다. 주인과 강아지 추가

GET으로 데이터베이스 JSON파일 형식으로 출력

http -v GET 127.0.0.1:8000/dog/owner 

전체 디비 출력

주요 포인트 및 생각해볼 점

이 부분은 나는 다른 분 블로그를 보고 안 썼으면 정말 몰랐을 것 같다.

dogs = [{"이름":dog.name} for dog in Dog.objects.filter(owner_id=owner.id)]