왜 데이터베이스를 연결하나?
데이터베이스를 연결하면 매번 외부 url로 데이터를 불러오지 않고 데이터베이스로 데이터를 로드하여 기능을 효율적으로 수행하기 위해서
python3 manage.py sqlmigrate hottrack 0001_initial
# 0001_initial : 생성한 migration 파일
import json
from urllib.request import urlopen
from django.core.management import BaseCommand
from hottrack.models import Song
class Command(BaseCommand):
help = "Load songs from melon chart"
def handle(self, *args, **options):
melon_chart_url = "https://raw.githubusercontent.com/pyhub-kr/dump-data/main/melon/melon-20230910.json"
json_string = urlopen(melon_chart_url).read().decode("utf-8")
# Song 인스턴스들은 아직 데이터베이스에 저장되지 않았습니다.
song_list = [Song.from_dict(song_dict) for song_dict in json.loads(json_string)]
print("loaded song_list :", len(song_list))
# Song 인스턴스들은 한 번에 INSERT 쿼리를 생성하여, INSERT 성능을 높입니다.
Song.objects.bulk_create(song_list, batch_size=100, ignore_conflicts=True)
total = Song.objects.all().count()
print("saved song_list :", total)
ong_list: Song 모델의 인스턴스들이 들어있는 리스트. 인스턴스들은 데이터베이스에 저장되지 않은 상태.
batch_size=100: bulk_create는 대량의 데이터를 작은 배치로 나누어 처리하는 방식을 사용. 이 인자는 한 번 삽입되는 배치의 크기. 100개씩 작은 배치로 나누어 삽입.
ignore_conflicts=True: 중복된 레코드 무시 여부를. True는 설정하중복된 레코드가 있어도 진행.
대량의 데이터를 다룰 때 일반적으로 bulk_create를 사용하여 여러 개의 쿼리를 수행하는 것보다 빠르고 효율적입니다.
여러 개의 쿼리를 각각 실행하는 것이 아닌, bulk_create로 한 번의 쿼리로 여러 개의 레코드를 삽입할 때의 장점
데이터베이스와의 통신 횟수 감소:
여러 개의 레코드를 각각 추가할 때마다 데이터베이스와의 통신이 발생.
네트워크 지연을 줄이고 성능을 향상시킬 수 있습니다.
트랜잭션 오버헤드 감소:
트랜잭션은 여러 쿼리를 하나의 논리적 작업 단위로 묶는데, 이를 줄이면 트랜잭션 관련 오버헤드가 감소하게 됩니다.
인덱스 업데이트 횟수 감소:
한 번 쿼리로 여러 개의 레코드를 추가할 때, 인덱스의 업데이트가 한 번에 이루어지므로 인덱스 업데이트 횟수가 감소합니다.
def index(request: HttpRequest) -> HttpResponse:
query = request.GET.get("query", "").strip()
song_qs: QuerySet[Song] = Song.objects.all()
# 외부 url 요청이 아닌 연결된 데이터베이스 안의 데이터를 가져온다
if query:
song_qs = song_qs.filter(
Q(name__icontains=query)
| Q(artist_name__icontains=query)
| Q(album_name__icontains=query)
) # 쿼리 기준에 맞는 데이터를 filter()와 Q를 실행하여 song_qs에 담는다.
return render(
request=request,
template_name="hottrack/index.html",
context={"song_list": song_qs, "query": query},
) # song_qs를 "song_list" key에 담아 html과 render한다.
왜 admin을 사용하나?
개발 공수를 줄이고 유저서비스에 집중할 수 있으며 model에 대한 조회/ 생성/ 삭제/ 수정 UI를 제공하주기 때문에 쉽고 편하게 model에 대한 CRUD가 가능하다.
python3 manage.py createsuperuser
from django.contrib import admin
from django.utils.html import format_html
from .models import Song
@admin.register(Song)
class SongAdmin(admin.ModelAdmin):
list_display = [
"name",
"artist_name",
"album_name",
"genre",
"like_count",
"release_date",
"cover_image",
]
def cover_image(self, song): # 해당 행에대한 song model instance를 넘겨준다.
return format_html('<img src="{}" style="width: 50px;" />', song.cover_url)
# format_html() 장고 내에서 html 태그문자열을 조합하는 안전한 방법.
# 첫번째 인자는 html 문법(값이 들어갈 영역)
# 두번째 사용 값
# model.py에서 admin 필드값 추가하기
class Song(models.Model):
melon_uid = models.CharField(max_length=20, unique=True)
rank = models.PositiveSmallIntegerField()
album_name = models.CharField(max_length=100)
name = models.CharField(max_length=100)
artist_name = models.CharField(max_length=100)
cover_url = models.URLField()
lyrics = models.TextField()
genre = models.CharField(max_length=100)
release_date = models.DateField()
like_count = models.PositiveIntegerField()
# PositiveIntegerField() : 좋아요 필드의 특성상 항상 0이상 이어야한다는 설정
@property
def cover_image_tag(self):
return format_html("<img src='{}' style='width:50px;", self.cover_url)
# 이후 필히 admin list_display에서도 cover_image_tag필드 추가.
@admin.register(Song)
class SongAdmin(admin.ModelAdmin):
search_fields = ["name", "artist_name", "album_name"]
list_filter = ["genre"]
검색창에 default delete action 외에 사용자 정의 action을 추가하는 과정
# admin에 사용자 정의 action 옵션 추가
@admin.register(Song)
class SongAdmin(admin.ModelAdmin):
actions = ["update_like_count"]
########################################################################
# 사용자 정의 action 함수 작성
def update_like_count(self, request, queryset):
melon_uid_list = queryset.values_list(
"melon_uid", flat=True
) # queryset = Song.objects.all()(admin을 통해 song 모델에 접속 후 검색창에서 기능 실행함)에서 melon_uid lsit에 접근하여 가져온다.
likes_dict = get_likes_dict(
melon_uid_list
) # melon.py get_likes_dict 새로운 좋아요 정보를 받아옴
changed_count = 0 # 좋아요를 업데이트한 곡 메세지로 출력하기
for song in queryset:
if (
song.like_count != likes_dict[song.melon_uid]
): # 기존 좋아요 수와 새로운 좋아요 수가 다르다면
song.like_count = likes_dict.get(
song.melon_uid
) # # 새로운 데이터로 like count 값을 없데이트
changed_count += 1 # 좋아요를 업데이트 한 곡에 추가
Song.objects.bulk_update(queryset, ["like_count"])
# queryset은 for문을 통해 업데이트 된 상태
# 두번째 인자는 업데이트 할 필드
self.message_user(request, f"{changed_count}곡의 쪼아요 갱신 완료")
# 참고
역직렬화 시 객체의 type은 dict
result = json.loads(urlopen(request).read())
result = {'contsLike': [{'CONTSID': 35667692, 'LIKEYN': 'N', 'SUMMCNT': 23798}, {'CONTSID': 36105548, 'LIK': 'N', 'SUMMCNT': 108942}], 'httpDomain': 'http://www.melon.com', 'httpsDomain': 'https://www.melon.com', 'staticDomain': 'https://static.melon.co.kr'}
# queyset의 type
<QuerySet [<Song: Song object (89)>, <Song: Song object (88)>, <Song: Song object (86)>]>
# 이후 query set을 for에 변수로 담아 object.field 형식으로 value에 접근한다.
# song.save : 모든 모델 필드에 대해서 업데이트를 수행, 필요가 아닌 경우 쿼리 수행이 복잡해진다.
# song.save(udate_fields=["like_count"]) : 지정 필드에 대해서 업데이트 수행
# 필요필드만 업데이트 하는 것을 권장.