I. Django cơ bản

Cài đặt thư viện

Để phát triển ứng dụng web với django, cần cài đặt các thư viện sau:

pip install django

Kiểm tra django đã được cài đặt: chạy python shell và gõ lệnh:

>> import django
>> print(django.get_version())

Tạo mới project

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:

django-admin startproject <project_name>

Project mới được tạo ra sẽ có cấu trúc như sau:

<project_name>
   manage.py
   <project_name>
      __init__.py
      settings.py
      urls.py
      wsgi.py
Khởi động server:

Mở cửa sổ command tại thư mục gốc của project và gõ lệnh:

python manage.py runserver

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:

python manage.py runserver <ip:port>

Ví dụ:

python manage.py runserver 8080 # chạy ứng dụng tại cổng 8080
python manage.py runserver 0.0.0.0:8080 # chạy ứng dụng ở cổng 8080, tất cả IP của máy

Tạo mới ứng dụng

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:

python manage.py startapp <app>

Sau khi app mới được tạo ra, cấu trúc thư mục project có dạng như sau:

<project_name>
   <app>
      admin.py
      apps.py
      migrations
      models.py
      tests.py
      views.py
      __init__.py
   manage.py
   <project_name>
      __init__.py
      settings.py
      urls.py
      wsgi.py

Ý nghĩa các file trong app:

Cho phép app hoạt động

Để 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.

INSTALLED_APPS = [
   'app', # add this line, note : comma (,) after 'app'
   ...,

Hàm xử lý request

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

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.

Lấy tham số từ url

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)

            ...
      
    
Tham số dạng biến đường dẫn

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>

Cấu hình database

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'
   }
}

Làm việc với Database qua ORM

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.

Khai báo đối tượng ORM

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
Liên kết giữa các bảng

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.

Migrate database

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):

Truy xuất Database qua ORM

Thêm mới bản ghi:

      
        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 bản ghi:

Để 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:

Để 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

Để 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

Để 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
Product.objects.filter(price__gt=10) # greater than
Product.objects.filter(price__gte=10) # greater than or equal
Product.objects.filter(price__lt=10) # less than
Product.objects.filter(price__lte=10) # less than or equal
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
from django.db.models import Q
 
Product.objects.filter(Q(price__lt=10) | Q(price__gt=15))
Phủ định điều kiện
from django.db.models import Q
 
Product.objects.filter(~Q(price=10))
Tìm kiếm theo trường ở bảng liên kết
Product.objects.filter(category__code='IPHONE')
Product.objects.filter(category__code__startswith='IP')

Admin Panel của Django

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 admin

Mở cửa sổ command trong thư mục gốc của project và gõ lệnh:

python manage.py createsuperuser

Django sẽ yêu cầu nhập username & password để tạo tài khoản admin.

Truy nhập Admin Panel

Trê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:

Đăng ký quản lý đối tượng database vào Admin Panel

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
      
    

II. DjangoRestFrameWork (DRF)

Cài đặt thư viện

Để phát triển ứng dụng với DjangoRestFrameWork, cần cài đặt các thư viện sau:

pip install django djangorestframework django-cors-headers

Cấu hình Project

Để sử dụng DRF trong Django project, cần thêm vào file settings.py dòng cấu hình sau:

INSTALLED_APPS = [
   'rest_framework', # new
   ...,

Dùng DRF để tạo web-service

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!"})

Các phương thức của REST

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 makemigrationsmigrate (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), ]

Test các web-service:

Tạo mới bản ghi:

Để 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 bản ghi theo id:

Để 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:

Để 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 bản ghi:

Để 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:

Để xoá bản ghi, sử dụng service tại địa chỉ http://127.0.0.1:8000/api/delete-student/<id>.

Serializer

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:

Khai báo Serializer class

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:

Sử dụng Serializer class

Dùng Serializer Class để chuyển dữ liệu từ request vào database:

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

Dùng Serializer Class để chuyển dữ liệu từ database xuống response:

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),
        ]
      
    

Tạo API với ViewClass

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 getpost, 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:


Tạo CRUD API với ViewSet

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.

Khai báo Viewset

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

Sử dụng Viewset

Sau khi đăng ký Viewset, DRF tự động sinh ra các web-service sau:


CORS

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.

Enable CORS trong DRF

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:

Kiểm tra CORS đã được bật

Để 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.

Bảo mật API với JWT

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:

Sử dụng JWT trong DRF

Có nhiều thư viện để sử dụng JWT với DRF, trong đó đơn giản nhất là djangorestframework_simplejwt:

pip install 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()),
        ]
      
    
Test API với Postman

Sau khi đã gắn bảo mật cho các API (ở ví dụ trên là /api/hello/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 TypeBearer, 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.