[pre_onboarding] Wantedlab 코드 분석

<개인 공부를 위한 블로깅... 가독성이 낮다.😮‍💨>

🍆 select_related()

-> select_related 은 foreign-key , one-to-one 와 같은 single-valued relationships에서만 사용이 가능.

a = Entry.objects.get(id=5)
# Hits the database.

b = a.blog
# Hits the database again to get the related Blog object.

-> a, b모두 데이터베이스를 건드린다.


a = Entry.objects.select_related('blog').get(id=5)
# Hits the database again to get the related Blog object.

b = a.blog
# Doesn't hit the database, because a.blog has been prepopulated
# in the previous query.

-> a에서 관련 블로그 객체를 가져오기위해 데이터 베이스를 건드렸고, b에서는 데이터베이스를 건드리지 않는다. 왜냐면 select_related를 통해 a가 참조하고 있는 대상을 caching. a.blog를 읽을 때 다시 db로 갈 필요 없이 caching되어있는 값을 읽어오니까 db 안건드림. 이게 되게 효율적인 것.

🔍 더 깊게 들어가면!

class City(models.Model):
  	name = models.CharField(max_length=20)
    
class Person(models.Model):
  	city = models.ForeignKey(City, on_delete=models.CASCADE)
      name = models.CharField(max_length=20)

class Pet(models.Model):
  	person = models.ForeignKey(Person, on_delete=models.CASCADE)
      name = models.CharField(max_length=20)
# Hits the database with joins to the person and city tables.
choon_sik = Pet.objects.select_related('person__city').get(id=1)
zunky = choon_sik.person         # Doesn't hit the database.
seoul = zunky.city       # Doesn't hit the database.
-> select_related를 통해 person과 city 테이블을 조인, database를 한번만 건들이게됨.

# Without select_related()...
choon_sik = Pet.objects.get(id=1)  # Hits the database.
zunky = choon_sik.person           # Hits the database.
seoul = zunky.city       # Hits the database

-> 3번을 데이터베이스를 건들이게 됨.

🍆 prefetch_related()

-> prefetch_related는 ManyToMany와 같은 경우 사용.

class City(models.Model):
  	name = models.CharField(max_length=20)

class Language(models.Model):
     name = models.CharField(max_length=100)
 
class Person(models.Model):
     city = models.ForeignKey(City, on_delete=models.CASCADE)
     name = models.CharField(max_length=20)
     language = models.ManyToManyField(Language, through='JoinPersonLanguage')
 
class JoinPersonLanguage(models.Model):
     person = models.ForeignKey(Person, on_delete=models.CASCADE)
     language = models.ForeignKey(Language, on_delete=models.CASCADE)
zunky = Person.objects.prefetch_related('language').get(id=1)
zunky.language.values() # Doesn't hit the database.
<QuerySet [{'id': 1, 'name': 'Python'}, {'id': 2, 'name': 'C'}, {'id': 4, 'name': 'Javascript'}]>
-> prefetch_related를 통해 zunky(person)에서 language를 참조해서 값을 가져올 때 database 안건드림.

zunky = Person.objects.get(id=1)
zunky.language.values() # Hits the database.
<QuerySet [{'id': 1, 'name': 'Python'}, {'id': 2, 'name': 'C'}, {'id': 4, 'name': 'Javascript'}]>
-> 그게 아니라면 database를 건드림.

🔍 역참조?

class City(models.Model):
     name = models.CharField(max_length=20)

class Language(models.Model):
     name = models.CharField(max_length=100)
 
class Person(models.Model):
     city = models.ForeignKey(City, on_delete=models.CASCADE)
     name = models.CharField(max_length=20)
     language = models.ManyToManyField(Language, through='JoinPersonLanguage')

class JoinPersonLanguage(models.Model):
     person = models.ForeignKey(Person, on_delete=models.CASCADE)
     language = models.ForeignKey(Language, on_delete=models.CASCADE)
>>> seoul = City.objects.get(id=1)
>>> seoul.person_set.values()
<QuerySet [{'id': 1, 'city_id': 1, 'name': 'zunky'}, {'id': 4, 'city_id': 1, 'name': 'byeong-min'}, {'id': 5, 'city_id': 1, 'name': 'sae-geul'}]>
-> 이런 경우 seoul이라는 City의 객체하나뿐이지만, 만약 모든 City 객체들에 역참조를 해야하는 상황이라면 person_set이라는 Query를 City의 수마다 DB에 요청하게 됨.

cities = City.objects.all()
for city in cities:
  print(city.person_set.values())
  
-> 위와 같이 명령을 실행한다면 City의 객체수 만큼 Query를 DB에 요청하게 되는 것.
City의 객체가 seoul, incheon, busan이라면 Query는 3번 요청.

-> 이런 상황일 때, prefetch_related()를 사용하면 Query를 줄일 수 있음.

cities = City.objects.prefetch_related('person_set').all()
for city in cities:
  print(city.person_set.values())

🍆 Models.py

class Company(models.Model):
    tags = models.ManyToManyField('Tag', through='CompanyTag', blank=True)
    
    class Meta:
        db_table = 'companies'

# ManyToManyField에서 Tag를 갖고 있다면 Company 테이블에는 실제로 id값 밖에 존재하지 않는다.
# Company에서 바로 tags로 연결이 가능하다. company.tags 이런식으로

class CompanyTag(models.Model):
    company = models.ForeignKey('Company', on_delete=models.CASCADE)
    tag     = models.ForeignKey('Tag', on_delete=models.CASCADE)

    class Meta:
        db_table = 'company_tags'

# 중간테이블로 작동하는 CompanyTag 테이블은 Company와 Tag를 ForeignKey로 물고 있다.
# 두 컬럼 모두 int 값을 갖고 있음

class Tag(models.Model):
    name = models.CharField(max_length=100)
    lang = models.CharField(max_length=100)
    
    class Meta:
        db_table = 'tags'

# Company와 ManyToManyField인 Tag 테이블은 따로 Company 컬럼을 갖고 있지않음.

class CompanyName(models.Model):
    name = models.CharField(max_length=100)
    lang = models.CharField(max_length=100)
    company = models.ForeignKey('Company', on_delete=models.CASCADE)
    
    class Meta:
        db_table = 'company_names'
        
# Company를 ForeignKey로 물고 있으며 바로 접근 가능.

🍆 Views.py

1. 회사명 자동완성:

-> /search?query="회사명"에서 query를 키워드로 사용 -> 해당 키워드가 포함된 회사명을 모두 찾음.
-> 찾은 회사명들을 이용해 다른 언어로 표현되는 같은 회사의 회사명들을 모두 찾기.
-> 이후에 x-wanted-language 헤더 값에 맞는 언어로 표현된 회사명들을 추려서 반환.

class CompanyView(View):
    def get(self, request):
        user_lang = request.headers.get('x-wanted-language', 'ko')
        # header에 들어온 x-wanted-language 받아주기, 안들어오면 ko가 기본값.
        keyword = request.GET.get('query', '')
		# 쿼리파라미터 사용해서 query로 들어온걸 keyword에 넣어주고 없으면 빈값처리.
        company_names = list(CompanyName.objects.filter(name__icontains=keyword))
		# 회사이름에 keyword가 대/소문자 형태로 포함되어있으면 필터링해줌 -> 리스트로 company_names에 넣기.
        company_ids = list(map(lambda company_name: company_name.company.id, company_names))
        # company_names라는 리스트에서 필터된 회사이름 하나씩 빼서company_name.id 값 찾아주고 걔네들을 리스트 형태로 company_ids 변수에 담는다.
        global_company_names = CompanyName.objects.filter(company_id__in=company_ids)
		company_ids에 company_id가 있는 애들 필터해서 global_company_names에 담기.
        results = [
            {'company_name': company_name.name}
            for company_name in global_company_names
            if company_name.lang == user_lang
        ]
        # if문으로 회사이름의 언어 조건걸어주고, 위에서 필터된 global_company_names를 for 문 돌려주기.
        return JsonResponse(data={'data': results}, status=200)

2. 회사명으로 회사 검색하기:

-> /companies/의 url에서 keyword에 회사이름을 입력하여 검색.
-> Header의 x-wanted-language로 ko(한국), en(영어), ja(일본) 등 언어를 선택하여 입력.
-> keyword에 입력된 회사이름과 header에서 선택한 언어를 토대로 회사이름과 태그를 출력할 수 있도록 구현.
-> 검색된 회사가 없는 경우 404를 리턴하고 에러메시지가 출력.

class SearchCompanyView(View):
    def get(self, request, keyword): 
    # keyword 인자를 받아줌 / 이렇게 들어가면 path paratmeter값으로 들어감(/companies/keyword)
        user_lang = request.headers.get('x-wanted-language', 'ko')
		# header에 들어오는 x-wanted-language의 값을 user_lang에 담음, 값 안들어오면 'ko'로.
        
        try:
            company_name = CompanyName.objects.get(name=keyword)
        # name=keyword인 CompanyName을 찾고 그걸 변수 company_name에 담아줌.
        
        except CompanyName.DoesNotExist:
            return JsonResponse({'message': 'company not found'}, status=404)
        # 존재하지않는 회사라면 CompanyName.DoesNotExist error를 띄워주기.
        
        else:
            user_company_name = CompanyName.objects.get(company_id=company_name.company.id, lang=user_lang)
        # keyword로 거른 company_name의 company.id와 같은 company_id를 찾고, lang은 user_lang으로 찾아서 get으로 하나를 걸러줌. 그걸 user_company_name에 담음.
        
            company_info = {
                'company_name': user_company_name.name,
                'tags': [tag.name for tag in user_company_name.company.tags.all() if tag.lang == user_lang]
            }
            # 최종적으로 company_name과 tags를 반환시켜줄건데,
            # user_company_name.company.tags.all()로 받고 for 문 돌려줘서 tag.name
            # 조건을 걸어주기 -> tag.lang이 user_lang과 같다면! tag에도 언어 걸어주기. 
            # company_id와 lang이 동일한 company의 이름을 반환.
            return JsonResponse(data=company_info, status=200)

좋은 웹페이지 즐겨찾기