---
# System prepended metadata

title: ActiveRecord-資料表關連

---

# ActiveRecord-資料表關連

為一種設計模式，他是一個物件，他把每一條條的資料包成一個物件，這個物件能夠做一些操作讓資料存取更便利。
![](https://i.imgur.com/NreObEW.png)

## model是什麼?

是一種抽象的概念，並不是資料庫，可以當作ActiveRecord設計出來的產物。
比如我們給model一個Book.all的語法他會翻譯成sql語法給資料庫，進而提取我們所需要的資料回傳給model。
![](https://i.imgur.com/B1JYptU.png)


## ORM(Object Relation Mapping)

透過物件的方式操作資料表。
今天去資料庫撈出一個候選人的資料出來，這個候選人的物件我們就可以對它去操作，在不用寫SQL的情況下去撈資料出來，目的是簡化資料庫操作的語法，所以也可以說ActiveRecord是一種ORM的設計框架。

**切記，之後還是得熟悉SQL語法!!!**

## COC(Convention Over Configuration)慣例優於設定

簡單來講就是減少一些不必要的程式碼。
常見的慣例:
* Model : 大寫 單數
* Table : 小寫 複數

![](https://i.imgur.com/nZeO5DS.png)

* 在設計表格時都會預設一個id欄位
* 在migration裡都會預設一個timestamps
    * timestamps會轉換成created_at & updated_at兩個時間欄位
    * 再新增或更新時會自動放上當下時間

#### 冷知識
* rails是如何得知英文單字的單複數?不規則變化?

    到rail c試看看在字串後方使用`.pluralize`會印出複數單字，`.singularize`印出單數單字。但rails沒有這麼聰明，它並不是字典，如果遇到亂打一通的字串使用`.pluralize`就會幫他多印一個s，例如"aaa"會變"aaas"。
    那要如何才能讓"AAA"變成"BBB"呢? 我們到專案裡找到/config/initializers/inflection.rb更改一下程式碼
```ruby=
ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, '\1en'
#   inflect.singular /^(ox)en/i, '\1'
  inflect.irregular 'AAA', 'BBB'
#   inflect.uncountable %w( fish sheep )
end
```
"AAA".pluralize後就會變成"BBB"摟~

* rails怎麼知道檔名跟類別之間的對照?

    其實是靠`.undercase` 以及 `camelize` 的方法
    "VotLog".undercase => "vote_log"
    "vote_log".camelize => "VotLog"
    
## Model之間的關連性

介紹關連性之前先用一個例子並且製作一個新的專案，產生這三個Model:
書店系統
* 店長(Owner)
    ![](https://i.imgur.com/eRwxWiF.png)

* 商店(Store)
    ![](https://i.imgur.com/C2xH5tf.png)
* 商品(Product)
    ![](https://i.imgur.com/VS5hwyk.png)

### 常見的關連性

* 一對一
    * 一個店長只能有一家店
* 多對一
    * 一間店可以賣很多商品
* 多對多
    * 一間店可以賣很多商品
    * 商品可以在很多間店販賣

### rails支援的關連
記得
* has_one
* has_many
* belong_to
* has_one:through
* has_many:through
* has_and_belong_to_many(HABTM)

其實都是透過_id在做事情

### has_one
```ruby=
class Owner < ApplicationRecord
  has_one :store
end
```
一對一，每個店長都只有一間店
has_one會自動幫你產生四種方法
* store
* store=
* build_store
* create_store

我們可以先在rails c的模式下建立兩個model
```ruby=
o1=Owner.new(name: '1')
S1 = Store.new(title:'s1')
```
o1.save後你可以直接使用o1.store這個方法問o1說你有沒有商店，它就會去幫你查O1所屬的商店，而且是根據owner_id這個欄位去查，如果stores這邊有些owner_od是1號的話這邊就能直接幫你撈出來。

但S1.save其實會出現錯誤，如果使用full_messages出現 "Owner must exist"，意思就是你要存可以但前提你必須要有一個Owner。

所以可以用sotre=的方法
```ruby=
O1.store= S1 
```
![](https://i.imgur.com/1Lym1UL.png)
在查詢後就能得知商店的資料已經被寫進去o1裡了

### has_many

has_many為一對多的方法，不是設定而是一個類別方法，且後方參數必須是複數，如果用單數也不會出錯但還是按照慣例會比較好。
```ruby=
class Store < ApplicationRecord
  has_many :products
end
```
一樣使用has_many後會產生幾種方法
* products
* products=
* build
* create

簡單來說，一間商店能有很多商品，所以在store的model下設定has_many:products，另一邊設定通常都是belong_to。

商品角度
```ruby=
p1 = Product.new(name: "ruby" price: 200)
p2 = Product.new(name: "rails" , price: 200)
S1.products=[p1,p2]
```
S1可以使用products=，同時存進兩筆商品資料或是可以利用另一種商品的角度撰寫也可以

商店角度
```ruby=
S1.products.build(name: "rails" , price: 200)#還沒存
S1.products.create(name: "rails" , price: 200)
```
### belong_to

和has_one一樣是類別方法
```ruby=
class Store < ApplicationRecord
  belongs_to :owner
end
```
執行後也能動態產生以下幾種方法
* owner
* owner=
這樣你就能以Store的角度去查詢owner是誰..等等
那你可能會問，belong_to和has_one需要同時存在嗎?只設定其中一個呢?
其實不會怎麼樣，只是owner可以查store但store不能反查回來而已。

### has_one:through
### has_many:through
簡單的用醫生與病人的例子來說，一個病人可以給很多醫生看，同理醫生也會看很多病人，也就是所謂的多對多。但今天醫生想要知道有哪些病人的話，他沒有辦法直接知道，一定要透過醫院的系統才能得知。醫生跟病人之間的關係雖然是多對多但是他們之間的資訊是紀錄在一個第三方的表格，做多對多的時候都必須要有一個第三方的表格!
![](https://i.imgur.com/Zva6N21.png)
* 需要第三邊的資料表儲存兩邊的資訊
* 第三個資料表通常僅存兩邊的ID，並且belong_to兩邊的model

war_houses的model必須包含兩邊的id所以建立的時候必須要填寫 store:references以及 product: references，才能夠連接兩邊的資訊
建立完成後到store以及product改寫一下連接的方式
store
```ruby=
class Store < ApplicationRecord
  belongs_to :owner
  has_many :war_houses
  has_many :products , through: :war_houses
end
```
product
```ruby=
class Product < ApplicationRecord
  has_many :war_houses
  has_many :stores , through: :war_houses
end
```
has_many第一行的意思指必須要先與war_houses做連接所以你也能夠看見war_houses的model有belong_to: store和product。第二行的意思為為了要查看store或是product的資訊，還是必須給他們一個has_mnay去做連接並且透過war_houses來幫他們查詢資料。

當今天在建立資料時能夠看見，被記載的資料也會被加入到war_houses的表格中
![](https://i.imgur.com/AgpSmVk.png)
如果今天想知道s1的商品數總共有幾個`s1.products.count`
![](https://i.imgur.com/UgKGAuJ.png)
仔細看終端機，`INNER JOIN "war_houses"`大概就是在說他現在透過war_houses幫你找資料出來!

### has_and_belongs_to_many(HABTM)

早期rails多對多的寫法
```ruby=
#product.rb
class Product < ApplicationRecord
  # has_many :war_houses
  # has_many :stores , through: :war_houses
  has_and_belong_to_many:stores
end
#store.rb
class Store < ApplicationRecord
  belongs_to :owner
  # has_many :war_houses
  # has_many :products , through: :war_houses
  has_and_belong_to_many:products
end

```
看起來較短且不需要第三方的支援，但還是會有一個小問題

在終端機想查看s1.products之後，卻出現`(Could not find table 'products_stores')`，找不到products_stores這個table?
是因為他會預期第三方表格的名字會是兩個model的名字做組合且他會英文字母的順序p比s小所以才會是'products_stores'
不過你可以破壞這個慣例，在model後方寫上一段程式碼
```ruby=
#product.rb
class Product < ApplicationRecord
  # has_many :war_houses
  # has_many :stores , through: :war_houses
  has_and_belong_to_many:stores, join_table: 'war_houses'
end
#store.rb
class Store < ApplicationRecord
  belongs_to :owner
  # has_many :war_houses
  # has_many :products , through: :war_houses
  has_and_belong_to_many:products, join_table: 'war_houses'
end
```
將他要搜尋的table更改成剛剛的war_houses即可成功搜尋。
只要正確的table(兩個model的組合名稱)存在，那麼就可以用HABTM，也不用再多建立一個model

## ORM基本操作-CRUD(Create、Read、Update、Delete)

### ORM基本操作-Create

* 可以使用.new建立一筆資料資料，但不會存到資料庫裡
* create、create!會建立一筆資料，而且也會寫入資料庫
    * create!就是代表他是激進派的，你寫入錯誤時，他會直接噴一個錯誤訊息給你
    * 而create他是保守派的，只會默默地rollback。


### ORM基本操作-Read
* first與last
* find_by(id:1)與find(1)
    * 找不到的時候find_by是保守派的會回傳nil
    * find就是激進派會噴一個例外訊息出來
* find_by_sql
* find_each
    * 預設會先撈一千筆出來，形成一個區塊，一直執行到完畢為止，會消耗較少的記憶體

```ruby=
Product.first #找第一筆資料
Product.last  #找最後一筆資料
Product.find(1) #找id=1的資料
Product.find_by(id:1) #同上

#直接sql查詢
Product.find_by_sql("select * from products where id = 1")
#batch find
Product.find_each do |p|
#...
end
```
* all
* select
* where
* order
* limit
```ruby=
Product.all #找出所有商品
Product.select('name')  #同上，但只找出NAME的欄位
Product.where(name: "ruby") #找所有name欄位是ruby的資料
Product.order('id DESC') #依照id大小反向排序
Product.order(id: :desc) #同上
Product.limit(5) #只取出5筆資料
```
* count
* average
* sum
* maximum與minimum

### ORM基本操作-Update

* save
* update
* update_attributes
    * 其實跟update一樣但現在很少再用了，太長了
* update_all
* increment 與 decrement
    * 遞增 與 遞減
* toggle
```ruby=
#儲存
p1 = Product.first
p1.save
#更新
p1.update(name: "aaa")
p1.update_attributes(name: "aaa")
#全部更新(請小心使用!)
Product.update_all(name: "aaa")
```
```ruby=
#儲存
my_order = Order.first
my_order.increment(:quantity) #quantity欄位的值+1
my_order.toggle(:is_online) #把原本的 true/false 的值對調
```
### ORM基本操作-Delete

* delete
    * 直接刪除資料後結束
* destroy
    * 再刪除的時候會走過一連串的callback
* destroy.all(condition=nil)

```ruby=
#儲存
p1 = Product.first
#刪除
p1.delete
p1.destroy
#刪除編號一的商品
Product.delete(1)
#刪除所有低於10元的商品
Product.destroy.all("price<10")
```

## Scope

* 把一群條件整理成一個scope
* 簡化使用時邏輯
* 減少在controller裡寫一堆where組合
* 用起來跟類別方法一樣

一樣使用vote-me的專案，b
```ruby=
@candidates = Candidate.where("age < 40")
#更改成Scope用法
@candidates = Candidate.young_people
```
然後必須到model/candidate.rb撰寫scope語法
```ruby=
class Candidate < ApplicationRecord
  validates :name,presence: true
  has_many :vote_logs ,dependent: :destroy
  #scope name ,block
  scope :young_people, -> { where('age < 40') } #後面是lamda用法
  scope :order_than, -> (x){ where('age > #{x}') } #帶參數寫法
  
end
```

scope :young_people
![](https://i.imgur.com/RycPgXr.png)
scope :order_than(10)，會找出所有大於10歲的資料
![](https://i.imgur.com/qN0Ittq.png)

所以使用scope確實能夠寫得更加便利，如果今天要更改條件，可以直接到scope更改，整個專案都會跟著更改，而且語意會更加清楚，一眼就能知道這個方法是要搜尋什麼

scope還有一個特別的寫法`default_scope`，讓scope也有預設值
```ruby=
class Candidate < ApplicationRecord
  validates :name,presence: true
  has_many :vote_logs ,dependent: :destroy
  default_scope { where('age > 0') } 
end
```
寫下去後不管.all還是什麼方法還是查詢都會再接一個`where('age > 0')`的條件來搜尋資料

那要如何讓預設的scope消失?
```ruby=
class Candidate < ApplicationRecord
  validates :name,presence: true
  has_many :vote_logs ,dependent: :destroy
    scope :order_than, -> (x){ unscope(:where).where('age > #{x}') } 
  default_scope { where('age > 0') } 
end
```
在不要預設值的語法前新增一段`unscope(:where)`

那應該會有人發現像是我們使用young_people這個方法其實就是類別方法，因為他作用於類別，所以scope本身在被後會把你轉換成類別方法。
所以你也能在model裡使用self的方法，本身其實跟scope一樣
```ruby=
class Candidate < ApplicationRecord
    def self.XXX
        #...
    end
end
```

## 資料驗證

* presence
* format
* uniqueness
* numbericality
* length
* condition
用起來大概像是這樣
```ruby=
class Product < ApplicationRecord
    validates :name, presence: true
end

#舊式寫法
class Product < ApplicationRecord
    validates_presence_of :name, :title , :price
end
```

### 資料驗證過程
![](https://i.imgur.com/bKoPOvV.png)

如果硬要繞過驗證其實也是可以的
![](https://i.imgur.com/QCISx0E.png)
只要在語法後方使用`validate: false`就會過了

## CALLBACK

* 資料存檔會經過以下流程
    * save > valid > before_validation > validate > after_validate > before_save > before_create > create > after_create > after_save > after_commit
    * 可以在流程執行的時候做一些事情

舉個例子

```ruby=
#在資料存檔前對 EMAIL 進行加密
class User < ApplicationRecord
before_save :encrypt_email

    private
        def encrypt_email
            require 'digest'
            self.email = Digest::MD5.hexdigest(email)
        end
end
```
但這個是只在存檔前加密，如果每次更新的時候都會先經歷存檔，這樣EMAIL會不斷的被加密，這邊應該使用before_create才對，在建立前就先加密