Để phát triển ứng dụng web với django, cần cài đặt các thư viện sau:
Kiểm tra django đã được cài đặt: chạy python shell và gõ lệnh:
Sử dụng công cụ django-admin (có sẵn sau khi cài django) để tạo mới project: từ cửa sổ command, dùng lệnh sau để tạo mới một project:
Project mới được tạo ra sẽ có cấu trúc như sau:
Mở cửa sổ command tại thư mục gốc của project và gõ lệnh:
Mở trình duyệt và truy nhập địa chỉ ứng dụng tại http://127.0.0.1:8000
Theo mặc định, server django sẽ chạy ở cổng 8000 và sử dụng địa chỉ IP 127.0.0.1, để thay đổi cổng và IP của server, sử dụng lệnh sau:
Ví dụ:
Một project django thường có nhiều ứng dụng (app). Để tạo mới một ứng dụng, mở cửa sổ command tại thư mục gốc của project và gõ lệnh:
Sau khi app mới được tạo ra, cấu trúc thư mục project có dạng như sau:
Ý nghĩa các file trong app:
Để app hoạt động, phải thêm tên app vào phần cấu hình INSTALLED_APPS trong file settings.py của project.
Hàm xử lý request được đặt trong file views.py của app có tác dụng xử lý các yêu cầu do người dùng gửi đến. Hàm xử lý request có thể trả về nội dung dưới dạng html (website), hoặc trả về nội dung dưới dạng dữ liệu json/xml/... (web-service).
Ví dụ:
# File: app/views.py
import json
from django.shortcuts import HttpResponse
# Create your views here.
def index(request):
return HttpResponse(
'''
<h1>Django App</h1>
<p>Hello from Django.</p>
'''
)
def hello_service(request):
return HttpResponse(
json.dumps({'message': 'Hello'}),
content_type='application/json'
)
# File: <project_name>/urls.py
from django.contrib import admin
from django.urls import path
from app.views import * # new
urlpatterns = [
path('', index), # new
path('api/hello', hello_service), # new
path('admin/', admin.site.urls),
]
Ví dụ trên khai báo 2 hàm xử lý request:
Khi truy nhập trang chủ (http://127.0.0.1:8000), kết quả trả về là một website:
Khi truy nhập địa chỉ http://127.0.0.1:8000/api/hello, kết quả trả về là một web-service:
Mapping url là việc cấu hình để mỗi trỏ các đường link truy nhập server (url) đến các hàm xử lý request tương ứng.
Ở ví dụ phần trên, chúng ta đã map 2 url vào 2 hàm xử lý request:
| / | → | index |
| /api/hello | → | hello_service |
Việc này được thực hiện thông qua các khai báo trong phần urlpatterns của file urls.py của project:
urlpatterns = [
path('', index),
path('api/hello', hello_service),
...
]
Khi có nhiều ứng dụng, việc khai báo toàn bộ mapping của các ứng dụng trong 1 file sẽ trở nên khó theo dõi. Do đó django cho phép chia nhỏ mapping ra theo từng ứng dụng. Có thể hình dung việc này tương tự như cách chia folder thành nhiều cấp trên ổ đĩa.
Chia nhỏ url mapping theo từng ứng dụng:Để chia nhỏ url mapping theo từng ứng dụng (app), thực hiện các bước sau:
Ví dụ:
Nếu project chỉ có một app như trong các ví dụ trước thì việc chia mapping như sau: tạo file app/urls.py, sau đó điền nội dung các file như sau:
# File: app/views.py
import json
from django.shortcuts import HttpResponse
# Create your views here.
def index(request):
return HttpResponse(
'''
<h1>Django App</h1>
<p>Hello from Django.</p>
'''
)
def hello_service(request):
return HttpResponse(
json.dumps({'message': 'Hello'}),
content_type='application/json'
)
# File: <project_name>/urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('app/', include('app.urls')), # new
]
# File: app/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('', index),
path('api/hello', hello_service),
]
Để truy nhập trang web do hàm index trả về, dùng địa chỉ: http://127.0.0.1:8000/app, lưu ý so với phần trước, phía cuối đường link có thêm đuôi /app
Tương tự ,để truy nhập web-service do hàm hello_serivce cung cấp, sử dụng địa chỉ: http://127.0.0.1:8000/app/api/hello
So với ví dụ trước, các link truy nhập ứng dụng có thêm phần /app phía sau địa chỉ gốc của server, điều này là do trong phần urlpatterns của project đã có khai báo:
path('app/', include('app.urls')),
Khai báo này khiến toàn bộ các url con trong file app/urls.py được gắn thêm phần 'app/' ở trước.
Khi có nhiều app khác nhau thì phần tiền tố trên có thể đặt theo tên app, ví dụ: 'app1/', 'app2/', ... Việc này tương tự việc chia file thành nhiều thư mục con trên ổ đĩa.
Thông thường trên các url truy nhập website/web-service có thêm các tham số tuỳ biến. Có thể hình dung các tham số này tương tự như các tham số đầu vào của các hàm xử lý, ví dụ:
http://<domain:port>/api/get-weather-data?location=Hanoi
Phần tham số được đặt cuối url và ngăn cách với phần địa chỉ bằng dấu ?:
?location=Hanoi
Nếu có nhiều tham số thì các tham số được ngăn cách nhau bởi dấu &:
?location=Hanoi&unit=metric
Ở phía hàm xử lý request, các tham số cần được tách ra để thực hiện logic xử lý phù hợp
Trong django, các tham số từ url được chứa trong đối tượng request.GET, đối tượng này có kiểu dữ liệu là Dictionary với các key là tên của tham số, còn value là giá trị của tham số
Ví dụ:
| Query string (from URL) | request.GET |
|---|---|
| ?location=Hanoi | {"location": "Hanoi"} |
| ?location=Hanoi&unit=metric | {"location": "Hanoi", "unit": "metric"} |
def get_weather_data(request):
location = request.GET.get("location")
unit = request.GET.get("unit")
print("location=", location, ", unit=", unit)
...
Ngoài cách truyền tham số qua query string như bên trên, còn có thể sử dụng tham số dưới dạng biến đường dẫn, ví dụ:
http://<domain:port>/api/get-weather-data/Hanoi
http://<domain:port>/api/get-weather-data/HCMCity
Trong Django, để lấy giá trị biến đường dẫn, cần khai báo trong cả urlpatterns và hàm xử lý request:
# File: app/views.py
...
def get_weather_data(request, location):
print('location=', location)
...
# File: app/urls.py
...
urlpatterns = [
path('api/get-weather-data/<location>', get_weather_data),
...
]
Trong urlpatterns, biến đường dẫn được đặt trong cặp ngoặc nhọn:
path('api/get-weather-data/<location>', get_weather_data),
Trong hàm xử lý request, giá trị của biến đường dẫ được chuyển vào tham số phụ sau tham số request:
def get_weather_data(request, location):
...
Ví dụ:
# File: app/views.py
import json
from django.shortcuts import HttpResponse
data = {
'Hanoi': {'temp': 19, 'humidity': 90},
'HCMCity': {'temp': 32, 'humidity': 80},
}
def get_weather_data(request):
location = request.GET.get('location')
result = data.get(location, {'error': 'Unknown location'})
return HttpResponse(json.dumps(result),
content_type='application/json')
def get_weather_data2(request, location):
result = data.get(location, {'error': 'Unknown location'})
return HttpResponse(json.dumps(result),
content_type='application/json')
# File: <project_name>/urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('app.urls')), # new
]
# File: app/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('get-weather-data', get_weather_data),
path('get-weather-data2/<location>', get_weather_data2),
]
Ứng dụng trên cung cấp 2 API để lấy thông tin thời tiết:
Theo tham số từ query string: /api/get-weather-data?location=<location>
Theo biến đường dẫn: /api/get-weather-data2/<location>
Thông tin kết nối database của project nằm trong file settings.py, theo mặc định, django sử dụng database sqlite cho project:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
Để chuyển sang sử dụng các database engine khác, trước hết cần cài đặt driver cho database engine, ví dụ với MySQL cần cài đặt :
pip install mysqlclient
Sau khi đã cài đặt driver cho database engine, thay đổi lại thông tin kết nối trong file settings.py:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': '<db_name>',
'USER': '<user_name>',
'PASSWORD': '<password>',
'HOST': '127.0.0.1'
}
}
ORM (Object Relational Mapping) là cách làm việc với database thông qua các class của ngôn ngữ lập trình
Mỗi bảng trong database sẽ tương ứng với một class của ngôn ngữ lập trình, mỗi cột của bảng dữ liệu tương ứng với mỗi trường của đối tượng.
Ưu điểm của ORM: chương trình ngắn gọn, cách viết truy vấn database giống với viết chương trình bình thường.
Nhược điểm của ORM: khó viết các query phức tạp, một số trường hợp hiệu năng không cao.
Trong Django, việc khai báo các đối tượng mapping với các bảng của database được thực hiện trong file models.py của ứng dụng.
# app/models.py
from django.db import models
class Product(models.Model):
code = models.CharField(max_length=30, unique=True)
name = models.CharField(max_length=200)
price = models.FloatField()
# Hàm hiển thị
def __str__(self):
return self.name
Các đối tượng mapping cần kế thừa class django.db.models.Model, trong mỗi đối tượng, thực hiện khai báo các trường dữ liệu tương ứng với các cột của bảng trong database. Một số loại trường dữ liệu thông dụng:
| Kiểu dữ liệu Django | Kiểu dữ liệu database |
|---|---|
| CharField | VARCHAR |
| TextField | TEXT |
| FloatField | FLOAT |
| IntegerField | INT |
| BigIntegerField | BIGINT |
| BooleanField | SMALLINT |
| DateField | DATE |
| DateTimeField | DATETIME |
| FileField | VARCHAR (lưu trong database đường link đến file) |
| ImageField | VARCHAR (lưu trong database đường link đến ảnh) |
Các trường dữ liệu có thể có thêm các thuộc tính ràng buộc:
| Thuộc tính | Ý nghĩa |
|---|---|
| unique | Giá trị của trường dữ liệu phải duy nhất |
| max_length | Độ dài tối đa trường dữ liệu |
| null | Trường dữ liệu được phép nhận giá trị null |
| blank | Trường dữ liệu (kiểu string) được phép nhận giá trị blank |
Các quan hệ trong SQL (OneToMany, ManyToOne, ManyToMany) cũng được thể hiện bằng các trường liên kết trong class của model. Ví dụ:
from django.db import models
# Create your models here.
class Category(models.Model):
code = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=100)
class Attribute(models.Model):
name = models.CharField(max_length=100)
value = models.CharField(max_length=200)
class Product(models.Model):
category = models.ForeignKey(Category, on_delete=models.PROTECT, null=True)
attributes = models.ManyToManyField(Attribute, blank=True)
code = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=100)
price = models.FloatField()
Ở ví dụ trên, trong bảng Product, trường category là foreign key liên kết đến bảng Category:
category = models.ForeignKey(Category, on_delete=models.PROTECT, null=True)
Mối quan hệ này thể hiện: mỗi sản phẩm thuộc một nhóm nhất định.
Lưu ý: Django sử dụng đối tượng chứ không phải id để thể hiện quan hệ ManyToOne, (do đó trường category của class Product có kiểu là Object (Category) chứ không phải Integer.Khi khai báo một trường ForeignKey, phải chỉ định giá trị cho thuộc tính on_delete. Giá trị này dùng để xác định xem nếu một bản ghi ở bảng cha (Category) bị xoá thì các bản ghi ở bảng con (Product) sẽ bị ảnh hưởng ra sao:
Tương tự, trường attributes thể hiện liên kết ManyToMany tới bảng Attribute:
attributes = models.ManyToManyField(Attribute, blank=True)
Mối quan hệ này thể hiện: mỗi sản phẩm có thể có nhiều thuộc tính.
Một khi thêm/bớt/sửa các đối tượng hay các trường của đối tượng trong file models.py thì cấu trúc của các bảng trong database sẽ không thay đổi theo ngay lập tức. Để đồng bộ thay đổi trong code (models.py) với database, cần thực hiện thao tác gọi là migrate database
Với Django, thông thường việc migrate database được thực hiện qua 2 lệnh (chạy lệnh trong cửa sổ command tại thư mục gốc của project):
category = Category.objects.create(code='IPHONE', name='IPhone')
product = Product.objects.create(
category=category,
code='IPX',
name='IPhone X',
price=10500000
)
hoặc
category = Category(code='IPhone', name='IPhone')
category.save()
product = Product(
category=category,
code='IPX',
name='IPhone X',
price=10500000
)
product.save()
Ở cách 1, bản ghi trong database được tạo ra ngay sau lệnh create. Lưu ý: phải dùng thuộc tính trung gian .objects phía sau các class ORM để thực hiện thao tác create này
Ở cách 2, sau khi đối tượng được khởi tạo bằng constructor của ORM class, nó chỉ tồn tại trong bộ nhớ mà chưa được lưu xuống database. Chỉ khi phương thức save được gọi, dữ liệu đối tượng mới được đẩy từ bộ nhớ xuống database.
Cách 2 phù hợp cho các trường hợp cần phải xử lý tính toán các trường dữ liệu, sau khi tính toán xong mới lưu vào database.
Để chỉnh sửa một bản ghi, cần lấy được bản ghi từ database vào trong bộ nhớ (objects.get), chỉnh sửa các thuộc tính trong bộ nhớ, sau đó lưu thay đổi xuống database (save)
product = Product.objects.get(code='IPX')
product.price = 9500000
product.save()
Để xoá bản ghi, gọi phương thức delete của ORM class
product = Product.objects.get(code='IPX')
product.delete()
Để lấy về một bản ghi, sử dụng phương thức .objects.get của ORM class:
product1 = Product.objects.get(id=1)
product2 = Product.objects.get(code='IPX')
Bên trong phương thức .objects.get cần chỉ định một điều kiện tìm kiếm (thường theo id hoặc mã định danh), nếu điều kiện tìm kiếm không trả về kết quả nào hoặc trả về nhiều hơn 1 kết quả, lệnh này sẽ báo lỗi.
Để tìm kiếm bản ghi, sử dụng phương thức .objects.filter của ORM class và truyền vào danh sách các điều kiện tìm kiếm. Ví dụ:
product_list = Product.objects.filter(price=10) # các sản phẩm có giá 10 triệu
product_list_2 = Product.objects.filter(code__startswith='IP') # các sản phẩm có mã bắt đầu bằng IP
Một số điều kiện filter thường sử dụng:
| Điều kiện tìm kiếm | Ví dụ |
|---|---|
| Bằng nhau |
Product.objects.filter(price=10)
|
| Bắt đầu bằng một chuỗi kí tự |
Product.objects.filter(name__startswith='ỊPhone')
lưu ý hai dấu gạch dưới trước startswith |
| Kết thúc bằng một chuỗi kí tự |
Product.objects.filter(name__endswith='ỊPhone')
|
| Chứa một chuỗi kí tự |
Product.objects.filter(name__contains='ỊPhone')
|
| Chứa một chuỗi kí tự, không phân biệt hoa/thường |
Product.objects.filter(name__icontains='ỊPhone')
|
| Lớn hơn/lớn hơn hoặc bằng/nhỏ hơn/nhỏ hơn hoặc bằng |
|
| Kết hợp 2 điều kiện theo AND |
Product.objects.filter(price__gt=10, price__lt=15)
hoặc
Product.objects.filter(price__gt=10).filter(price__lt=15)
|
| Kết hợp 2 điều kiện theo OR |
|
| Phủ định điều kiện |
|
| Tìm kiếm theo trường ở bảng liên kết |
|
Admin Panel là công cụ có sẵn của Django để quản lý các đối tượng trong database.
Tạo tài khoản adminMở cửa sổ command trong thư mục gốc của project và gõ lệnh:
Django sẽ yêu cầu nhập username & password để tạo tài khoản admin.
Truy nhập Admin PanelTrên trình duyệt, truy nhập Admin Panel tại địa chỉ: http://127.0.0.1:8000/admin
Đăng nhập username/password đã tạo ra ở bước phía trên.
Giao diện Admin Panel sau khi đăng nhập:
Admin Panel mặc định chỉ quản lý 2 đối tượng : User, Group. Để quản lý thêm các đối tượng riêng của ứng dụng, phải thực hiện đăng ký cho các đối tượng này.
Việc đăng ký được thực hiện trong file admin.py của ứng dụng:
#app/admin.py
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(Category)
admin.site.register(Product)
Sau khi đăng ký đối tượng xong, reload lại Admin Panel, đối tượng sẽ xuất hiện trong danh sách quản lý:
Sử dụng Admin Panel để tạo mới/chỉnh sửa/xem thông tin/xoá các bản ghi của các đối tượng đã đăng ký:
Lưu ý: Để các đối tượng hiện theo tên (thay cho tên class kèm id), cần khai báo phương thức __str__ trong các ORM Class:
# app/models.py
from django.db import models
class Category(models.Model):
...
# Hàm hiển thị
def __str__(self):
return self.name
class Product(models.Model):
...
# Hàm hiển thị
def __str__(self):
return self.name
Để phát triển ứng dụng với DjangoRestFrameWork, cần cài đặt các thư viện sau:
Để sử dụng DRF trong Django project, cần thêm vào file settings.py dòng cấu hình sau:
Tương tự với Django thông thường, để tạo ra web-service cần khai báo hàm xử lý request trong views.py và thực hiện mapping url truy nhập trong file urls.py
Tuy nhiên DRF cung cấp sẵn một số tính năng giúp việc phát triển web-service dễ dàng hơn.
Ví dụ về một web-service đơn giản dùng DRF:
# File: app/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(['GET'])
def hello(request):
return Response({"message" : "Hello world!"})
# File: <project_name>/urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('app.urls')), # new
]
# File: app/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('hello', hello),
]
Khởi động server và truy nhập web-service tại địa chỉ http://127.0.0.1:8000/api/hello
Tương tự như với Django thông thường, hàm xử lý request trả về một đối tượng Response chứa dữ liệu dạng JSON.
return Response({"message" : "Hello world!"})
REST (Representational state transfer) là chuẩn định nghĩa các phương thức trao đổi dữ liệu được dùng phổ biến trong các hệ thống web-service. REST quy định các phương thức trao đổi (chính) sau:
| Phương thức | Mục đích sử dụng | Ứng dụng trong truy xuất database (CRUD) |
|---|---|---|
| GET | Lấy về dữ liệu | Lấy thông tin bản ghi, tìm kiếm bản ghi |
| POST | Đẩy dữ liệu lên | Tạo mới bản ghi |
| PUT | Đẩy dữ liệu lên để thay thế dữ liệu cũ (update) | Chỉnh sửa bản ghi |
| DELETE | Xoá dữ liệu | Xoá bản ghi |
DRF (và các web-service framework) đều hỗ trợ các phương thức trên.
Để chỉ định một hàm xử lý request hỗ trợ những phương thức nào , DRF sử dụng decorator @api_view:
# File: app/views.py
from rest_framework.decorators import api_view
...
@api_view(['GET', 'POST', 'PUT', ...])
def web_service(request):
...
Các phương thức REST được hỗ trợ cần được đặt bên trong tham số của decorator @api_view. Trường hợp client truy nhập theo vào service theo một phương thức không được hỗ trợ thì sẽ dẫn đến lỗi 405 (Method not allowed)
Ví dụ: Sử dụng các phương thức REST để tạo các service CRUD (Create/Retrieve/Update/Delete) cho database:
# File: app/models.py
from django.db import models
class Student(models.Model):
student_number = models.CharField(max_length=20, unique=True)
fullname = models.CharField(max_length=100)
birthdate = models.DateField()
def __str__(self):
return self.fullname
# TODO: Run db migration:
# python manage.py makemigrations
# python manage.py migrate
Lưu ý: Sau khi khai báo đối tượng trong models.py phải thực hiện các lệnh makemigrations và migrate (xem phần làm việc với database qua ORM)
# File: app/views.py
from datetime import datetime
from django.db.models import Q
from .models import Student
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(['POST'])
def create_student(request):
try:
data = request.data
Student.objects.create(
student_number = data['student_number'],
fullname = data['fullname'],
birthdate = datetime.strptime(data['birthdate'], '%Y-%m-%d')
)
return Response({'success': True})
except Exception as e:
return Response({'success': False, 'error': str(e)})
@api_view(['PUT'])
def update_student(request, pk):
try:
data = request.data
student = Student.objects.get(pk=pk)
student.student_number = data['student_number']
student.fullname = data['fullname']
student.birthdate = datetime.strptime(data['birthdate'], '%Y-%m-%d')
student.save()
return Response({'success': True})
except Exception as e:
return Response({'success': False, 'error': str(e)})
@api_view(['DELETE'])
def delete_student(request, pk):
try:
student = Student.objects.get(pk=pk)
student.delete()
return Response({'success': True})
except Exception as e:
return Response({'success': False, 'error': str(e)})
def model_to_dict(student):
return {
'student_number': student.student_number,
'fullname': student.fullname,
'birthdate': student.birthdate.strftime('%Y-%m-%d')
}
@api_view(['GET'])
def get_student_by_id(request, pk):
try:
student = Student.objects.get(pk=pk)
return Response(model_to_dict(student))
except Exception as e:
return Response({'success': False, 'error': str(e)})
@api_view(['GET'])
def search_student(request):
keyword = request.GET.get('keyword', '')
student_list = Student.objects.filter(
Q(fullname__icontains=keyword) |
Q(student_number__icontains=keyword)
)
result = [model_to_dict(student) for student in student_list]
return Response(result)
# File: <project_name>/urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('app.urls')), # new
]
# File: app/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('create-student', create_student),
path('update-student/<pk>', update_student),
path('delete-student/<pk>', delete_student),
path('get-student-by-id/<pk>', get_student_by_id),
path('search-student', search_student),
]
Để tạo mới bản ghi, sử dụng service tại địa chỉ http://127.0.0.1:8000/api/create-student.
Trong phần content gửi lên server, nhập vào các trường dữ liệu của bản ghi dưới dạng JSON.
Ở phía server, dữ liệu gửi lên sẽ được chuyển vào biến request.data có dạng Dictionary (key-value):
data = request.data
Để lấy thông tin một bản ghi theo id, sử dụng service tại địa chỉ http://127.0.0.1:8000/api/get-student-by-id/<id>.
Để tìm kiếm bản ghi (theo tên/mã), sử dụng service tại địa chỉ http://127.0.0.1:8000/api/search-student?keyword=<keyword>.
Để cập nhật thông tin bản ghi, sử dụng service tại địa chỉ http://127.0.0.1:8000/api/update-student/<id>.
Tương tự như với tạo mới, nhập vào phần content các trường dữ liệu của bản ghi dưới dạng JSON.
Để xoá bản ghi, sử dụng service tại địa chỉ http://127.0.0.1:8000/api/delete-student/<id>.
Trong ví dụ về các web-service CRUD bên trên, ở các thao tác thêm mới, chỉnh sửa, khi server nhận dữ liệu do client gửi lên (request.data - dạng Dictionary/key-value), để lưu vào database, hàm xử lý request phải tách ra từng trường dữ liệu một:
def create_student(request):
data = request.data
Student.objects.create(
student_number = data['student_number'], # student_number
fullname = data['fullname'], # fullname
birthdate = datetime.strptime(data['birthdate'], '%Y-%m-%d') # birthdate
)
...
Tương tự, khi tìm kiếm bản ghi, ở phía server dữ liệu được lấy từ database ra dưới dạng các bản ghi, sau đó phải chuyển từng trường một sang dạng dictionary trước khi trả về cho Response:
def model_to_dict(student):
return {
'student_number': student.student_number, # student_number
'fullname': student.fullname, # fullname
'birthdate': student.birthdate.strftime('%Y-%m-%d') # birthdate
}
Nếu đối tượng có nhiều trường dữ liệu, việc viết chương trình như trên sẽ rất dài (liệt kê từng trường một)
Ngoài ra, nếu người dùng nhập vào dữ liệu không hợp lệ (thiếu trường, trùng mã ,...) thì cách viết trên không chỉ ra được lỗi xảy ra do đâu (không có validate dữ liệu đầu vào).
Để giải quyết 2 vấn đề trên, DRF đưa ra class Serializer giúp:
Mỗi class của Database (trong file models.py) cần có một class Serializer riêng. Các Serializer class thường được đặt trong file serializers.py (file này không có từ đầu và cần tạo mới):
# File : app/serializers.py
from rest_framework.serializers import ModelSerializer
from .models import *
class StudentSerializer(ModelSerializer):
class Meta:
model = Student
fields = '__all__' # ['student_number', 'fullname', 'birthdate']
Các Serializer class cần kế thừa class ModelSerializer của DRF
Bên trong mỗi Serializer class, cần khai báo phần Meta (cấu hình) với 2 thông tin:
# File : app/serializers.py
...
class StudentSerializer(ModelSerializer):
class Meta:
model = Student
exclude = ['birthdate'] # fields = ['student_number', 'fullname']
Trong các hàm thêm mới, chỉnh sửa đối tượng, dùng Serializer để validate dữ liệu người dùng gửi lên (trong biến request.data). Nếu dữ liệu không hợp lệ Serializer Class sẽ đưa ra thông báo lỗi cụ thể. Nếu hợp lệ, dữ liệu được lưu xuống database:
# File : app/views.py
...
from .serializers import StudentSerializer
@api_view(['POST'])
def create_student(request):
serializer = StudentSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=400)
@api_view(['PUT'])
def update_student(request, pk):
student = Student.objects.get(pk=pk)
serializer = StudentSerializer(data=request.data, instance=student)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=400)
Việc validate dữ liệu được thực hiện qua phương thức is_valid của Serializer:
if serializer.is_valid():
...
Dữ liệu hợp lệ sẽ được lưu vào database nhờ phương thức save của Serializer:
serializer.save()
Nếu dữ liệu không hợp lệ, nội dung lỗi được trả về trong trường errors của Serializer:
return Response(serializer.errors, status=400) # 400: Bad request
Trong các hàm tìm kiếm, lấy dữ liệu bản ghi, Serializer được dùng để chuyển đối tượng ORM sang dạng Dictionary để có thể trả về cho response:
@api_view(['GET'])
def get_student_by_id(request, pk):
student = Student.objects.get(pk=pk)
data = StudentSerializer(student).data
return Response(data)
@api_view(['GET'])
def search_student(request):
keyword = request.GET.get('keyword', '')
student_list = Student.objects.filter(
Q(fullname__icontains=keyword) |
Q(student_number__icontains=keyword)
)
data = StudentSerializer(student_list, many=True).data
return Response(data)
Dữ liệu chuyển đổi được lấy ra từ trường data của Serializer:
data = StudentSerializer(student).data
Nếu cần chuyển đổi một danh sách nhiều bản ghi, cần thêm tham số many=True trong lệnh chuyển đổi:
data = StudentSerializer(student_list, many=True).data
Chương trình đầy đủ để tạo các web-service CRUD có sử dụng Serializer như sau:
# File: app/models.py
from django.db import models
class Student(models.Model):
student_number = models.CharField(max_length=20, unique=True)
fullname = models.CharField(max_length=100)
birthdate = models.DateField()
def __str__(self):
return self.fullname
# TODO: Run db migration:
# python manage.py makemigrations
# python manage.py migrate
# File : app/serializers.py
from rest_framework.serializers import ModelSerializer
from .models import *
class StudentSerializer(ModelSerializer):
class Meta:
model = Student
fields = '__all__' # ['student_number', 'fullname', 'birthdate']
# File: app/views.py
from datetime import datetime
from django.db.models import Q
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Student
from .serializers import StudentSerializer
@api_view(['POST'])
def create_student(request):
serializer = StudentSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=400)
@api_view(['PUT'])
def update_student(request, pk):
student = Student.objects.get(pk=pk)
serializer = StudentSerializer(data=request.data, instance=student)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=400)
@api_view(['GET'])
def get_student_by_id(request, pk):
student = Student.objects.get(pk=pk)
data = StudentSerializer(student).data
return Response(data)
@api_view(['GET'])
def search_student(request):
keyword = request.GET.get('keyword', '')
student_list = Student.objects.filter(
Q(fullname__icontains=keyword) |
Q(student_number__icontains=keyword)
)
data = StudentSerializer(student_list, many=True).data
return Response(data)
@api_view(['DELETE'])
def delete_student(request, pk):
try:
student = Student.objects.get(pk=pk)
student.delete()
return Response({'success': True})
except Exception as e:
return Response({'success': False, 'error': str(e)})
# File: <project_name>/urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include # new
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('app.urls')), # new
]
# File: app/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('create-student', create_student),
path('update-student/<pk>', update_student),
path('delete-student/<pk>', delete_student),
path('get-student-by-id/<pk>', get_student_by_id),
path('search-student', search_student),
]
Ngoài cách tạo API thông qua hàm xử lý request (như các phần bên trên), DRF còn hỗ trợ việc tạo API sử dụng class (ViewClass). Mỗi ViewClass cần kế thừa class APIView của DRF. Bên trong mỗi ViewClass, cần khai báo các hàm get, post, put, delete,... để xử lý tương ứng cho các phương thức REST.
Ví dụ:
# File: app/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Student
from .serializers import StudentSerializer
class StudentView(APIView):
def get(self, request):
student_list = Student.objects.all()
data = StudentSerializer(student_list, many=True).data
return Response(data)
def post(self, request):
serializer = StudentSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=400)
# File: app/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('student', StudentView.as_view()),
]
# File: app/models.py
# ... Same as above
# File: app/serializers.py
# ... Same as above
# File: <project_name>/urls.py
# ... Same as above
Việc đăng ký web-service với ViewClass trong file urls.py được thực hiện nhờ phương thức as_view của ViewClass:
urlpatterns = [
path('student', StudentView.as_view()),
]
Với đăng ký này, web-service ở địa chỉ http://127.0.0.1:8000/api/student sẽ được xử lý bởi các phương thức bên trong class StudentView.
Do class StudentView hỗ trợ 2 phương thức get và post, có thể thực hiện gọi service đến http://127.0.0.1:8000/api/student thông qua cả 2 phương thức:
Các API CRUD (thêm mới/lấy thông tin bản ghi/chỉnh sửa/xoá) thường giống nhau cho các đối tượng. Để tránh phải viết lại các logic giống nhau này, DRF đưa ra ViewSet giúp tạo ra các CRUD API một cách tự động.
Mỗi ViewSet cần kế thừa class ModelViewSet của DRF và cần khai báo 2 trường thông tin:
Ví dụ:
// File: app/views.py
from rest_framework import viewsets
from .serializers import *
from .models import *
class StudentViewSet(viewsets.ModelViewSet):
serializer_class = StudentSerialzer
queryset = Student.objects.all()
// File: app/urls.py
from rest_framework.routers import DefaultRouter
from .views import *
urlpatterns = [
#Declare normal routes
]
#View set
router = DefaultRouter()
router.register('student', StudentViewSet)
urlpatterns += router.urls
# File: app/models.py
# ... Same as above
# File: app/serializers.py
# ... Same as above
# File: <project_name>/urls.py
# ... Same as above
Việc đăng ký web-service với ViewSet được thực hiện trong file urls.py:
router = DefaultRouter()
router.register('student', StudentViewSet)
urlpatterns += router.urls
Sau khi đăng ký Viewset, DRF tự động sinh ra các web-service sau:
Cross-Origin Resource Sharing (CORS) là việc cho phép gọi webservice (từ trình duyệt) giữa các website có domain khác nhau. Thông thường việc gọi webservice từ một địa chỉ (ví dụ http://www.site1) sang một địa chỉ ở site khác (ví dụ http://www.site2) sẽ bị chặn lại nếu site1 không được site2 cho phép.
Với web-server sử dụng DRF, để cấu hình CORS cho phép các site khác gọi service đến, cần thêm vào file settings.py của project các đoạn cấu hình sau:
INSTALLED_APPS = [
'corsheaders',
...
]
MIDDLEWARE_CLASSES = [
'corsheaders.middleware.CorsMiddleware',
...
]
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = [...] # danh sách các website được gọi service, ví dụ http://127.0.0.1:3000
Để kiểm tra CORS đã được bật, tạo mới một file html với nội dung như sau:
<script>
fetch('http://127.0.0.1:8000/api/student')
.then(resp => resp.json())
.then(result => console.log(result));
</script>
Click chuột file và chọn mở file với trình duyệt (Chrome/FireFox/Edge, ...)
Khi trình duyệt đã mở, click chuột file vào màn hình trắng, chọn Inspect, sau đó chọn tab Console
Nếu ở màn hình log của tab Console có hiện danh sách bản ghi do web-service trả về thì có nghĩa CORS đã được bật thành công:
Nếu có thông báo lỗi: ... blocked by CORS policy ..., thì có nghĩa việc cấu hình CORS chưa đúng, cần kiểm tra lại xem có thiếu/sai bước nào trong 3 bước cấu hình CORS ở phía trên.
JWT (Json Web Token) là cơ chế bảo mật để chỉ cho phép người dùng đã đăng nhập mới có khả năng truy nhập dịch vụ.
Cơ chế hoạt động của JWT:
Có nhiều thư viện để sử dụng JWT với DRF, trong đó đơn giản nhất là djangorestframework_simplejwt:
Sau khi cài đặt thư viện, cần bổ sung thêm cấu hình JWT vào cuối file settings.py:
# File: settings.py
...
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1) # đặt thời gian hết hạn token
}
Trong file urls.py của project bổ sung khai báo url cho API lấy token:
# File: <project_name>/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt import views as jwt_views # new
urlpatterns = [
path('api/token', jwt_views.TokenObtainPairView.as_view()), # new
path('api/', include('app.urls')),
path('admin/', admin.site.urls),
]
Gắn chức năng bảo mật cho các web-service:
# File: app/views.py
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def hello_service(request, format=None):
return Response({"message" : "Hello"})
# ViewClass
class HelloView(APIView):
permission_classes = [IsAuthenticated]
def get(request):
return Response({"message" : "Hello"})
# File: app/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('hello', hello_service),
path('hello2', HelloView.as_view()),
]
Sau khi đã gắn bảo mật cho các API (ở ví dụ trên là /api/hello và /api/hello2), nếu truy nhập trực tiếp API từ trình duyệt sẽ gặp lỗi 403 Forbidden:
Để gọi được API, phải lấy token và gửi kèm trong các lần gọi API.
Lấy token:
Sử dụng token để gọi API: để gọi API, cần thêm token dưới dạng Bearer vào Header của request.
Sử dụng Postman để thực hiện gọi API có gắn token đi kèm. Sau khi nhập địa chỉ API vào thanh địa chỉ của Postman, sang tab Authorization, chọn Type là Bearer, phần Token nhập vào giá trị của trường access trả về ở bước trên.
Chọn Send để gọi API, kết quả sẽ trả về trong phần response bên dưới.