身為一個需要跟資料庫打交道的框架,怎麼可以在Model這邊草草結束呢!今天繼續來深入Model
我們今天會提到以下重點:
多資料庫可能會有幾種狀況:
以Django的角度來說,在技術上都是可行的
通常資料量非常大需要分表或是多租戶可能會用到第一點,需要維持查表效率或是隔離數據。第二種可以使app的操作更有彈性,但是配置上需要更加小心。使用第三種的話較為單純,這邊也示範第三種方法
python3 manage.py startapp chat
# settings.py
INSTALLED_APPS = [
...
# my apps
"article",
"chat",
]
class Chat(models.Model):
chat_id = models.AutoField(primary_key=True)
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
user = models.IntegerField()
class Meta:
app_label = "chat"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": DB_NAME,
"USER": DB_USER,
"PASSWORD": DB_PWD,
"HOST": DB_HOST,
"PORT": "5432",
},
"second_db": {
"ENGINE": "django.db.backends.postgresql",
"NAME": SECOND_DB_NAME,
"USER": SECOND_DB_USER,
"PASSWORD": SECOND_DB_PWD,
"HOST": SECOND_DB_HOST,
"PORT": "5432",
},
}
接下來還不能馬上進行遷移,因為makemigrations
只會確認default下的DB,因此我們需要配置DB的路由
dbRouter.py
from django.conf import settings
class DatabaseRouter:
def db_for_read(self, model, **hints):
if model._meta.app_label == "article":
return "default"
elif model._meta.app_label == "chat":
return "second_db"
else:
return "default"
def db_for_write(self, model, **hints):
if model._meta.app_label == "article":
return "default"
elif model._meta.app_label == "chat":
return "second_db"
else:
return "default"
def allow_relation(self, obj1, obj2, **hints):
"""
決定是否允許兩個對象之間建立關係。
對於跨數據庫的關係,我們需要更精確的控制。
"""
# 如果兩個對象在同一個數據庫,允許關係
if obj1._state.db == obj2._state.db:
return True
# 其他跨數據庫的關係默認不允許
return False
def allow_migrate(self, db, app_label, model_name=None, **hints):
"""
表示是否允許模型在指定資料庫上進行遷移操作
"""
if app_label == "article":
return db == "default"
elif app_label == "chat":
return db == "second_db"
elif app_label == "auth":
return db == "default"
return None
settings.py
中配置DATABASE_ROUTERS = ["django_project.dbRouter.DatabaseRouter"]
python manage.py makemigrations chat
python manage.py migrate chat --database=second_db
# shell 模式
>>> from chat.models import Chat
>>> chat = Chat(message="", user=1)
>>> chat.save()
# 如果都沒有設定路由,也可以使用如下方式
>>> Author.objects.using("default")
>>> Chat.objects.using("second_db")
一般狀況下,增加model並且遷移到實際的DB中需要我們寫程式碼並且執行遷移指令。但是有時候我們可能會想要動態的添加表格,可能操作人員需要後台有這樣的功能,又或是有一些排程下的資料需要不斷建立表格來輸入資料,此時能動態添加表格就是很重要的功能了。當然在執行操作之前,一定要確保相關的安全措施與用戶的權限設置等等
這邊加上type hint是更方便理解整個流程的運作
from django.db import models
from typing import Dict, Any, Optional, Type, Callable
def create_model(
name: str,
fields: Optional[Dict[str, Any]] = None,
app_label: str = "",
options: Optional[Dict[str, Any]] = None,
) -> Type[models.Model]:
"""
創建模型對象
:param name: 模型名稱
:param fields: 模型欄位
:param app_label: 應用名稱
:param options: 模型選項
:return: 模型對象
"""
class Meta:
pass
setattr(Meta, "app_label", app_label)
# 設置模型的meta選項
if options is not None:
for key, value in options.items():
setattr(Meta, key, value)
# 設置模型的欄位
attrs: Dict[str, Any] = {"__module__": f"{app_label}.models", "Meta": Meta}
if fields:
attrs.update(fields)
# 創建模型
model: Type[models.Model] = type(name, (models.Model,), attrs)
return model
def create_db(model: Type[models.Model]) -> None:
"""
創建資料表
:param model: 模型對象
"""
from django.db import connection
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
try:
with BaseDatabaseSchemaEditor(connection) as schema_editor:
schema_editor.create_model(model=model)
except Exception as e:
pass
def create_table(model_name: str) -> Type[models.Model]:
"""
創建資料表
:param model_name: 模型名稱
:return: 模型對象
"""
fields: Dict[str, Any] = {
"id": models.AutoField(primary_key=True),
"views": models.IntegerField(default=0),
"date": models.DateField(auto_now_add=True),
"article": models.ForeignKey("Article", on_delete=models.CASCADE),
"__str__": lambda self: f"{self.article.title}_{self.date}_views",
}
options: Dict[str, str] = {
"verbose_name": model_name,
"verbose_name_plural": model_name,
}
model: Type[models.Model] = create_model(
name=model_name, fields=fields, app_label="article", options=options
)
create_db(model)
return model
在create_model
中,我們根據傳入的參數來建造模型對象,並且使 python內置的type()
來建立
type(name, bases, dict)
這個函數有三個參數:
name
: 這是要創建的類的名稱bases
: 這是一個元組,包含新類要繼承的基類。(models.Model,)
,表示新創建的類將繼承 models.Model
dict
: 這是一個字典,包含類的屬性和方法。 attrs
字典中包含了 __module__
、Meta
以及所有的模型欄位而create_db
則是讓我們能夠在資料庫中建立我們設置好的模型類,其中BaseDatabaseSchemaEditor
是Django 用於執行數據庫架構更改的基礎類別
最後create_table
則是將整個流程定下來,調用create_model
後把model當作參數丟入create_db
中,最後返回model對象提供後續的操作
實際操作一次
def create_table_view(request):
today = time.localtime(time.time())
article = Article.objects.get(article_id=2)
model_name = f"{article.article_id}_{time.strftime('%Y%m%d', today)}_view"
new_model = create_table(model_name=model_name)
new_model.objects.create(
views=0,
date=today,
article=article,
)
return JsonResponse({"status": "success"})
可以看到確實新增了表格以及載入了數據
我們介紹了兩樣比較進階的資料庫操作方式
這兩者都能讓我們在開發中,面臨到動態的需求變更等都能更靈活的進行調整,滿足各式各樣的需求
目前為止,有稍微感受到Django對於資料庫使用ORM操作的魅力了嗎?除了程式碼簡潔且可讀性高之外,透過自定義的路由設置,讓我們在多變的系統下不用去修改原先的ORM語法,可以說是相當便利~
到此有關Model的部分先告一個段落,下一次再來討論資料庫的結構就會是之後的多租戶架構了
到目前為止,我們添加數據都是透過shell模式下進行操作,明天開始我們會透過表單來進行數據的添加。有了Django表單,除了在數據驗證的部分更加方便,在視圖中的程式碼也會更加的簡潔與易讀,可以期待一下
多資料庫使用:https://docs.djangoproject.com/en/5.1/topics/db/multi-db/
SchemaEditor:https://docs.djangoproject.com/en/5.1/ref/schema-editor/