Rails Model

Rails Model

https://guides.rubyonrails.org/

https://api.rubyonrails.org/

第一次寫於 : 2020年3月22日

忙碌了一段時間,這幾天根據官方文檔和API手冊,學習了一些Rails中對數據庫操作的內容;
也就是Active Record;

文章中大部分內容就是對文檔的翻譯和抽取; 同時,也加入了一些個人的理解;
翻譯過程,有些內容懶得翻譯了;

好的框架都是相似的,不好的框架各不相同;

學習過spring,JPA 等框架;
發現除了寫法上有差異,但是總體上類似;

ORM框架啊,都是對數據庫操作的封裝;
這裏,就不得不提, 學好SQL和數據庫原理對使用ORM框架是非常有幫助的;
當然,這兩者也是相輔相成的;

知道原理後,ORM就是對這些操作的封裝.使用起來會更加得心應手;

Active Record 基礎

命名約定

命名約定

模型Class 大駝峯單數
rails會自動生成 複數的名稱的table

模式約定

模型Class 大駝峯單數

創建 Active Record 模型

默認會添加三個字段

  • id
  • created_at
  • updated_at
rails g model User username:string

命令輸入完成後,會在 db/migrate 目錄下生成一個遷移文件
完善文件後 執行遷移命令

rails db:migrate

之後,我們就可以在 db/schema.rb 文件中看到我們聲明的model

記住: 這個文件只能看,不要手動修改;

覆蓋命名約定:self.table_name

取消默認命名;使用自定義的命名

class Product < ApplicationRecord
  self.table_name = "my_products"
end

CRUD

在官網上有詳細的介紹

https://guides.rubyonrails.org/active_record_basics.html

  • 創建
user = User.create(name: "David", occupation: "Code Artist")
# 只有當執行save方法時; 纔會把數據保存到數據庫中
user = User.new
user.name = "David"
user.occupation = "Code Artist"
user.save
  • 讀取
user = User.first
user = User.find_by_id(1)
  • 更新
user = User.first
user.update(username: "hello")
# 
user.name = 'Dave'
user.save

User.update_all "max_login_attempts = 3, must_change_password = 'true'"
  • 刪除
user = User.find_by(name: 'David')
user.destroy

# find and delete all users named David
User.destroy_by(name: 'David')
 
# delete all users
User.destroy_all

事務

Transactions

事務的開啓

儘管,事務是被每一個Active Record類調用的;
但是,由於一個事務屬於一個數據庫連接,而非一個class;
因此,在一個class中,寫不用的model調用也是可以的;

另外 savedestroy 方法,自動被事務包裹

我們傳入一個 block ; 在這個block中的操作都會被事務包裹;

分佈式事務, 不在Active Record 支持範圍;

Account.transaction do
  balance.save!
  account.save!
end

balance.transaction do
  balance.save!
  account.save!
end

事務中的異常處理

記住,如果一個事務block中拋出的異常,會在觸發ROLLBACK後傳播到上級;
注意要捕獲這些問題;

我們不要在事務的block中 捕獲ActiveRecord::StatementInvalid ;
如果捕獲了,可能會導致整個事務的block被廢棄;

# Suppose that we have a Number model with a unique column called 'i'.
Number.transaction do
  Number.create(i: 0)
  begin
    # This will raise a unique constraint error...
    Number.create(i: 0)
  rescue ActiveRecord::StatementInvalid
    # ...which we ignore.
  end

  # On PostgreSQL, the transaction is now unusable. The following
  # statement will cause a PostgreSQL error, even though the unique
  # constraint is no longer violated:
  Number.create(i: 1)
  # => "PG::Error: ERROR:  current transaction is aborted, commands
  #     ignored until end of transaction block"
end

內置事務

內置事務只有MS-SQL支持,Active Record 只是儘可能的模擬內置事務;
下面操作的結果可能會令你大喫一驚;

User.transaction do
  User.create(username: 'Kotori')
  User.transaction do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

這樣,會創建兩個對象 Kotori 和 Nemu;
儘管,拋出了異常,但是並沒有讓事務回滾;

在事務block中發生的異常,父級block看不到;導致了無法回滾;

下面, User.transaction(requires_new: true) 設置需要新的事務塊
可以正常回滾;
但是,實際上很多數據庫是不支持內置事務的,這只是模擬;

User.transaction do
  User.create(username: 'Kotori')
  User.transaction(requires_new: true) do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

Active Record 遷移

命令

If the migration name is of the form “AddColumnToTable” or “RemoveColumnFromTable” and
is followed by a list of column names and types then a migration containing the appropriate
add_column and remove_column statements will be created.

創建獨立的遷移

rails generate migration AddPartNumberToProducts

生成的遷移文件會有一個timestamp 前綴;
每次遷移都會在數據庫中生成一條記錄;
防止數據操作不一致;導致數據庫修改錯亂;

執行遷移命令

rails db:migrate

模型生成器:命令

rails g model User

創建數據表

一般都是通過創建model 來生成遷移文件

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :username
      t.string :password
      t.numeric :gender
      t.string :avatar_url
      t.string :email
      t.timestamps
    end
  end
end

修改字段

添加字段

rails generate migration AddPartNumberToProducts part_number:string

change_column command is irreversible. 改變column是不可逆的

class AddTestToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :test, :string
    remove_column :users,:test
    # change_column :users,:gender,:text
    add_index :users,:username
    change_column_null :products, :name, false
    change_column_default :products, :approved, from: true, to: false
  end
end

字段修飾符(重要)

column-modifiers

字段修改符 可以用在修改或者創建column時

  • limit Sets the maximum size of the string/text/binary/integer fields.
  • precision Defines the precision for the decimal fields, representing the total number of digits in the number.
  • scale Defines the scale for the decimal fields, representing the number of digits after the decimal point.
  • polymorphic Adds a type column for belongs_to associations.
  • null Allows or disallows NULL values in the column.
  • default Allows to set a default value on the column. Note that if you are using a dynamic value (such as a date), the default will only be calculated the first time (i.e. on the date the migration is applied).
  • comment Adds a comment for the column.
# change_column(:users, :password, :string, {:limit=>30, :default=>"123456", :comment=>"加入默認密碼"})
change_column :users, :password, :string, limit: 30, default: '123456'

execute

執行任意sql

Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")

change 方法

change 方法中; 可以使用之前所用的add remove字段等;

up 和 down 方法

這是一個過時的方法; 但也可以使用;

up 和 down 方法是成對出現的;

  • up 表示將要修改的內容
  • down 表示要回退的內容 rollback

例如: 在up中我給username 加入了索引; 那麼在 down中就要定義 刪除username的索引;

運行指定遷移

遷移

VERSION=時間戳

rails db:migrate VERSION=20080906120000

如果VERSION版本比現在的新
那麼它會執行 change 或 up 方法;

如果發現執行的代碼錯誤了
可以使用rollback 回滾

rails db:rollback

在不同環境中運行遷移

rails db:migrate RAILS_ENV=test

Active Record 查詢

方法名中 帶有 ! 標明可能會報異常
帶有 ? 返回 true/false

The find method will raise an ActiveRecord::RecordNotFound exception if no matching record is found.

查詢方法中返回一個集合 (例如 where , group ) 是 ActiveRecord::Relation 實例對象
查詢方法中返回一個對象 (例如 find , first ) 是 model 的一個單獨實例對象

單表查詢

active_record_querying

簡單查詢

# find 如果找不到會報異常
# SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
user = User.find(1)
user = User.find([1,2])
# find_by_id
user = User.find_by_id(1) 
# take 按照數據庫隱式排序提取前兩條
user = User.take(2) 
# first 主鍵asc 第一條
# SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
user = User.first
user = User.first(3)
# 返回第一條符合的數據
user = User.find_by(username: 'ju') 
# 默認0 , 1000
User.find_each(start: 2000, batch_size: 5000) do |user|
  NewsMailer.weekly(user).deliver_now
end

數據存在不存在

Client.exists?(1)
Client.exists?(id: [1,2,3])
Client.exists?(name: ['John', 'Sergei'])
Client.where(first_name: 'Ryan').exists?

count

Client.count
# SELECT COUNT(*) FROM clients
Client.where(first_name: 'Ryan', orders: { status: 'received' }).count 

where

不要使用參數拼接
這樣會有sql注入

Never ever put your arguments directly inside the conditions string.

# where
# 下面這麼查詢是不安全的
User.where("id = " + id) 
# 使用 ? 佔位符
Client.where("orders_count = ? AND locked = ?", params[:orders], false) 
# 加入參數
users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc)

佔位符

# 問號佔位符(?)
Client.where("orders_count = ? AND locked = ?", params[:orders], false)
# Placeholder Conditions
Client.where("created_at >= :start_date AND created_at <= :end_date",
  {start_date: params[:start_date], end_date: params[:end_date]})
# hash條件
Client.where(id:1)

覆蓋:unscope(瞭解)

用的比較少; 表示去掉一部分執行

# SELECT "articles".* FROM "articles" WHERE trashed = 0
Article.where(id: 10, trashed: false).unscope(where: :id)

only 表示只執行

# SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
Article.where('id > 10').limit(20).order('id desc').only(:order, :where)

select

默認查詢全部字段;
也可以顯式地指出要查詢的內容

Client.select(:viewable_by, :locked)
# OR
Client.select("viewable_by, locked")

# distinct 
Client.select(:name).distinct
# limit offset
Client.limit(5).offset(30)
# group 分組
Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
# having 條件
Order.select("date(created_at) as ordered_date, sum(price) as total_price").
  group("date(created_at)").having("sum(price) > ?", 100)
# includes

批量查詢

  • each 把數據都放到內存中; 當大數據量時,不應該使用這種方法;
User.all.each do |ele|
  # do something
end

下面的兩種; 是內存友好型的;

  • find_each

分批讀取 ele是一條數據
可以指定batch_size,start,finish

User.find_each do |ele|
  
end


User.find_each(batch_size: 5000) do |user|
  NewsMailer.weekly(user).deliver_now
end
  • find_in_batches

分批讀取 arr是數組

User.find_in_batches do |arr|
  
end

排序:order 多字段排序

默認 asc

Client.order(orders_count: :asc, created_at: :desc)
# OR
Client.order(:orders_count, created_at: :desc)
# OR
Client.order("orders_count ASC, created_at DESC")
# OR
Client.order("orders_count ASC", "created_at DESC")

NOT

Client.where.not(locked: true)

OR

Client.where(locked: true).or(Client.where(orders_count: [1,3,5]))

作用域 (重要)

scopes

scope

scope 可以把常用的查詢語句封裝起來;
可以model或相關聯的對象調用;

注意: scope 返回的都是 ActiveRecord::Relation 對象; 可以使用方法鏈一直點點點;
但是,普通的方法,如果返回的是false 等可能會造成NoMethodError異常!

-> 表示 lambda rocket

定義

class Article < ApplicationRecord
  scope :published,               -> { where(published: true) }
  scope :published_and_commented, -> { published.where("comments_count > 0") }
  # 帶有參數和判斷條件
  scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
end
Article.published # => [published articles]
Article.created_before Date.now
category = Category.first
category.articles.published # => [published articles belonging to this category]

注意:

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time) if time.present?
  end
end

看着是不是很像scope 但是有一個很不一樣的點;
scope 當返回 nil 時, 也會被處理成 ActiveRecord::Relation
防止調用時出現NoMethodError

default scope

如果想讓每一個請求默認加上一個條件;
那麼可以在 default_scope 中定義

class Client < ApplicationRecord
  default_scope { where("removed_at IS NULL") }
end

在執行update操作時,默認的scope不會起作用

The default_scope is also applied while creating/building a record when the scope
arguments are given as a Hash. It is not applied while updating a record. E.g.:

unscoped

當然; 有默認的查詢條件了;
同時,也會有 不想帶着默認查詢條件的情況;
unscoped 就是把默認條件去掉

注意: 要和 unscope 區分;

class Client < ApplicationRecord
  default_scope { where(active: true) }
end
 
Client.new          # => #<Client id: nil, active: true>
Client.unscoped.new # => #<Client id: nil, active: nil>

sql查詢:

find_by_sql

對於簡單的查詢,使用model 可以大大地提高我們的效率;
減少重複;

但是,對於複雜的查詢,多表聯合等;
雖然,使用model也可以做到;

但是,沒有SQL語句直接和容易理解;

在Rails 中也提供了接口; 讓我們直接使用sql

Client.find_by_sql("SELECT * FROM clients
  INNER JOIN orders ON clients.id = orders.client_id
  ORDER BY clients.created_at desc")

find_by_sql 會返回一個 數組
注意: 只有一條數據,也會返回一個數組

pluck

注意 pluck 查詢返回的是ruby數組;
數組中的元素也是普通的對象; 並不是ActiveRecord 對象
它適用於大數據量的查詢;

class Client < ApplicationRecord
  def name
    "I am #{super}"
  end
end
 
Client.select(:name).map &:name
# => ["I am David", "I am Jeremy", "I am Jose"]
 
Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]

# pluck 並不返回ActiveRecord
Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
 
Client.limit(1).pluck(:name)
# => ["David"] 

Dynamic Finders

通過方法名來自定義查詢;
用的不多;

Client.find_by_first_name_and_locked("Ryan", true)

Enum

枚舉 在一些場景還是挺方便的;
尤其是定義只有幾個值的小map

class Book < ApplicationRecord
  enum availability: [:available, :unavailable]
end

而且rails 還幫我們生成了定義enum的scope

# Both examples below query just available books.
Book.available
# or
Book.where(availability: :available)
 
book = Book.new(availability: :available)
book.available?   # => true
book.unavailable! # => true
book.available?   # => false

Method Chaining

在上面的文章中;
我們可以看到,鏈式方法的調用;
我們可以不斷地點點點,來加上條件,join table;
但是,需要在最後返回一個單獨的對象;
否則,無法使用鏈式調用,並且會報異常

retrieving-a-single-object

Null Relation


# The visible_articles method below is expected to return a Relation.
@articles = current_user.visible_articles.where(name: params[:name])

def visible_articles
 case role
 when 'Country Manager'
   Article.where(country: country)
 when 'Reviewer'
   Article.published
 when 'Bad User'
   Article.none # => returning [] or nil breaks the caller code in this case
 end
end

多表關聯

之前,我們看到的都是單表的查詢與處理;
那麼,ORM 肯定涉及到多表的關聯關係;

下面我們集中精力看一下多表是怎麼在Active Record中操作的

Rails中有6種關聯關係

association_basics.html

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

一對一 一對多 多對一

belongs_to associations must use the singular term.
If you used the pluralized form in the above example for
the author association in the Book model and tried to create the
instance by Book.create(authors: @author), you would be told that
there was an “uninitialized constant Book::Authors”.
This is because Rails automatically infers the class name from the association name.
If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too.

belongs_to

舉個例子: 假設一本書 (Book)有且僅有一個作者(Author)

Book belongs_to Author

那麼反過來,對嗎?

Author has_one Book

其實是不對的,因爲作者可能有兩本書;

那麼 has_one 可以用在什麼地方呢?

class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :authors do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :books do |t|
      t.belongs_to :author
      t.datetime :published_at
      t.timestamps
    end
  end
end

has_one

has_one 同樣是創建 一對一關係的;
但是在語義上有所不同;

我們也可以說

Book has_one Author

但是,我們不會這麼做;
因爲,兩者之間具有主次之分;

以 Book belongs_to Author

has_one 在創建表的時候,並不會創建實際的字段去關聯
belongs_to 則會創建一個字段去關聯 例如 author_id

has_many 表示一對多的關係;

一個作者可能有0 本書 也有可能有N本書;
注意books ; 要使用複數

那麼就可以有

Author has_many Books

class Author < ApplicationRecord
  has_many :books
end
class CreateAuthors < ActiveRecord::Migration[5.0]
  def change
    create_table :authors do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :books do |t|
      t.belongs_to :author
      t.datetime :published_at
      t.timestamps
    end
  end
end

Supplier has_one Account
Account has_one Account_history

那麼肯定也有

Supplier has_one Account_history

我們可以通過through: :account 來標明;
之後,直接獲取

class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account
end
 
class Account < ApplicationRecord
  belongs_to :supplier
  has_one :account_history
end
 
class AccountHistory < ApplicationRecord
  belongs_to :account
end

多對多

has_many :through 經常用於創建多對多的關係;

假設: 病人預約去看病

一個病人通過預約找一個醫生去看病;

一個病人可能去找到多個醫生看病;
同時,一個醫生也會給多個病人看病;

但是,在一次預約看病中;
一個醫生只給一個病人看病;

那麼就需要使用 has_many :through
:through 後面加的是 中間表, 也就是 預約表

class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end
 
class Appointment < ApplicationRecord
  belongs_to :physician
  belongs_to :patient
end
 
class Patient < ApplicationRecord
  has_many :appointments
  has_many :physicians, through: :appointments
end

對應的遷移文件

class CreateAppointments < ActiveRecord::Migration[5.0]
  def change
    create_table :physicians do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :patients do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :appointments do |t|
      t.belongs_to :physician
      t.belongs_to :patient
      t.datetime :appointment_date
      t.timestamps
    end
  end
end

through: 還有一個方便的功能
就是可以直接獲取到has_many 對象的 has_many

加入 Document has_many Sections
Section has_many Paragraphs
通過 has_many :paragraphs, through: :sections 就可以直接
取到 paragraphs

class Document < ApplicationRecord
  has_many :sections
  has_many :paragraphs, through: :sections
end
 
class Section < ApplicationRecord
  belongs_to :document
  has_many :paragraphs
end
 
class Paragraph < ApplicationRecord
  belongs_to :section
end
@document.paragraphs

上述情況是建立了一箇中間的 model ; 我們可以自定義一些字段處理;

我們也可以使用 Active Record提供的簡單的關係

has_and_belongs_to_many 使用這個關係;
Active Record 會幫我們創建一個只有兩個字段是中間表;
我們不需要自己創建一箇中間的model;

雖然方便了,卻也缺少了自定義的字段的功能;

class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end
 
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
  def change
    create_table :assemblies do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :parts do |t|
      t.string :part_number
      t.timestamps
    end
 
    create_table :assemblies_parts, id: false do |t|
      t.belongs_to :assembly
      t.belongs_to :part
    end
  end
end

多表查詢

Person
  .select('people.id, people.name, comments.text')
  .joins(:comments)
  .where('comments.created_at > ?', 1.week.ago)
SELECT people.id, people.name, comments.text
FROM people
INNER JOIN comments
  ON comments.person_id = people.id
WHERE comments.created_at > '2015-01-01'

N+1問題:includes

eager-loading-associations

飢餓式加載 (eager-loading)關聯關係的原因,會造成 N+1 問題;

這個問題是框架爲了延遲加載所致;
在需要的時候採取真正地去數據庫中查找數據;

一般情況下,這樣做會給我帶來好處,等我們真正操作完,採取執行數據庫層面的操作;

但是,萬物有好有壞;

這也給我們帶來了著名的 N+1 問題:

clients = Client.limit(10)
 
clients.each do |client|
  puts client.address.postcode
end

我們看上面的代碼, 它會執行多少條SQL 語句呢?

因爲 eager-loading 的原因, 它會執行 11 條
先查詢前10條, 這是一條SQL;
然後,在每一次each, client.address.postcode 都再執行一條SQL

我們可以看到,這是一個明顯的性能浪費;
我們可以通過一次查詢到關聯的數據,不就可以了嗎?
爲啥還要傻乎乎地查詢那麼多次呢!!

所以,Active Record 爲我們提供了 includes 解決這個問題;

clients = Client.includes(:address).limit(10)
 
clients.each do |client|
  puts client.address.postcode
end
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))

同時呢:

如果我們經常性地要讀取 二級關聯的數據;
我們也可以指定 beloings_to 的回調來每次都執行 includes

There’s no need to use includes for immediate associations - that is,
if you have Book belongs_to :author, then the author is eager-loaded automatically when it’s needed.

原來的

class Chapter < ApplicationRecord
  belongs_to :book
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :chapters
end
 
class Author < ApplicationRecord
  has_many :books
end

加上 includes

class Chapter < ApplicationRecord
  belongs_to :book, -> { includes :author }
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :chapters
end
 
class Author < ApplicationRecord
  has_many :books
end

Active Record 回調

回調可理解爲Hook

目的就是對某一個狀態下,進行統一的操作;

例如,在save前,save後之類的;
很多框架都提供類似的hook;

As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model’s validations, the registered callbacks, and the database operation to be executed.
The whole callback chain is wrapped in a transaction.
If any callback raises an exception, the execution chain gets halted and a ROLLBACK is issued.
To intentionally stop a chain use:

註冊回調

The :on option specifies when a callback will be fired. If you don’t supply the :on option the callback will fire for every action.

class User < ApplicationRecord
# on 指定操作類型
  before_validation :normalize_name, on: :create
 
  # :on takes an array as well
  after_validation :set_location, on: [ :create, :update ]
 
  private
    def normalize_name
      self.name = name.downcase.titleize
    end
 
    def set_location
      self.location = LocationService.query(self)
    end
end

可用的回調

available-callbacks

總的來說;

對 CRUD , 對象的初始化, 事務 等各個地方都留有了 HOOK;

觸發回調方法

running-callbacks

跳過回調方法

skipping-callbacks

關聯回調

relational-callbacks

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end
 
class Article < ApplicationRecord
  after_destroy :log_destroy_action
 
  def log_destroy_action
    puts 'Article destroyed'
  end
end

條件回調

默認我們加入的回調,都會在每一個hook處執行;
但是,總有例外,是我們不想讓它觸發的;

conditional-callbacks

class Comment < ApplicationRecord
  after_create :send_email_to_author,
    if: [Proc.new { |c| c.user.allow_send_email? }, :author_wants_emails?],
    unless: Proc.new { |c| c.article.ignore_comments? }
end

事務回調

There are two additional callbacks that are triggered by the completion of a database
transaction: after_commit and after_rollback

When a transaction completes, the after_commit or after_rollback callbacks are
called for all models created, updated, or destroyed within that transaction.

However, if an exception is raised within one of these callbacks,
the exception will bubble up and any remaining after_commit or after_rollback methods
will not be executed. As such, if your callback code could raise an exception,
you’ll need to rescue it and handle it within the callback in order to allow other callbacks to run.

Using both after_create_commit and after_update_commit in
the same model will only allow the last callback defined to take effect, and will override all others
There is also an alias for using the after_commit callback for both create and update together:

save 是 create update的別名回調
就是說 在create,update時,都會執行save的回調

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章