第十章: 數據模型高級進階

第十章: 數據模型高級進階

在第5章裏,咱們介紹了Django的數據層如何定義數據模型以及如何使用數據庫API來建立、檢索、更新以及刪除記錄 在這章裏,咱們將向你介紹Django在這方面的一些更高級功能。html

相關對象

先讓咱們回憶一下在第五章裏的關於書本(book)的數據模型:python

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    def __unicode__(self):
        return self.name

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()

    def __unicode__(self):
        return u'%s %s' % (self.first_name, self.last_name)

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

    def __unicode__(self):
        return self.title

如咱們在第5章的講解,獲取數據庫對象的特定字段的值只需直接使用屬性。 例如,要肯定ID爲50的書本的標題,咱們這樣作:mysql

>>> from mysite.books.models import Book
>>> b = Book.objects.get(id=50)
>>> b.title
u'The Django Book'

可是,在以前有一件咱們沒說起到的是表現爲ForeignKey 或 ManyToManyField的關聯對象字段,它們的做用稍有不一樣。web

訪問外鍵(Foreign Key)值

當你獲取一個ForeignKey 字段時,你會獲得相關的數據模型對象。 例如:sql

>>> b = Book.objects.get(id=50)
>>> b.publisher
<Publisher: Apress Publishing>
>>> b.publisher.website
u'http://www.apress.com/'

對於用`` ForeignKey`` 來定義的關係來講,在關係的另外一端也能反向的追溯回來,只不過因爲不對稱性的關係而稍有不一樣。 經過一個`` publisher`` 對象,直接獲取 books ,用 publisher.book_set.all() ,以下:shell

>>> p = Publisher.objects.get(name='Apress Publishing')
>>> p.book_set.all()
[<Book: The Django Book>, <Book: Dive Into Python>, ...]

實際上,book_set 只是一個 QuerySet(參考第5章的介紹),因此它能夠像QuerySet同樣,能實現數據過濾和分切,例如:數據庫

>>> p = Publisher.objects.get(name='Apress Publishing')
>>> p.book_set.filter(name__icontains='django')
[<Book: The Django Book>, <Book: Pro Django>]

屬性名稱book_set是由模型名稱的小寫(如book)加_set組成的。django

訪問多對多值(Many-to-Many Values)

多對多和外鍵工做方式相同,只不過咱們處理的是QuerySet而不是模型實例。 例如,這裏是如何查看書籍的做者:服務器

>>> b = Book.objects.get(id=50)
>>> b.authors.all()
[<Author: Adrian Holovaty>, <Author: Jacob Kaplan-Moss>]
>>> b.authors.filter(first_name='Adrian')
[<Author: Adrian Holovaty>]
>>> b.authors.filter(first_name='Adam')
[]

反向查詢也能夠。 要查看一個做者的全部書籍,使用author.book_set ,就如這樣:架構

>>> a = Author.objects.get(first_name='Adrian', last_name='Holovaty')
>>> a.book_set.all()
[<Book: The Django Book>, <Book: Adrian's Other Book>]

這裏,就像使用 ForeignKey字段同樣,屬性名book_set是在數據模型(model)名後追加_set

更改數據庫模式(Database Schema)

在咱們在第5章介紹 syncdb 這個命令時, 咱們注意到 syncdb僅僅建立數據庫裏尚未的表,它 並不 對你數據模型的修改進行同步,也不處理數據模型的刪除。 若是你新增或修改數據模型裏的字段,或是刪除了一個數據模型,你須要手動在數據庫裏進行相應的修改。 這段將解釋了具體怎麼作:

當處理模型修改的時候,將Django的數據庫層的工做流程銘記於心是很重要的。

  • 若是模型包含一個不曾在數據庫裏創建的字段,Django會報出錯信息。 當你第一次用Django的數據庫API請求表中不存在的字段時會致使錯誤(就是說,它會在運行時出錯,而不是編譯時)。

  • Django關心數據庫表中是否存在未在模型中定義的列。

  • Django關心數據庫中是否存在未被模型表示的表格。

改變模型的模式架構意味着須要按照順序更改Python代碼和數據庫。

添加字段

當要向一個產品設置表(或者說是model)添加一個字段的時候,要使用的技巧是利用Django不關心表裏是否包含model裏所沒有的列的特性。 策略就是如今數據庫里加入字段,而後同步Django的模型以包含新字段。

然而 這裏有一個雞生蛋蛋生雞的問題 ,因爲要想了解新增列的SQL語句,你須要使用Django的 manage.py sqlall命令進行查看 ,而這又須要字段已經在模型裏存在了。 (注意:你並 不是非得使用與Django相同的SQL語句建立新的字段,可是這樣作確實是一個好主意 ,它能讓一切都保持同步。)

這個雞-蛋的問題的解決方法是在開發者環境裏而不是發佈環境裏實現這個變化。 (你使用的是測試/開發環境,對吧?)下面是具體的實施步驟。

首先,進入開發環境(也就是說,不是在發佈環境裏):

  1. 在你的模型裏添加字段。

  2. 運行 manage.py sqlall [yourapp] 來測試模型新的 CREATE TABLE 語句。 注意爲新字段的列定義。

  3. 開啓你的數據庫的交互命令界面(好比, psql 或mysql , 或者可使用 manage.py dbshell )。 執行 ALTER TABLE 語句來添加新列。

  4. 使用Python的manage.py shell,經過導入模型和選中表單(例如, MyModel.objects.all()[:5] )來驗證新的字段是否被正確的添加 ,若是一切順利,全部的語句都不會報錯。

而後在你的產品服務器上再實施一遍這些步驟。

  1. 啓動數據庫的交互界面。

  2. 執行在開發環境步驟中,第三步的ALTER TABLE語句。

  3. 將新的字段加入到模型中。 若是你使用了某種版本控制工具,而且在第一步中,已經提交了你在開發環境上的修改,如今,能夠在生產環境中更新你的代碼了(例如,若是你使用Subversion,執行svn update

  4. 從新啓動Web server,使修改生效。

讓咱們實踐下,好比添加一個num_pages字段到第五章中Book模型。首先,咱們會把開發環境中的模型改爲以下形式:

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()
    **num_pages = models.IntegerField(blank=True, null=True)**

    def __unicode__(self):
        return self.title

(注意 閱讀第六章的「設置可選字段」以及本章下面的「添加非空列」小節以瞭解咱們在這裏添加blank=Truenull=True的緣由。)

而後,咱們運行命令manage.py sqlall books 來查看CREATE TABLE語句。 語句的具體內容取決與你所使用的數據庫, 大概是這個樣子:

CREATE TABLE "books_book" (
    "id" serial NOT NULL PRIMARY KEY,
    "title" varchar(100) NOT NULL,
    "publisher_id" integer NOT NULL REFERENCES "books_publisher" ("id"),
    "publication_date" date NOT NULL,
    "num_pages" integer NULL
);

新加的字段被這樣表示:

"num_pages" integer NULL

接下來,咱們要在開發環境上運行數據庫客戶端,若是是PostgreSQL,運行 psql,,而後,我執行以下語句。

ALTER TABLE books_book ADD COLUMN num_pages integer;

添加 非NULL 字段

這裏有個微妙之處值得一提。 在咱們添加字段num_pages的時候,咱們使用了 blank=True 和 null=True 選項。 這是由於在咱們第一次建立它的時候,這個數據庫字段會含有空值。

然而,想要添加不能含有空值的字段也是能夠的。 要想實現這樣的效果,你必須先建立 NULL 型的字段,而後將該字段的值填充爲某個默認值,而後再將該字段改成 NOT NULL 型。 例如:

BEGIN;
ALTER TABLE books_book ADD COLUMN num_pages integer;
UPDATE books_book SET num_pages=0;
ALTER TABLE books_book ALTER COLUMN num_pages SET NOT NULL;
COMMIT;

若是你這樣作,記得你不要在模型中添加 blank=True 和 null=True 選項。

執行ALTER TABLE以後,咱們要驗證一下修改結果是否正確。啓動python並執行下面的代碼:

>>> from mysite.books.models import Book
>>> Book.objects.all()[:5]

若是沒有異常發生,咱們將切換到生產服務器,而後在生產環境的數據庫中執行命令ALTER TABLE 而後咱們更新生產環境中的模型,最後重啓web服務器。

刪除字段

從Model中刪除一個字段要比添加容易得多。 刪除字段,僅僅只要如下幾個步驟:

刪除字段,而後從新啓動你的web服務器。

用如下命令從數據庫中刪除字段:

ALTER TABLE books_book DROP COLUMN num_pages;

請保證操做的順序正確。 若是你先從數據庫中刪除字段,Django將會當即拋出異常。

刪除多對多關聯字段

因爲多對多關聯字段不一樣於普通字段,因此刪除操做是不一樣的。

從你的模型中刪除ManyToManyField,而後重啓web服務器。

用下面的命令從數據庫刪除關聯表:

DROP TABLE books_book_authors;

像上面同樣,注意操做的順序。

刪除模型

刪除整個模型要比刪除一個字段容易。 刪除一個模型只要如下幾個步驟:

從文件中刪除你想要刪除的模型,而後重啓web 服務器models.py

而後用如下命令從數據庫中刪除表:

DROP TABLE books_book;

當你須要從數據庫中刪除任何有依賴的表時要注意(也就是任何與表books_book有外鍵的表 )。

正如在前面部分,必定要按這樣的順序作。

Managers

在語句Book.objects.all()中,objects是一個特殊的屬性,須要經過它查詢數據庫。 在第5章,咱們只是簡要地說這是模塊的manager 。如今是時候深刻了解managers是什麼和如何使用了。

總之,模塊manager是一個對象,Django模塊經過它進行數據庫查詢。 每一個Django模塊至少有一個manager,你能夠建立自定義manager以定製數據庫訪問。

下面是你建立自定義manager的兩個緣由: 增長額外的manager方法,和/或修manager返回的初始QuerySet。

增長額外的Manager方法

增長額外的manager方法是爲模塊添加表級功能的首選辦法。 (至於行級功能,也就是隻做用於模型對象實例的函數,一下子將在本章後面解釋。)

例如,咱們爲Book模型定義了一個title_count()方法,它須要一個關鍵字,返回包含這個關鍵字的書的數量。 (這個例子有點牽強,不過它能夠說明managers如何工做。)

# models.py

from django.db import models

# ... Author and Publisher models here ...

**class BookManager(models.Manager):**
    **def title_count(self, keyword):**
        **return self.filter(title__icontains=keyword).count()**

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()
    num_pages = models.IntegerField(blank=True, null=True)
    **objects = BookManager()**

    def __unicode__(self):
        return self.title

有了這個manager,咱們如今能夠這樣作:

>>> Book.objects.title_count('django')
4
>>> Book.objects.title_count('python')
18

下面是編碼該注意的一些地方:

  • 咱們創建了一個BookManager類,它繼承了django.db.models.Manager。這個類只有一個title_count()方法,用來作統計。 注意,這個方法使用了self.filter(),此處self指manager自己。

  • 咱們把BookManager()賦值給模型的objects屬性。 它將取代模型的默認manager(objects)若是咱們沒有特別定義,它將會被自動建立。 咱們把它命名爲objects,這是爲了與自動建立的manager保持一致。

爲何咱們要添加一個title_count()方法呢?是爲了將常用的查詢進行封裝,這樣咱們就沒必要重複編碼了。

修改初始Manager QuerySets

manager的基本QuerySet返回系統中的全部對象。 例如,`` Book.objects.all()`` 返回數據庫book中的全部書本。

咱們能夠經過覆蓋Manager.get_query_set()方法來重寫manager的基本QuerySet。 get_query_set()按照你的要求返回一個QuerySet。

例如,下面的模型有* 兩個* manager。一個返回全部對像,另外一個只返回做者是Roald Dahl的書。

from django.db import models

**# First, define the Manager subclass.**
**class DahlBookManager(models.Manager):**
    **def get_query_set(self):**
        **return super(DahlBookManager, self).get_query_set().filter(author='Roald Dahl')**

**# Then hook it into the Book model explicitly.**
class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=50)
    # ...

    **objects = models.Manager() # The default manager.**
    **dahl_objects = DahlBookManager() # The Dahl-specific manager.**

在這個示例模型中,Book.objects.all()返回了數據庫中的全部書本,而 Book.dahl_objects.all()只返回了一本. 注意咱們明確地將objects設置成manager的實例,由於若是咱們不這麼作,那麼惟一可用的manager就將是dah1_objects。

固然,因爲get_query_set()返回的是一個QuerySet對象,因此咱們可使用filter(),exclude()和其餘一切QuerySet的方法。 像這些語法都是正確的:

Book.dahl_objects.all()
Book.dahl_objects.filter(title='Matilda')
Book.dahl_objects.count()

這個例子也指出了其餘有趣的技術: 在同一個模型中使用多個manager。 只要你願意,你能夠爲你的模型添加多個manager()實例。 這是一個爲模型添加通用濾器的簡單方法。

例如:

class MaleManager(models.Manager):
    def get_query_set(self):
        return super(MaleManager, self).get_query_set().filter(sex='M')

class FemaleManager(models.Manager):
    def get_query_set(self):
        return super(FemaleManager, self).get_query_set().filter(sex='F')

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female')))
    people = models.Manager()
    men = MaleManager()
    women = FemaleManager()

這個例子容許你執行`` Person.men.all()`` ,`` Person.women.all()`` ,`` Person.people.all()`` 查詢,生成你想要的結果。

若是你使用自定義的Manager對象,請注意,Django遇到的第一個Manager(以它在模型中被定義的位置爲準)會有一個特殊狀態。 Django將會把第一個Manager 定義爲默認Manager ,Django的許多部分(可是不包括admin應用)將會明確地爲模型使用這個manager。 結論是,你應該當心地選擇你的默認manager。由於覆蓋get_query_set() 了,你可能接受到一個無用的返回對像,你必須避免這種狀況。

模型方法

爲了給你的對像添加一個行級功能,那就定義一個自定義方法。 有鑑於manager常常被用來用一些整表操做(table-wide),模型方法應該只對特殊模型實例起做用。

這是一項在模型的一個地方集中業務邏輯的技術。

最好用例子來解釋一下。 這個模型有一些自定義方法:

from django.contrib.localflavor.us.models import USStateField
from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    birth_date = models.DateField()
    address = models.CharField(max_length=100)
    city = models.CharField(max_length=50)
    state = USStateField() # Yes, this is U.S.-centric...

    def baby_boomer_status(self):
        "Returns the person's baby-boomer status."
        import datetime
        if datetime.date(1945, 8, 1) <= self.birth_date <= datetime.date(1964, 12, 31):
            return "Baby boomer"
        if self.birth_date < datetime.date(1945, 8, 1):
            return "Pre-boomer"
        return "Post-boomer"

    def is_midwestern(self):
        "Returns True if this person is from the Midwest."
        return self.state in ('IL', 'WI', 'MI', 'IN', 'OH', 'IA', 'MO')

    def _get_full_name(self):
        "Returns the person's full name."
        return u'%s %s' % (self.first_name, self.last_name)
    full_name = property(_get_full_name)

例子中的最後一個方法是一個property。 想了解更多關於屬性的信息請訪問http://www.python.org/download/releases/2.2/descrintro/#property

這是用法的實例:

>>> p = Person.objects.get(first_name='Barack', last_name='Obama')
>>> p.birth_date
datetime.date(1961, 8, 4)
>>> p.baby_boomer_status()
'Baby boomer'
>>> p.is_midwestern()
True
>>> p.full_name  # Note this isn't a method -- it's treated as an attribute
u'Barack Obama'

執行原始SQL查詢

有時候你會發現Django數據庫API帶給你的也只有這麼多,那你能夠爲你的數據庫寫一些自定義SQL查詢。 你能夠經過導入django.db.connection對像來輕鬆實現,它表明當前數據庫鏈接。 要使用它,須要經過connection.cursor()獲得一個遊標對像。 而後,使用cursor.execute(sql, [params])來執行SQL語句,使用cursor.fetchone()或者cursor.fetchall()來返回記錄集。 例如:

>>> from django.db import connection
>>> cursor = connection.cursor()
>>> cursor.execute("""
...    SELECT DISTINCT first_name
...    FROM people_person
...    WHERE last_name = %s""", ['Lennon'])
>>> row = cursor.fetchone()
>>> print row
['John']

connectioncursor幾乎實現了標準Python DB-API,你能夠訪問` http://www.python.org/peps/pep-0249.html <http://www.python.org/peps/pep-0249.html>`__來獲取更多信息。 若是你對Python DB-API不熟悉,請注意在cursor.execute() 的SQL語句中使用`` 「%s」`` ,而不要在SQL內直接添加參數。 若是你使用這項技術,數據庫基礎庫將會自動添加引號,同時在必要的狀況下轉意你的參數。

不要把你的視圖代碼和django.db.connection語句混雜在一塊兒,把它們放在自定義模型或者自定義manager方法中是個不錯的主意。 好比,上面的例子能夠被整合成一個自定義manager方法,就像這樣:

from django.db import connection, models

class PersonManager(models.Manager):
    def first_names(self, last_name):
        cursor = connection.cursor()
        cursor.execute("""
            SELECT DISTINCT first_name
            FROM people_person
            WHERE last_name = %s""", [last_name])
        return [row[0] for row in cursor.fetchone()]

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    objects = PersonManager()

而後這樣使用:

>>> Person.objects.first_names('Lennon')
['John', 'Cynthia']
相關文章
相關標籤/搜索