Django REST Framework คือ toolkit หรือไลบรารีของ Python ในการสร้าง RESTful APIs ได้อย่างรวดเร็วและสมบูรณ์แบบ ในบทความนี้จะมาอธิบายการสร้าง RESTful APIs และพื้นฐานต่าง ๆ ที่จำเป็นต้องรู้ พร้อมทั้งได้ Web Service/API เป็นของตัวเองด้วยการ deploy ไปที่ PaaS (Platform as a Service) อย่าง Heroku และใน Ep ถัดไปซึ่งเป็นบทความที่ 2 เป็น (Final EP) จะเป็นการใช้งาน Vue.js ร่วมกับ Django REST Framework
ปล.ถ้ายังไม่มีพื้นฐาน Django แนะนำบทความ - พัฒนาเว็บไซต์ด้วย Django เฟรมเวิร์คฉบับสมบูรณ์ ปี 2023
REST : REpresentational State Transfer คือสถาปัตยกรรมอย่างหนึ่งของซอฟต์แวร์ที่อยู่บนพื้นฐานของ HTTP Protocol ถึงแม้เราจะมองไม่เห็นแต่ว่ามันมีอยู่จริง (แน่นอนละ เพราะมันเกี่ยวข้องกับซอฟต์แวร์ จะมองเห็นได้ไงเนอะ) โดยจะมี 2 ตัวละครหลักคือ Client และ Server
ตรงส่วนนี้ขอไม่อธิบายรายละเอียดมาก อาจจะต้องไปหาข้อมูลเพิ่มเติมครับ ถือว่าให้เป็นคีย์เวิร์ดไปสืบค้นต่อไป
บทความแนะนำ/อ่านเพิ่มเติม What is a REST API
เชื่อว่าหลายคนอาจจะยังมีข้อสงสัยที่ว่า Django กับ Django REST Framework นั้นแตกต่างกันอย่างไร ซึ่งขอยกเอา discussion ที่น่าสนใจในเว็บ stackoverflow มาเป็น reference ในครั้งนี้ครับ ดังนั้นจึงขอสรุปให้แบบเนื้อ ๆ
เลือก
Note: จริง ๆ แล้วเราสามารถที่จะเขียนและสร้าง API ด้วย Django แบบปกติคือเรียกว่า build from scratch ได้เลย แต่เมื่อมี tool อย่าง REST Framework แล้ว ก็ไม่มีเหตุผลที่จะไป re-invent the wheels หรือสร้างอะไรสักอย่างที่มันดีและมีอยู่แล้วขึ้นมาใหม่นั่นเอง
นี่คือเหตุผลบางส่วนว่าทำไมต้องใช้ Django REST Framework
ทั้งสองส่วนนี้เรียกได้ว่ามีบทบาทสำคัญสำหรับการสร้าง RESTful APIs ไปทำความเข้าใจทั้งสองส่วนนี้กันได้เลย
HTTP methods นั้นก็แทบจะเรียกไว้ว่าสื่อความหมายและอธิบายได้ในตัวมันเอง (Self-explanatory) และตรงไปตรงมา ซึ่งทั้ง 4 เมธอดดังต่อไปนี้มีความนิยมและพบเจอได้บ่อยที่สุด
ซึ่งจริง ๆ แล้ว HTTP Methods ไม่ได้มีเพียงแค่ 4 ตัวนี้ ยังมีตัวอื่น ๆ เช่น HEAD, PATCH, CONNECT, TRACE, OPTIONS ดังนั้นมีลิ้งค์อ้างอิงในท้ายบทความ สามารถไปศึกษาเพิ่มเติมได้เลยครับ
Note: Safe method: GET เพราะว่าไม่ได้ทำให้ข้อมูล (data/resource) ใน server มีการเปลี่ยนแปลง ส่วน POST , PUT , DELETE เป็น unsafe methods เพราะว่าทำให้ข้อมูลใน server เกิดการเปสี่ยนแปลง ถ้าเคยเขียน Django มาก่อน เช่นในหน้า HTML form นั้นถ้ามีการใช้งานเมธอด POST จะต้องใส่แท็กของ CSRF token เข้าไปช่วยป้องกันทุกครั้ง
ซึ่งโดยปกติแล้ว status codes ที่เรามักจะพบเจอบ่อย ๆ มีดังนี้
ทำการสร้างโฟลเดอร์เพื่อเก็บโปรเจคท์พร้อมทั้งสร้าง virtual environment ให้กับโปรเจคท์ (quick start)
# Create a folder to store our project
mkdir restframework
# ชี้ไปที่โฟลเดอร์ที่สร้างขึ้นมา
cd restframework
# สร้าง virtual environment ชื่อว่า env
python -m venv env
# Activate (เรียกใช้งาน) virtual environment (Windows)
env/Scripts/activate
# Virtual env ถูก activate และพร้อมใช้งาน
(env)
คลิปวิดีโอแนะนำ (Youtube) - Virtual Environment คืออะไร
ทำการติดตั้ง Django และ Django REST Framework ผ่าน pip
(env) pip install django
(env) pip install djangorestframework
เมื่อทำการติดตั้ง Django และ Django REST Framework เสร็จแล้ว ก็จะเป็นการเริ่มต้นสร้างโปรเจคท์และแอพ
(env) django-admin startproject mysite . (env) cd mysite (env) python manage.py startapp api
requirements.txt
(env) pip freeze > requirements.txt
จะมีแพ็คเกจและไลบรารีที่ติดตั้งดังนี้
asgiref==3.3.4
Django==3.2.2
djangorestframework==3.12.4
pytz==2021.1
sqlparse==0.4.1
typing-extensions==3.10.0.0
Project Structure
├── .gitignore
└── restframework
├── db.sqlite3
├── mysite
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── api
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── requirements.txt
ทำการรีจิสเตอร์แอพใน settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api', # Oup app
'rest_framework' # DRF
]
เมื่อสร้างโปรเจคท์และแอพเสร็จเรียบร้อย ถัดมาก็จะเป็นการสร้าง model ซึ่งในที่นี้จะหมายถึงการสร้าง class/object เพื่อติดต่อกับกับฐานข้อมูล เป็นอันว่ามักจะเข้าใจกันดีถ้าพูดถึง model นั้น แน่นอนว่าจะมีความเกี่ยวข้องกับ database นั่นเอง โดย database ที่ใช้ในโปรเจคท์นี้ก็จะใช้ SQLite ซึ่งเก็บข้อมูลในรูปแบบของไฟล์ ไม่ต้องทำการคอนฟิกเซิร์ฟเวอร์หรือพอร์ตอะไรต่าง ๆ ให้ยุ่งยาก เรียกได้ว่ามีมาให้กับ Django และพร้อมใช้กันได้เลย
Note: SQLite จะนิยมใช้กับโปรเจคท์เล็ก ๆ หรือไม่ว่าจะเป็นในช่วงของการ development ใน localhost เท่านั้น แต่ถ้าโปรเจคท์ใหญ่ ๆ มีผู้ใชเ้ยอะ ๆ มี concurrency สูง ๆ จะไม่เหมาะ แต่สำหรับคำแนะนำของผู้เขียน ถ้าจะ build โปรเจคท์จริง ๆ ควรใช้ PostgreSQL ทั้งในส่วนของ development และ production นั่นก็คือ keep the same environment as possible
ทำการออกแบบและสร้างตารางใน models.py
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=80)
description = models.TextField()
date_created = models.DateTimeField(auto_now_add=True)
complete = models.BooleanField()
class Meta:
ordering = ['-date_created']
db_table = 'task'
def __str__(self):
return self.title
จากนั้นทำการรันคำสั่งเพื่อ migrate database โดยต้อง makemigrations ก่อน
(env) python manage.py makemigrations
จะได้
Migrations for 'task':
api\migrations\0001_initial.py
- Create model Task
จากนั้นทำการ migrate
(env) python manage.py migrate
ตารางทุกตารางจะถูก migrate ไปที่ฐานข้อมูล SQLite เป็นอันเสร็จสิ้นขั้นตอน migrat่ion
Operations to perform:
Apply all migrations: admin, api, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying api.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
serializer คือกระบวนการแปลง model object ไปเป็น JSON ฟอร์แมตเพื่อส่งไปที่ client หรือจะมีส่วนกลับอีกตัวที่มีชื่อว่า deserializer ที่ทำหน้าที่ตรงกันข้ามคือแปลง JSON ไปเป็น Django object เพื่อให้ Django สามารถอ่านเข้าใจได้
ทำการสร้างไฟล์ใหม่ขึ้นมาชื่อ serializers.py และทำการอิมพอร์ตโมดูล serializers เข้ามาเพื่อเรียกใช้งานคลาส ModelSerializer ซึ่งจะเป็นพระเอกในการ serialize ออปเจคท์นั่นเอง
serializers.py
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ('id', 'title', 'description', 'date_created', 'complete')
Views คือส่วนของการเขึยน business logic ของ Django เช่นการจัดการกับ request/response ต่าง ๆ ซึ่งปกติแล้วจะมีรูปแบบการเขียนได้ 2 แบบ คือ Function Based (FBV) View และ Class Based View (CBV) ซึ่งในโปรเจคท์นี้จะเขียนแบบที่สอง เพราะว่าจะสามารถเรียกใช้ฟีเจอร์ต่าง ๆ ของ REST Framework ได้เต็มประสิทธิภาพ ซึ่งพระเอกของเราต่อไปนี้ก็คือโมดูล generics ที่ประกอบไปด้วย APIView ต่าง ๆ ให้เราสามารถเลือกใช้งานได้อย่างสะดวกและรวดเร็วมาก ๆ
ไปที่ไฟล์ views.py
from django.shortcuts import render
from .serializers import TaskSerializer
from .models import Task
from rest_framework import generics
class TaskList(generics.ListAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
# return Response(queryset.data)
Note: การเขียนแบบ Class Based View จะขึ้นต้นด้วยคีย์เวิร์ด class และตัวที่เป็น Function Based View จะขึ้นต้นด้วย def
บทความแนะนำ/อ่านเพิ่มเติม CBV vs FBV
ต่อมาจะเป็นการกำหนด URLs ให้กับโปรเจคท์ โดยทำอิมพอร์ตโมดูล views จากแอพ api เข้ามาเพื่อเรียกใช้งานคลาสและออปเจคท์ต่าง ๆ ในโมดูล
ไปที่ไฟล์ mysite/urls.py
from django.contrib import admin
from django.urls import path, include
from api import views
urlpatterns = [
path('admin/', admin.site.urls),
path('api/tasks/', views.TaskList.as_view()),
]
Endpoint --> Method --> Action
อีกหนึ่งจุดเด่นของ Django REST Framework คือมีหน้า Interface ที่ใช้สำหรับเรียกดู ทดสอบ API ได้ ซึ่งทำให้สะดวกมาก ๆ ในการพัฒนา ซึ่งเราจะเรียกหน้านี้ว่า Browsable API แปลเป็นไทยก็คือหน้า API ที่สามารถเรียกดูได้นั่นเอง
http://127.0.0.1:8080/api/tasks/
สังเกตเห็นว่าตอนนี้ยังไม่มีข้อมูลหรือ record ใด ๆ ใน URI นี้ ก็เพราะว่ายังไม่ได้มีการเพิ่มหรือบันทึกข้อมูล ๆ เข้ามานั่นเองครับ เลยยังแสดงเป็นลิสต์เปล่า ๆ []
ทำการสร้าง superuser เพื่อเข้าใช้งานหน้าแอดมินด้วยคำสั่ง createsuperuser
(env) python manage.py createsuperuser
จะขึ้น console ตามนี้ และจัดการกำหนด username และ password ให้เรียบร้อย
Username (leave blank to use 'user'): sonny
Email address: [email protected]
Password:
Password (again):
The password is too similar to the username.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
Note: password เป็น hidden field จะไม่แสดงให้เห็นในหน้าคอนโซล และ Email เป็น blank field สามารถปล่อยว่างได้
ทำการสร้าง superuser เสร็จแล้วก็ทำการรีจิสเตอร์โมเดลใน admin โดยไปที่ไฟล์ admin.py จากนั้นทำการรีจิสเตอร์ตามปกติ
from django.contrib import admin
from .models import Task
# Register our model
admin.site.register(Task)
จากนั้นเข้าหน้าแอดมินและทำการเพิ่มข้อมูลเข้าไปสัก 1 record ดังภาพด้านล่าง
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"id": 1,
"title": "Copy code from Stackoverflow",
"description": "I have no idea, so I decided to copy code from Stackoverflow for my new project",
"date_created": "2021-05-10T10:46:37.932394Z",
"complete": false
}
]
อ่านเพิ่มเติม Content Type (mozilla.org)
http://127.0.0.1:8080/api/tasks/
ซึ่งถ้าสังเกตในคอนโซลจะพบว่ามีการรีเทิร์น status code เป็น 201 ซึ่งหมายถึงมีการสร้าง task ขึ้นมาใหม่สำเร็จ ซึ่งจะอธิบายในส่วนของ HTTP Methods และ Status Code
[10/May/2021 18:01:00] "POST /api/tasks/ HTTP/1.1" 201 9379
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"id": 2,
"title": "Read articles on FreeCodeCamp",
"description": "I must read articles on FreeCodeCamp everyday to boost my programming knowledge",
"date_created": "2021-05-10T11:01:00.844176Z",
"complete": true
},
{
"id": 1,
"title": "Copy code from Stackoverflow",
"description": "I have no idea, so I decided to copy code from Stackoverflow for my new project",
"date_created": "2021-05-10T10:46:37.932394Z",
"complete": false
}
]
ต่อมาจะเป็นในส่วนของการเข้าดูรายละเอียดของแต่ละ task ซึ่งนั่นก็คือ Task Detail นั่นเอง ปกติแล้วเราต้องทำการเขียนฟังก์ชันเพื่อกำหนดแอคชั่นของแต่ละเมธอด ว่าจะให้เรียกดู อัปเดต เพิ่มหรือลบ แต่ด้วยความพิเศษของ genericsView ช่วยให้เราสามารถเขียนอยู่บนคลาสเดียวและจัดการให้เสร็จสรรพ
สร้างคลาสใหม่ที่มีชื่อว่า TaskDetail ใน views.py
...
class TaskList(generics.ListCreateAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
# New
class TaskDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
ทำการกำหนด end-point ใหม่ซึ่งจะอ้างอิงด้วย URL พารามิเตอร์ในรูปแบบของ pk คือ /<int:pk> ต่อท้าย tasks
อัปเดตโค้ดต่อไปนี้ใน mysite/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('api/tasks/', views.TaskList.as_view()),
path('api/tasks/<int:pk>', views.TaskDetail.as_view()) # New
]
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"id": 1,
"title": "Copy code from Stackoverflow",
"description": "I have no idea, so I decided to copy code from Stackoverflow for my new project",
"date_created": "2021-05-10T10:46:37.932394Z",
"complete": false
}
ตอนนี้สามารถสร้าง CRUD แอพได้เรียบร้อยแล้ว แต่ว่ายังเหลือระบบ permissions และ authentication เหตุผลก็เพราะว่า
เรียกใช้งาน permissions จาก
rest_framework เพื่อทำการกำหนด permission ให้กับ user
จากนั้นทำการกำหนดตัวแปร permisssion_classes โดยกำหนดเป็น permisssions.IsAuthenticated
คือต้องมีการ login เข้ามาเท่านั้นถึงจะสามารถเข้าถึงหน้านี้ได้
from rest_framework import generics, permissions # New
...
class TaskList(generics.ListCreateAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
# Only an authenticated user can access this API endpoint
permission_classes = [permissions.IsAuthenticated]
...
user ที่ไม่ได้ทำการล็อกอิน หรือไม่มี permission จะไม่สามารถเข้าถึงหน้านี้ได้
http://127.0.0.1:8080/api/tasks/
HTTP 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"detail": "Authentication credentials were not provided."
}
นอกจากนี้การล็อกอินผ่าน Browsable API นอกจากสะดวกแล้ว ยังทำให้รู้ได้ว่า user คนไหนที่กำลังใช้งานอยู่ โดยการตั้งค่า login/logout ผ่าน Browsable API ก็ทำได้ง่าย ๆ โดยทำการกำหนดเพิ่มใน URL ของโปรเจคท์
mysite/urls.py
urlpatterns = [
...
path('api-auth/', include('rest_framework.urls')) # New
]
Note: ชื่อ URL end-point จะต้ั้งเป็นชื่อไหนก็ได้ตามต้องการไม่จำเป็นต้องเป็น แต่ใน URL ใน Docs ที่กำหนดมาก็ถือว่า make sense ไม่มีเหตุผลต้องเปลี่ยน
ทำการรีเฟรช จะพบว่ามีข้อความแสดงสถานะการ login หรือไม่ ผ่าน Browsable API
Log in ผ่าน Browsable API
ซึ่งแน่นอนว่าตอนนี้มี user เพียงแค่ 1 นั่นก็คือ sonny และ user คนนี้มี task อยู่ทั้งสิ้น 2 tasks
Tasks ทั้งหมดของ sonny
สร้าง user ขึ้นมาอีก 1 user โดยสามารถสร้างผ่านด้วยวิธีการที่ง่ายที่สุดคือสร้างผ่านหน้าแอดมินได้เลย
เสร็จแล้ว SAVE
เพิ่ม user เข้ามาอีก 1 รวมเป็น 2 เพื่อทดสอบ
ตอนนี้มี 2 users คือ sonny และ Sam เรียบร้อยพร้อมทดสอบ จากนั้นให้ทำการล็อกอินผ่าน user ชื่อ Sam และสร้าง task ขึ้นมา 1 task
HTTP 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"id": 3,
"title": "This is a task from Sam",
"description": "A new task created by Sam",
"date_created": "2021-05-12T05:32:56.891877Z",
"complete": false
}
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"id": 3,
"title": "This is a task from Sam",
"description": "A new task created by Sam",
"date_created": "2021-05-12T05:32:56.891877Z",
"complete": false
},
{
"id": 2,
"title": "Read articles on FreeCodeCamp",
"description": "I must read articles on FreeCodeCamp everyday to boost my programming knowledge",
"date_created": "2021-05-10T11:01:00.844176Z",
"complete": true
},
{
"id": 1,
"title": "Copy code from Stackoverflow",
"description": "I have no idea, so I decided to copy code from Stackoverflow for my new project",
"date_created": "2021-05-10T10:46:37.932394Z",
"complete": false
}
]
ทดสอบเข้าดูที่ URL จะสังเกตว่าพอเข้ามาที่ URL end-point นี้แล้วมีสามารถทำได้ทุกอย่างไม่ว่าจะเป็นดึงข้อมูล (GET), เพิ่มข้อมูล (POST), อัปเดตข้อมูล (PUT), ลบข้อมูล (DELETE), etc
http://127.0.0.1:8080/api/tasks/1
Sam สามารถแก้ไข task ของ sonny ได้ ?
จะเห็นว่า Sam สามารถเข้าดูข้อมูลของ sonny และสามารถอัปเดตหรือลบข้อมูลในนี้ได้ ซึ่ง use case จริง ๆ ไม่ควรเป็นแบบนี้ ควรจะเป็น owner ของ task นั้น ๆ ที่มี permission ในการทำแบบนั้นได้เท่านั้น
ถือว่าจบไปแล้วเซสชั่นแรกกับ Django REST Framework - Tutorial ฉบับเต็ม ซึ่งในบทความนี้จริง ๆ แล้วยังไม่จบเพียงเท่านี้ครับ ยังมีอีก 2 - 3 หัวข้อที่จะมาทำการอัปเดตให้เพิ่มเติมคือ Authentication, Deployment (Heroku), etc ติดตามการอัปเดตในครั้งถัดไปได้เลยครับ และจะมีการเพิ่ม Vue.js สำหรับ Front-end ในการเรียกใช้งาน API ที่ได้สร้างไว้ผ่านไลบรารีที่มีชื่อว่า axios
สำหรับเพื่อน ๆ ที่อ่านจบแล้ว ถ้ารู้สึกชอบหรืออยากให้เขียนต่อให้จบไว ๆ หรือมีคำแนะนำตรงส่วนไหน ก็สามารถคอมเมนต์ไปที่โพสต์นี้ เพื่อส่งสารโดยตรงให้ผม ได้ปลุกไฟในตัว แล้วลุยเขียนให้จบสิ้นสนองความต้องการของผู้อ่านกันครับ ไปที่โพสต์บน Facebook
บทความแนะนำ/อ่านเพิ่มเติม Django REST Framework by Wasin
ปล.บทความด้านบนเป็นอีกบทความ Django REST Framework ฉบับภาษาไทยที่เขียนได้ค่อนข้างละเอียดมาก ๆ โดยพี่ Toey Wasin ที่จะสอนในคลาส Live ผ่าน Google Meet ในวันที่ 29 พฤษภาคมนี้ กับคอร์สสร้าง RESTful APIs ด้วย Django REST Framework with React.js สำหรับใครที่สนใจหรือยังไม่ได้ลงทะเบียนรีบด่วนครับ มีโปรโมชั่นอยู่
References
[ redhat.com ] - What is a REST API
[ djangostars.com ] - Why Use the Django REST Framework
[ developer.mozilla.org ] - HTTP Request Methods
[ developer.mozilla.org ] - HTTP Response Status Codes
[ stackoverflow.com ] - Django or Django REST Framework
[ django-rest-framework.org ] - Authentication & Permissions
[ django-rest-framework.org ] - Class-Based Views
กิจกรรมที่กำลังจะมาถึง
ไม่พลาดกิจกรรมเด็ด ๆ ที่น่าสนใจ
Event นี้จะเริ่มขึ้นใน April 25, 2023
รายละเอียดเพิ่มเติม/สมัครเข้าร่วมคอร์สเรียนไพธอนออนไลน์ที่เราได้รวบรวมและได้ย่อยจากประสบการณ์จริงและเพื่อย่นระยะเวลาในการเรียนรู้ ลองผิด ลองถูกด้วยตัวเองมาให้แล้ว เพราะเวลามีค่าเป็นอย่างยิ่ง พร้อมด้วยการซัพพอร์ตอย่างดี