Đây là bài tóm tắt cách sử dụng django rest framework để viết REST Web Service API. Trong bài này bạn sẽ học cách quản lý danh sách nhà hàng và các chi nhánh của nhà hàng. API sẽ được sẽ là:
GET /restapp/restaurant # danh sách nhà hàng POST /restapp/restaurant # thêm nhà hàng mới PATCH /restapp/restaurant # update thông tin nhà hàng GET /restapp/branch # danh sách các chi nhánh GET /restapp/branch?restaurant={id} # danh sách chi nhánh theo id của nhà hàng POST /restapp/branch #thêm chi nhánh mới PATCH /restapp/branch # update thông tin chi nhánh
Hai website tài liệu chính được sử dụng cho bài học này là: Django Rest, Django Queryset
Bạn nên sử dụng POSTMAN để test các api mình làm.
Dùng python 3.7
Bạ nên dùng tối thiểu python 3.7. Bài viết này hỗ trợ chủ yếu cho các bạn dùng Linux (Ubuntu…) hay Mac OS X.
Mở Terminal và kiểm tra version của python
python3 --version # bạn cũng có thể dùng rõ phiên bản: python3.7 --version
Cài đặt Django, rest framework và module mysqlclient
Dùng lệnh sau để cài đặt django qua Terminal:
python3.7 -m pip install Django
Sau đó cài mysqlclient để kết nối django với mysql database. Tiếp đó, cài đặt rest framework:
#Ubuntu sudo python3.7 -m pip install mysql-connector sudo apt-get install python3.7-dev libmysqlclient-dev python3.7 -m pip install mysqlclient python3.7 -m pip install djangorestframework
Tạo dự án
Khởi tạo dự án :
django-admin startproject djangoresttraining
Tạo cơ sở dữ liệu trên mysql mang tên “djangoresttraining” và import cấu trúc sau:
CREATE TABLE `restapp_branches` ( `id` int(11) NOT NULL, `branch_name` varchar(255) NOT NULL, `restaurant_id` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `restapp_restaurants` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE `restapp_branches` ADD PRIMARY KEY (`id`), ADD KEY `restaurant_id` (`restaurant_id`); ALTER TABLE `restapp_restaurants` ADD PRIMARY KEY (`id`); ALTER TABLE `restapp_branches` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; ALTER TABLE `restapp_restaurants` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; ALTER TABLE `restapp_branches` ADD CONSTRAINT `restapp_branches_ibfk_1` FOREIGN KEY (`restaurant_id`) REFERENCES `restapp_restaurants` (`id`);
Tiếp đó, bạn config để django có thể kết nối vào mysql database của bạn. Update file djangoresttraining/settings.py:
# djangoresttraining/settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'djangoresttraining', 'USER': 'root', 'PASSWORD': 'root', 'HOST': 'localhost', # hoặc 127.0.0.1 hoặc domain/ip của server của bạn 'PORT': '3636', # hoặc port của bạn } }
Sau đó vào thư mục của dự án và tao restapp nơi sẽ chứa REST API code của bài học này. Một project của django có thể có nhiều app khác nhau (như nhiều module khác nhau)
cd djangoresttraining # them app vao du an python3.7 manage.py startapp restapp # start thử server. python3.7 manage.py runserver # Nếu thành công, bạn sẽ thấy thông điệp như sau: Django version 3.1, using settings 'driver_app.settings' Starting development server at http://127.0.0.1:2000/ Quit the server with CONTROL-C.
Thêm restapp vào dự án:
#djangoresttraining/settings.py: INSTALLED_APPS = [... ..., 'rest_framework', 'restapp', ]
Update djangoresttraining/urls.py:
from django.urls import path, include, re_path ... urlpatterns = [ path('admin/', admin.site.urls), path('restapp/', include('restapp.urls')), # sử dụng file restapp/urls.py khi path được gọi bắt đầu bởi "restapp/" ]
Tạo restapp/urls.py
from django.urls import path from django.conf.urls import url from . import views urlpatterns = [ path('', views.RestaurantView.as_view(), name='restaurant') ]
Tạo model trong restapp/models.py
from django.db import models # Tên class cần giống với tên bảng trong cơ sở dữ liệu class Restaurants(models.Model): name = models.CharField(max_length=255) # giống tên cột class Branches(models.Model): branch_name = models.CharField(max_length=255) # giống tên cột restaurant = models.ForeignKey(Restaurants, on_delete=models.CASCADE) # bỏ "_id" khỏi tên cột nếu là foreign key. Django sẽ tự thêm "_id" khi truy vấn.
Tạo restapp/serializers.py
File serializers giúp convert data từ model thành json và ngược lại. Serializers cũng có nhiều tiện ích cho việc save/update data:
from rest_framework import serializers from . import models # 1 serializer class duy nhất cho restaurant class RestaurantsSerializer(serializers.ModelSerializer): class Meta: model = models.Restaurants fields = '__all__' # Riêng đối với branch, chúng ta sẽ có 2 serializer khác nhau # cho việc view vì sẽ có thêm thông tin restaurant kèm theo (chỉ để read_only) class BranchesViewSerializer(serializers.ModelSerializer): restaurant = RestaurantsSerializer(read_only=True) class Meta: model = models.Branches fields = '__all__' # Serializer cho branch phục vụ cho các thao tác create, update, delete class BranchesSerializer(serializers.ModelSerializer): class Meta: model = models.Branches fields = '__all__'
Tạo views cho API của restaurant
Views là cách mà django rest sẽ trả về kết quả json cho các api. Trong view, chúng ta cũng sẽ dùng class Response của django rest để trả về kết quả theo format bao gồm data và status code.
#restapp/views.py from django.shortcuts import render from . import models from . import serializers from rest_framework.response import Response # Create your views here. from rest_framework import generics, permissions, status class RestaurantView(generics.GenericAPIView): # nhớ cần khai báo: permission_classes, queryset và serializer_class # để đầy đủ điều kiện triển khai của GenericAPIView permission_classes = (permissions.AllowAny,) queryset = models.Restaurants.objects.all() serializer_class = serializers.RestaurantsSerializer # khi gọi api bằng method GET để lấy record, django sẽ gọi hàm get() dưới đây để xử lý def get(self, request): myqueryset = models.Restaurants.objects.all() # chú ý để many=True vì kết quả sẽ là dạng list các model list_serializer = serializers.RestaurantsSerializer(myqueryset, many=True) # sử dụng Response class của django rest return Response(list_serializer.data, status=status.HTTP_200_OK) # khi gọi api bằng method POST để tạo record mới, django sẽ gọi hàm post() dưới đây để xử lý def post(self, request, *arg, **kwargs): try: # lưu ý dùng partial=True để chuỗi json đầu vào có thể có 1 # hoặc nhiều field của restaurants (nếu có). # Nếu không có partial=True, # django rest sẽ đòi hỏi phải truyền vào tất cả các field của bảng restaurant (ngoại trừ id) obj_serializer = serializers.RestaurantsSerializer(data=request.data, partial=True) # kiểm tra xem json có hợp lệ không? if obj_serializer.is_valid(): obj_serializer.save() # save dữ liệu return Response(obj_serializer.data, status=status.HTTP_201_CREATED) except Exception as ex: return Response(ex.args, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Dùng postman để kiểm tra kết quả của API:
Tạo View cho API của Branch
Tương tự như restaurant, bạn có thể tạo api để lấy danh sách các branch như sau:
# cùng trong restapp/views.py class BranchView(generics.GenericAPIView): permission_classes = (permissions.AllowAny,) queryset = models.Branches.objects.all() serializer_class = serializers.BranchesViewSerializer def get(self, request): myqueryset = models.Branches.objects.all() # Chú ý dùng BranchesViewSerializer để có được data của toàn bộ record restaurant (vì BranchesViewSerializer có khai báo quan hệ với forgeinkey restaurant_id) list_serializer = serializers.BranchesViewSerializer(myqueryset, many=True) return Response(list_serializer.data, status=status.HTTP_200_OK)
Điểm chú ý đặc biệt ở trên là bạn cần dùng BranhcesViewSerializer chứ không phải BranchesSerializer. Vì BranchesViewSerializer đã thiết lập thêm quan hệ với restaurant, và sẽ cho phép kết quả json trả về bao gồm cả thông tin của bảng restaurant. BranchesViewSerializer chỉ dùng cho các api liên quan đến xem dữ liêu (không update/create/delete).
Tiếp theo, update urls.py để tách api của restaurant và branch thành 2 nhóm endpoints (paths) khác nhau, không gây ra sự nhầm lẫn.
# restapp/urls.py
urlpatterns = [ path('restaurant', views.RestaurantView.as_view(), name='restaurant'), path('branch', views.BranchView.as_view(), name='branch') ]
API tạo branch mới
Để thêm api tạo mới 1 branch, chúng ta viết thêm hàm post trong view của branch.
Tuy nhiên serializer sẽ là BranchSerializer chứ không dùng BranchViewSerializer:
class BranchView(generics.GenericAPIView): ... def get(self, request): ... def post(self, request): try: obj_serializer = serializers.BranchesSerializer(data=request.data, partial=True) if obj_serializer.is_valid(): obj_serializer.save() return Response(obj_serializer.data, status=status.HTTP_201_CREATED) except Exception as ex: return Response(ex.args, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
API để update thông tin của branch có sẵn
Chúng ta sẽ dùng method PATCH để thực hiện việc update thông tin có sẵn của branch. Triển khai hàmg patch() dưới đây của GenericAPIView để có được chức năng này:
# Chúng ta sẽ dùng exception này để xử lý trường hợp # record không tồn tại trong cơ sở dữ liệu from django.core.exceptions import ObjectDoesNotExist class BranchView(generics.GenericAPIView): ... ... # API sẽ được gọi kèm theo param "?branch={id của branch}" # PATCH /branch/?id=2 def patch(self, request, *args, **kwargs): # lấy param từ URL path branch_id = request.query_params.get("id") try: # lấy record từ cơ sở dữ liệu. "pk" có nghĩa là primary key, ở đây tương đương cột "id" model = models.Branches.objects.get(pk=branch_id) # chú ý sự khác biệt khi dùng BranchesSerializer ở đây so với api GET # có thêm biến đầu tiên truyền vào chính là model đại diện cho record được tìm thấy. obj_serializer = serializers.BranchesSerializer(model, data=request.data, partial=True) if obj_serializer.is_valid(): obj_serializer.save() return Response(obj_serializer.data, status=status.HTTP_200_OK) except ObjectDoesNotExist as e: # nếu không có record tồn tại, trả về exception ObjectDoesNotExist: return Response(ex.args, status=status.HTTP_404_NOT_FOUND) return Response({"error": "Something went wrong"}, status.HTTP_400_BAD_REQUEST)
Phân trang với Pagination
Django Rest có đi kèm sẵn cấu hình để giúp việc phân trang kết quả trả về trong API json. Dưới đây là các bước cần làm để api của bạn có thể sử dụng chức năng này:
# import class PageNumberPagination để phân trang from rest_framework.pagination import PageNumberPagination ... class RestaurantView(generics.GenericAPIView): ... # khai báo class sử dụng cho việc phân trang pagination_class = PageNumberPagination # khai báo số record trong 1 trang page_size = 10 # số record tối đa max_page_size = 20 # tên query param có thể dùng để thay đổi số record của 1 trang page_size_query_param = 'page_size' # Update code của hàmg xử lý phương thức get def get(self, request): # khai báo kích cỡ của 1 trang: self.pagination_class.page_size = self.page_size # tạo queryset để lấy tất cả các nhà hàng trong csdl myqueryset = models.Restaurants.objects.all() # dùng hàm paginate_queryset của PageNumberPagination để lấy theo trang page = self.paginate_queryset(myqueryset) # Nếu lấy thành công theo trang, # kết quả trả về "result" cũng cần format theo dạng phân trang # bằng cách sử dụng hàm get_paginated_response if page is not None: serializer = self.serializer_class(page, many=True) result = self.get_paginated_response(serializer.data) else: result = self.serializer_class(myqueryset, many=True) return Response(result.data, status=status.HTTP_200_OK)
Truy vấn cơ sở dữ liệu với filter() và Q
Trong các ví dụ trên, chúng ta đã thấy cách để truy vấn cơ sở dữ liệu sử dụng queryset của django:
myqueryset = models.Restaurants.objects.all()
Tương tự vậy, để truy vấn có điều kiện, chúng ta có thể dùng như sau:
# name được truyền vào qua url: /restapp/restaurant?name=KFC requested_name = request.query_params.get("name") myqueryset = models.Restaurants.objects.filter(name=requested_name)
Chúng ta cũng có thể dùng từ khoá “LIKE”:
myqueryset = models.Restaurants.objects.filter(name__contains=requested_name)
Để biết được tất cả các từ khoá hay so sánh mà chúng ta có thể sử dụng, bạn hãy tham khảo tại: https://docs.djangoproject.com/en/3.0/ref/models/querysets/#id4
Kết hợp điều kiện với “AND”
Giả định bạn có thêm 2 trường mới cho bảng restaurants:
- is_active TINY_INT(1): với giá trị = 1 tương đương với cửa hàng còn hoạt động, = 0 nếu cửa hàng đã đóng cửa
- rating FLOAT: là đánh giá chất lượng, thang điểm từ 0.0 tới 5.
ALTER TABLE `restapp_restaurants` ADD `is_active` TINYINT(1) NULL DEFAULT '1' COMMENT '1: active, 0: inactive' AFTER `name`, ADD `rating` FLOAT NULL COMMENT '0.0->5' AFTER `is_active`;
Cập nhật models.py:
class Restaurants(models.Model): name = models.CharField(max_length=255) is_active = models.IntegerField(null=True) # thêm mới rating = models.FloatField(null=True) # thêm mới
Truy vấn các restaurant có is_active = 1, và rating >= 3.5
myqueryset = models.Restaurants.objects.filter(is_active=1, rating__gte=3.5)
Tuy nhiên, nếu chúng ta chỉ muốn kiểm tra rating nếu rating là 1 điều kiện khi gọi API, vd khi chúng ta gọi “/restapp/restaurant?rating=3”. Nếu chỉ gọi “/restapp/restaurant” thì không kiểm tra điều kiện rating. Với trường hợp này, chúng ta sử dụng “Q” của django:
from django.db.models import Q def get(self, request): self.pagination_class.page_size = self.page_size # IMPORTANT LINE min_rating = request.query_params.get("rating", 0) is_active = request.query_params.get("is_active", 1) myq = Q(is_active=is_active) if int(min_rating) > 0: # dùng "&" để kết hợp myq = myq & Q(rating__gte=min_rating) myqueryset = models.Restaurants.objects.filter(myq)
Truy vấn với OR
Với OR, bạn cũng dùng “Q” và kết hợp với operand “|”:
myq = Q(is_active=is_active) | Q(rating__gte=min_rating)
Bạn có thể tra cứu tại đây để tạo các câu lệnh phức tạp với Q: https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q
Truy vấn với excludes
Tìm hiểu thêm về exclude: https://docs.djangoproject.com/en/3.0/ref/models/querysets/#exclude