本節內容
- 路由系統
- models模型
- admin
- views視圖
- template模板
引子
講django的models之前, 先來想一想, 讓你通過django操作數據庫,你怎麼做? 做苦思冥想,可能會這樣寫。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import pymysql def index(request): # 創建連接 conn = pymysql.connect(host = '127.0.0.1' , port = 3306 , user = 'root' , passwd = 'alex123' , db = 'luffy_dev' ) # 創建遊標 cursor = conn.cursor() cursor.execute( "select username,email,mobile from web_account" ) data_set = cursor.fetchall() cursor.close() conn.close() return HttpResponse(data_set) |
很方便就實現了從數據庫裏取數據,事實上,很多人確實就是這麼做的。但這樣做會帶來2個問題
- SQL注入危險,因爲有的時候你操作數據庫的語句不是寫死在代碼裏的,而是通過前端傳參數拼接的,這就給黑客有了可趁之機,通過拼接參數實現sql注入。
- 語句跟代碼揉在一起了,增加後續維護成本
那怎麼辦呢?ORM提供了新思路。
什麼是ORM呢?
對象關係映射(Object Relational Mapping),它的實質就是將關係數據(庫)中的業務數據用對象的形式表示出來,並通過面向對象(Object-Oriented)的方式將這些對象組織起來,實現系統業務邏輯的過程。
在ORM過程中最重要的概念是映射(Mapping),通過這種映射可以使業務對象與數據庫分離。從面向對象來說,數據庫不應該和業務邏輯綁定到一起,ORM則起到這樣的分離作用,使數據庫層透明,開發人員真正的面向對象。
上面的解釋有點矇蔽對不?其實你只需要抓住2個關鍵詞, “映射” 和 “對象”,就能知道orm是什麼幹什麼的了。
- 映射(Mapping) —— 把表結構映射成類
- 對象 —— 像操作類對象一樣,操作數據庫裏的數據
映射
看下面的圖,就是直觀的例子,把右邊的表結構映射成了左邊的類
Sql語句到對象
ORM可以使你不用再寫原生SQL, 而是像操作對象一樣就可以實現對錶裏數據的增刪改查
好棒棒,媽媽再也不用逼你寫原生sql啦!
但是不要開心太早,ORM確實提高了開發效率,並且降低了數據操作與代碼之間的耦合,不過有利就有弊,我們總結一下orm的優缺點。
優點:
- 實現了代碼與數據操作的解耦合
- 不需自己寫原生sql, 提高開發效率
- 防止SQL注入, 通過對象操作的方式,默認就是防止sql注入的。
缺點:
- 犧牲性能, 對象到原生SQL勢必會有轉換消耗,對性能有一定的影響
- 複雜語句力不從心, 一些複雜的sql語句,用orm對象操作的方式很難實現,就還得用原生sql
講Django爲什麼說ORM? 哈, 好啦,是時候該引出主角啦,因爲Django的models基於架構ORM實現的。
Models模型
Django 的models把數據庫表結構映射成了一個個的類, 表裏的每個字段就是類的屬性。我們都知道數據庫有很多字段類型,int,float,char等, Django的models類針對不同的字段也設置了不同的類屬性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
AutoField #An IntegerField that automatically increments according to available IDs BigAutoField #A 64-bit integer, guaranteed to fit numbers from 1 to 9223372036854775807. BigIntegerField #-9223372036854775808 to 9223372036854775807 BinaryField #A field to store raw binary data. It only supports bytes assignment BooleanField CharField DateField #e.g 2019-04-27 DateTimeField #e.g 2019-04-27 17:53:21 DecimalField DurationField #storing periods of time ,e.g [DD] [HH:[MM:]]ss[.uuuuuu]" EmailField FileField #存儲文件 FloatField ImageField #Inherits all attributes and methods from FileField, but also validates that the uploaded object is a valid image. IntegerField GenericIPAddressField #IP地址,支持ipv4 NullBooleanField #Like a BooleanField, but allows NULL as one of the options PositiveIntegerField #Like an IntegerField, but must be either positive or zero (0). Values from 0 to 2147483647 PositiveSmallIntegerField #only allows positive values from 0 to 32767 SlugField # A slug is a short label for something, containing only letters, numbers, underscores or hyphens. SmallIntegerField TextField #A large text field. TimeField #A time, represented in Python by a datetime.time instance. URLField UUIDField #A field for storing universally unique identifiers. Uses Python’s UUID class. |
除了普通的表字段,針對外鍵也有映射
1
2
3
4
|
ForeignKey # 外鍵關聯 ManyToManyField #多對多 OneToOneField # 1對1 |
好啦,接下來就用django的orm來設計一個博客表。
需求
- 每個用戶有自己的賬戶信息
- 用戶可以發文章
- 文章可以打多個標籤
根據需求,我們設計3張表
注意Article表和Tag表是屬於多對多關係,什麼是多對多?即一個文章有多個標籤,一個標籤又可以屬於多個文章。
比如上圖的Article表中id爲3的文章 ,它的標籤是4,26, 即投資、大文娛、社交, 你看“投資”這個標籤同時還屬於文章2。 這就是多對多關係 , 即many to many .
那這種多對多的關係如何在表中存儲呢?難道真的像上圖中一樣,在Article表中加個tags字段,關聯Tag表裏的多條數據,通過逗號區分?
這倒確實是個解決辦法。但是也有問題,一個字段裏存多條紀錄的id,就沒辦法做查詢優化了。比如不能做索引等。
所以若想實現多對多關係的高效存儲+查詢優化,可以在Article and Tag表之間再搞出一張表。
這樣是不是就實現了多對多關聯?
yes, 沒錯, django也是這麼做的, django 有個專門的字段,叫ManyToManyField, 就是用來實現多對多關聯的,它會自動生成一個如上圖一樣的第3張表來存儲多對多關係。
正式的表結構
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
from django.db import models # Create your models here. class Account(models.Model): username = models.CharField(max_length = 64 ,unique = True ) email = models.EmailField() password = models.CharField(max_length = 128 ) register_date = models.DateTimeField( "註冊日期" ,auto_now_add = True ) signature = models.CharField(verbose_name = "簽名" ,max_length = 128 ,blank = True ,null = True ) class Article(models.Model): """文章表""" title = models.CharField(max_length = 255 ,unique = True ) content = models.TextField( "文章內容" ) account = models.ForeignKey( "Account" ,verbose_name = "作者" ,on_delete = models.CASCADE) tags = models.ManyToManyField( "Tag" ,blank = True ) pub_date = models.DateTimeField() read_count = models.IntegerField(default = 0 ) class Tag(models.Model): """文章標籤表""" name = models.CharField(max_length = 64 ,unique = True ) date = models.DateTimeField(auto_now_add = True ) |
我們發現,每個字段其實都是一個獨立的對象,一張表其實是很多類的組合。
上面好多字段裏還跟了些參數,我們來看以下常用的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
null #If True, Django will store empty values as NULL in the database. Default is False. blank #If True, the field is allowed to be blank. Default is False. db_column #The name of the database column to use for this field. If this isn’t given, Django will use the field’s name. db_index #If True, a database index will be created for this field. default #The default value for the field. This can be a value or a callable object. If callable it will be called every time a new object is created. editable # django admin中用,後面講 help_text # django admin中用,後面講 primary_key # If True, this field is the primary key for the model. unique #If True, this field must be unique throughout the table unique_for_date #Set this to the name of a DateField or DateTimeField to require that this field be unique for the value of the date field. For example, if you have a field title that has unique_for_date="pub_date", then Django wouldn’t allow the entry of two records with the same title and pub_date. unique_for_month #Like unique_for_date, but requires the field to be unique with respect to the month. unique_for_year verbose_name #A human-readable name for the field. If the verbose name isn’t given, Django will automatically create it using the field’s attribute name |
還有幾個特殊的字段屬性需要單獨介紹下
choices
An iterable (e.g., a list or tuple) consisting itself of iterables of exactly two items (e.g. [(A, B), (A, B) ...]) to use as choices for this field.
The first element in each tuple is the actual value to be set on the model, and the second element is the human-readable name.
1
2
3
4
5
6
7
8
9
10
11
12
|
class Student(models.Model): YEAR_IN_SCHOOL_CHOICES = ( ( 'FR' , 'Freshman' ), ( 'SO' , 'Sophomore' ), ( 'JR' , 'Junior' ), ( 'SR' , 'Senior' ), ) year_in_school = models.CharField( max_length = 2 , choices = YEAR_IN_SCHOOL_CHOICES, default = FRESHMAN, ) |
ForeignKey.on_delete
當一條記錄關聯的外鍵紀錄被刪除時,django 也會根據外鍵關聯限制的配置來決定如何處理當前這條紀錄。舉例,如果你有個可以爲null的外鍵關聯,並且你想在本紀錄關聯的數據被刪除時,把當前紀錄的關聯字段設爲null,那就配置如下
1
2
3
4
5
6
|
user = models.ForeignKey( User, on_delete = models.SET_NULL, blank = True , null = True , ) |
這個on_delete就是決定在關聯對象被刪除時,如何處理當前紀錄的,常用的參數如下:
- CASCADE——Cascade deletes. Django emulates the behavior of the SQL constraint ON DELETE CASCADE and also deletes the object containing the ForeignKey.
- PROTECT——Prevent deletion of the referenced object by raising ProtectedError, a subclass of django.db.IntegrityError.
- SET_NULL——Set the ForeignKey null; this is only possible if null is True.
- SET_DEFAULT——Set the ForeignKey to its default value; a default for the ForeignKey must be set.
配置Django數據庫連接信息
Django支持多種數據庫,Sqlite、Mysql、Oracle、PostgreSQL,默認的是小型文件數據庫Sqlite
1
2
3
4
5
6
|
DATABASES = { 'default' : { 'ENGINE' : 'django.db.backends.sqlite3' , 'NAME' : os.path.join(BASE_DIR, 'db.sqlite3' ), } } |
咱們是幹大事的人,怎麼也得用個Mysql呀, 改成mysql 也so easy.
1
2
3
4
5
6
7
8
9
10
|
DATABASES = { 'default' : { 'ENGINE' : 'django.db.backends.mysql' , 'NAME' : 'my_db' , 'USER' : 'mydatabaseuser' , 'PASSWORD' : 'mypassword' , 'HOST' : '127.0.0.1' , 'PORT' : '3306' , } } |
不過注意,python3 連接mysql的得使用pymysql,MysqlDB模塊300年沒更新了,但django默認調用的還是MySQLdb, so pymysql有個功能可以讓django以爲是用了MySQLdb. 即在項目目錄下的__init__.py中加上句代碼就好
1
2
3
|
import pymysql pymysql.install_as_MySQLdb() |
不加的話,一會連接數據時會報錯噢 。
同步數據庫
你在ORM定義的表結構如何同步到真實的數據庫裏呢? 只需2條命令。但django只能幫你自動創建表,數據庫本身還是得你自己來。
1
|
create database my_db charset utf8; |
好了,可以同步了,說好只需2步。
1. 生成同步文件, django自帶一個專門的工具叫migrations, 負責把你的orm錶轉成實際的表結構,它不旦可以幫自動創建表,對錶結構的修改,比如增刪改字段、改字段屬性等也都能自動同步。只需通過下面神奇的命令。
1
|
python manage.py makemigrations |
不出意外的話,會顯示類似以下信息
1
2
3
4
5
6
7
|
$ python manage.py makemigrations Migrations for 'app01' : app01 /migrations/0001_initial .py - Create model Account - Create model Article - Create model Tag - Add field tags to article |
此時你會發現,你的app下的migrations目錄裏多了一個0001_initial.py的文件 ,這個文件就是因爲你這條命令而創建的,migrations工具就會根據這個文件來創建數據庫裏的表。
2. 同步到數據
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
$ python manage.py migrate Operations to perform: Apply all migrations: admin, app01, 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 app01.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 sessions.0001_initial... OK (venv_django2) Alexs-MacBook-Pro:mysite alex$ |
此時登錄你的數據庫,會發現創建了好多張表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
mysql> show tables; +----------------------------+ | Tables_in_luffy_dev2 | +----------------------------+ | app01_account | #對應Account表 | app01_article | #對應Article表 | app01_article_tags | #自動創建的Article to Tag的多對多關聯表 | app01_tag | #對應Tag表 | auth_group | #下面這些,都是django 自帶的表,這個是自動用戶系統的組 | auth_group_permissions | #自帶的組與權限的多對多關聯表 | auth_permission | #自帶權限表 | auth_user | #用戶表 | auth_user_groups | | auth_user_user_permissions | | django_admin_log | #現在你的無法理解 | django_content_type | #現在你的無法理解 | django_migrations | #紀錄migartions工具同步紀錄的表 | django_session | #現在你的無法理解 +----------------------------+ 14 rows in set (0.00 sec) |
好啦,表結構也有了,我們可以往裏面插數據啦。
之前說好的是可以不用SQL語句的,一點不騙你。
用orm對錶數據進行增刪改查
先進入已經連接好數據庫的django python環境
1
2
3
4
5
6
7
|
(venv_django2) Alexs-MacBook-Pro:mysite alex$ python manage.py shell Python 3.5.2 (v3.5.2:4def2a2901a5, Jun 26 2016, 10:47:25) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin Type "help" , "copyright" , "credits" or "license" for more information. (InteractiveConsole) >>> >>> from app01 import models |
創建
創建數據簡單的令人髮指
查
filter 支持很多的過濾條件,我們來看下:
contains
包含,相當於sql的like條件
1
|
Entry.objects.get(headline__contains= 'Lennon' ) |
SQL equivalent:
1
|
SELECT ... WHERE headline LIKE '%Lennon%' ; |
Note this will match the headline 'Lennon honored today' but not 'lennon honored today'.
icontains 大小寫不敏感
in
In a given iterable; often a list, tuple, or queryset.
1
|
Entry.objects. filter (id__in = [ 1 , 3 , 4 ]) |
SQL equivalent:
1
|
SELECT ... WHERE id IN (1, 3, 4); |
You can also use a queryset to dynamically evaluate the list of values instead of providing a list of literal values:
1
2
|
inner_qs = Blog.objects. filter (name__contains = 'Cheddar' ) entries = Entry.objects. filter (blog__in = inner_qs) |
This queryset will be evaluated as subselect statement:
1
|
SELECT ... WHERE blog.id IN ( SELECT id FROM ... WHERE NAME LIKE '%Cheddar%' ) |
gt
1
|
Entry.objects. filter (id__gt = 4 ) |
SQL equivalent:
1
|
SELECT ... WHERE id > 4; |
gte
Greater than or equal to.
lt
Less than.
lte
Less than or equal to.
startswith
Case-sensitive starts-with.
1
|
Entry.objects. filter (headline__startswith = 'Lennon' ) |
SQL equivalent:
1
|
SELECT ... WHERE headline LIKE 'Lennon%' ; |
SQLite doesn’t support case-sensitive LIKE statements; startswith acts like istartswith for SQLite
istartswith
Case-insensitive starts-with.
endswith
Case-sensitive ends-with.
iendswith
Case-insensitive ends-with
range
區間過渡,可對數字、日期進行過濾
1
2
3
4
|
import datetime start_date = datetime.date( 2005 , 1 , 1 ) end_date = datetime.date( 2005 , 3 , 31 ) Entry.objects. filter (pub_date__range = (start_date, end_date)) |
SQL equivalent:
1
|
SELECT ... WHERE pub_date BETWEEN '2005-01-01' and '2005-03-31' ; |
Warning!
Filtering a DateTimeField with dates won’t include items on the last day, because the bounds are interpreted as “0am on the given date”. If pub_date was a DateTimeField, the above expression would be turned into this SQL:
SELECT ... WHERE pub_date BETWEEN '2005-01-01 00:00:00' and '2005-03-31 00:00:00';
Generally speaking, you can’t mix dates and datetimes.
date
For datetime fields, casts the value as date. Allows chaining additional field lookups. Takes a date value.
1
2
|
Entry.objects. filter (pub_date__date = datetime.date( 2005 , 1 , 1 )) Entry.objects. filter (pub_date__date__gt = datetime.date( 2005 , 1 , 1 )) |
year
For date and datetime fields, an exact year match. Allows chaining additional field lookups. Takes an integer year.
1
2
|
Entry.objects. filter (pub_date__year = 2005 ) Entry.objects. filter (pub_date__year__gte = 2005 ) |
SQL equivalent:
1
2
|
SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31' ; SELECT ... WHERE pub_date >= '2005-01-01' ; |
When USE_TZ is True, datetime fields are converted to the current time zone before filtering. 簡單解決辦法是把USE_TZ=False
month
For date and datetime fields, an exact month match. Allows chaining additional field lookups. Takes an integer 1 (January) through 12 (December).
1
2
|
Entry.objects. filter (pub_date__month = 12 ) Entry.objects. filter (pub_date__month__gte = 6 ) |
When USE_TZ
is True
, datetime fields are converted to the current time zone before filtering. This requires time zone definitions in the database.
SQL equivalent:
1
2
|
SELECT ... WHERE EXTRACT( 'month' FROM pub_date) = '12' ; SELECT ... WHERE EXTRACT( 'month' FROM pub_date) >= '6' ; |
day
For date and datetime fields, an exact day match. Allows chaining additional field lookups. Takes an integer day.
1
2
|
Entry.objects.filter(pub_date__day=3) Entry.objects.filter(pub_date__day__gte=3) |
SQL equivalent:
1
2
|
SELECT ... WHERE EXTRACT( 'day' FROM pub_date) = '3' ; SELECT ... WHERE EXTRACT( 'day' FROM pub_date) >= '3' ; |
week
For date and datetime fields, return the week number (1-52 or 53) according to ISO-8601, i.e., weeks start on a Monday and the first week contains the year’s first Thursday.
Example:
1
2
|
Entry.objects. filter (pub_date__week = 52 ) Entry.objects. filter (pub_date__week__gte = 32 , pub_date__week__lte = 38 ) |
week_day
For date and datetime fields, a ‘day of the week’ match. Allows chaining additional field lookups.
Takes an integer value representing the day of week from 1 (Sunday) to 7 (Saturday).
Example:
1
2
|
Entry.objects.filter(pub_date__week_day=2) Entry.objects.filter(pub_date__week_day__gte=2) |
hour
For datetime and time fields, an exact hour match. Allows chaining additional field lookups. Takes an integer between 0 and 23.
Example:
1
2
3
|
Event.objects. filter (timestamp__hour = 23 ) Event.objects. filter (time__hour = 5 ) Event.objects. filter (timestamp__hour__gte = 12 ) |
SQL equivalent:
1
2
3
|
SELECT ... WHERE EXTRACT( 'hour' FROM timestamp ) = '23' ; SELECT ... WHERE EXTRACT( 'hour' FROM time ) = '5' ; SELECT ... WHERE EXTRACT( 'hour' FROM timestamp ) >= '12' ;同 |
同時,還支持mintue,second
1
2
3
4
|
Event.objects.filter(time__minute=46) Event.objects.filter(timestamp__second=31) |
isnull
Takes either True
or False
, which correspond to SQL queries of IS NULL
and IS NOT NULL
, respectively.
Example:
1
|
Entry.objects. filter (pub_date__isnull = True ) |
SQL equivalent:
1
|
SELECT ... WHERE pub_date IS NULL ; |
regex
Case-sensitive regular expression match.
Example:
1
|
Entry.objects.get(title__regex = r '^(An?|The) +' ) |
SQL equivalents:
1
2
3
4
5
6
7
|
SELECT ... WHERE title REGEXP BINARY '^(An?|The) +' ; -- MySQL SELECT ... WHERE REGEXP_LIKE(title, '^(An?|The) +' , 'c' ); -- Oracle SELECT ... WHERE title ~ '^(An?|The) +' ; -- PostgreSQL SELECT ... WHERE title REGEXP '^(An?|The) +' ; -- SQLite |
iregex 大小寫不敏感
改刪
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 批量修改 models.Account.objects. filter (username = 'elina' ).update(password = "Luffy#21" ) # 單條修改 obj = models.Account.objects.get(username = 'linux' ) obj.username = 'python' obj.save() # 批量刪除 models.User.objects.get(password = 'oldboy' ).delete() # 單條刪除 obj = models.User.objects.get( id = 3 ) obj.delete() |
數據返回後的展示
values()
Returns a QuerySet
that returns dictionaries, rather than model instances, when used as an iterable.
1
2
3
4
|
>>> Blog.objects.values() <QuerySet [{ 'id' : 1 , 'name' : 'Beatles Blog' , 'tagline' : 'All the latest Beatles news.' }]> >>> Blog.objects.values( 'id' , 'name' ) <QuerySet [{ 'id' : 1 , 'name' : 'Beatles Blog' }]> |
order_by()
By default, results returned by a QuerySet
are ordered by the ordering tuple given by the ordering
option in the model’s Meta
. You can override this on a per-QuerySet
basis by using the order_by
method.
1
|
Entry.objects. filter (pub_date__year = 2005 ).order_by( '-pub_date' , 'headline' ) |
The result above will be ordered by pub_date
descending, then by headline
ascending. The negative sign in front of "-pub_date"
indicates descending order. Ascending order is implied.
reverse()
Use the reverse()
method to reverse the order in which a queryset’s elements are returned. Calling reverse()
a second time restores the ordering back to the normal direction.
To retrieve the “last” five items in a queryset, you could do this:
1
|
my_queryset.reverse()[: 5 ] |
ORM對象操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
單表對象操作 o = models.Article.objects. all ()[ 0 ] o.tilte 外鍵關聯 >>> o.account.username 'jack' >>> o.account.username = rain 外鍵反向關聯操作 >>> a = models.Account.objects.get(username = 'alex' ) >>> a.article_set. all () <QuerySet [<Article: 你好, 2018 >]> >>> a.article_set.select_related() <QuerySet [<Article: 你好, 2018 >]> 多對多操作 >>> o = models.Article.objects. all ()[ 1 ] >>> o.tags. all () <QuerySet [<Tag: 投資>, <Tag: 科技>]> 多對多反向操作 >>> t = models.Tag.objects.get(name = "投資" ) >>> t.article_set. all () <QuerySet [<Article: 你好, 2018 >, <Article: 粉絲超過 10 萬後,我經歷了抖音盜號風波>]> |
好啦,orm的操作先點到爲止,後面學項目時再帶你搞複雜的。
練習題
- 基於前面課程設計的表結構,完成以下練習:
- 創建5條account和5條新tag紀錄
- 創建5條article信息,關聯上面的不同的用戶和tag
- 在account表裏找到用戶名包含al的紀錄,然後把密碼改掉
- 在article表找到文章內容包含“電影”2個字的,把這些文章加上”大文娛”tag
- 把用戶elina發表的文章找出來,並且把作者都改成alex
- 找到用戶表裏註冊日期在2018-04月,並且signature爲空的紀錄
- 打到文章中標籤爲“投資”的所有文章
- 找到每個月8號註冊的用戶
- 找到每年5月發表的文章
- 找到2015-2017年5月發表的文章
- 找到文章作者以’a’或’k’開頭的文章