# 工作一年半我踩過 Active Record 的雷 五倍紅寶石 **廢的 Fred** --- ### 觀念篇 - scope 注意事項 - subquery - n + 1 的問題 - connection pool --- ### scope --- 把常用的 query 放進 scope 讓他變漂亮 ```ruby class User < ApplicationRecord scope :adult, ->{ where( 'age >= 18') } end adult_users = User.adult ``` --- ### scope 新手誤區 - 每次查詢都要回傳 relation - 每次回傳都要是自己 --- ### scope 新手誤區 ```ruby class User < ApplicationRecord scope :names, ->{ map(&:name) } # 誰准你把 scope 當 method 用? scope :posts, ->(user_id){ Post.where(user_id: user_id)} # 這到底是.... scope :oldest, ->{order(:age).last} # 這個不是 relation end ``` --- ### scope 寫好可以讓 code 變美 ```ruby class Address < ApplicationRecord belongs_to :location end class User < ApplicationRecord scope :lives_in_taipei, -> do joins(address: :location) .where( address: {location: 'taipei'} ) end end class Post < ApplicationRecord belongs_to :author, class_name: "User" scope :sports, ->{ where(category: 'sports')} scope :sports_in_taipei, -> do joins(:author).merge(User.lives_in_taipei) end end some_posts = Post.sports_in_taipei ``` --- ### Subquery --- 1. `where.... IN...` 2. `INNER JOIN table ON ..` 3. `FROM table1 , (SUBQUERY TABLE) as table2` --- ### 要注意 subquery 效能問題 ```sql SELECT * FROM TABLE WHERE COLUMN IN (SELECT COLUMN FROM TABLE) ``` --- - 這樣做是很耗資源的(要看 db 有沒有優化),因為sql會先處理內部查詢,查完才做外面的查詢 - 通常 JOIN 效能會比 subquery 好 - 有些 DBMS 會優化 query 所以要看實際執行效率 --- [15個優化你的SQL QUERY的方式](http://blog.davidou.org/archives/609) [rewriting subqueries](https://dev.mysql.com/doc/refman/5.7/en/rewriting-subqueries.html) [MySQL 子查詢及其優化](https://codertw.com/ios/59303/) [where in 效能](https://ithelp.ithome.com.tw/questions/10155255) [pg where in 效能](https://dba.stackexchange.com/questions/91247/optimizing-a-postgres-query-with-a-large-in) [卡米 where in 效能](http://etrex.blogspot.com/2017/08/sql-where-in.html) [MySQL EXPLAIN 獨立子查詢dependent subquery 優化示例](https://www.itread01.com/content/1549843400.html) --- ### N + 1 問題 --- 如果要用到,先用 includes 讀進來 ```ruby adults = User.adults.includes(posts: :comments) adults.each do |adult| adult.posts.each do |post| puts "#{adult.name}, post: #{post.title}, lastest comment: #{post.comments.last}" end end ``` --- ### N + 1 or 效能檢查工具 - [active record query trace](https://github.com/brunofacca/active-record-query-trace) (查哪邊噴出 query 來,自己用過) - [Bullet](https://github.com/flyerhzm/bullet) - [Mini Profiler](https://github.com/MiniProfiler/rack-mini-profiler) --- ### connection pool --- 每次建立連線很貴 ![connection pool](http://4.bp.blogspot.com/-JRxCs_TF-7U/U9fX12mV5-I/AAAAAAAAAFQ/5IMQtJyChhI/s1600/ap_without_cp.jpg =500x) ##### credit: http://peggg327.blogspot.com/2014/11/connection-pool.html --- - 先連好,要用到再去領 - pg 預設是 100 個 ![connection pool](http://3.bp.blogspot.com/-RSdnkn_fvs0/U9fX1f1HURI/AAAAAAAAADw/fu83fUYZwZE/s1600/ap_with_cp1.jpg =500x) ##### credit: http://peggg327.blogspot.com/2014/11/connection-pool.html --- ### Rails 的 connection pool 以下是每個 process 的 pool 數 ```yml # config/database.yml development: pool: 5 ``` --- ### Rails 的 connection pool sidekiq 要注意 https://dev.to/appaloosastore/active-record-sidekiq-pools-and-threads-18d5 --- 私心推薦 - [Upcase - Advanced ActiveRecord Querying](https://thoughtbot.com/upcase/advanced-activerecord-querying?utm_source=giantrobots&utm_medium=blog&utm_campaign=aarq-announcement) - [Upcase - Advanced Query Optimization](https://thoughtbot.com/upcase/videos/advanced-query-optimization) --- ### 進階篇 - Transaction - Lock - Mulitple DB in Rails --- ### Transaction --- ### 交易的四個特性 ACID - Atomicity (原子性、不可分割):全執行、全部不執行。 - Consistency (一致性):所有資料必須符合預設的驗證規則。例如銀行轉帳 - Isolation (隔離性):多個交易可以獨立、同時執行,不會互相干擾。 - Durability (持久性):交易完成後,異動結果須完整的保留。 [參考資料](https://openhome.cc/Gossip/EJB3Gossip/TransactionABC.html) --- ### Transaction: COMMIT or ROLLBACK --- #### Transaction in MySql ```sql -- 記得要先把 autocommit 關掉 -- 1. start a new transaction START TRANSACTION; -- 2. Get the latest order number SELECT @orderNumber:=MAX(orderNUmber)+1 FROM orders; -- 3. insert a new order for customer 145 INSERT INTO orders(orderNumber, orderDate, requiredDate, shippedDate, status, customerNumber) VALUES(@orderNumber, '2005-05-31', '2005-06-10', '2005-06-11', 'In Process', 145); -- 4. Insert order line items INSERT INTO orderdetails(orderNumber, productCode, quantityOrdered, priceEach, orderLineNumber) VALUES(@orderNumber,'S18_1749', 30, '136', 1), (@orderNumber,'S18_2248', 50, '55.09', 2); -- 5. commit changes COMMIT; ``` --- #### Transaction in Rails ```ruby ActiveRecord::Base.transaction do david.withdrawal(100) mary.deposit(100) end ``` [參考資料](https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html) --- #### 注意 commit 之後才會寫入資料庫 ```ruby ActiveRecord::Base.transaction do book = Book.find(1) book.sold! unless book.sold? book2 = Book.find(1) book.sold! unless book.sold? end ``` --- ### Lock --- [鎖定模式 Lock Mode](https://docs.microsoft.com/zh-tw/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver15#lock_modes) 太複雜了自己看 --- ### Race Condition ![race condition](https://d33wubrfki0l68.cloudfront.net/f4381870b1d02fcc67b2c5189e282f2eee92e8a1/b0cfa/img/acid/race-condition.png =600x) --- ### Active Record 的鎖 - [樂觀鎖 Optimistic Locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html) - [悲觀鎖 Pessimistic Locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html) --- ### 樂觀鎖 - 要有 `lock_version` 的欄位 - 可以有多個 thread SELECT, 但是會依據 lock_version 鎖定 update - 在 web form 的話要塞一個 `lock_version` hidding field 給它 --- 範例 ```ruby p1 = Person.find(1) p2 = Person.find(1) p1.first_name = "Michael" p1.save p2.first_name = "should fail" p2.save # Raises an ActiveRecord::StaleObjectError p1 = Person.find(1) p2 = Person.find(1) p1.first_name = "Michael" p1.save p2.destroy # Raises an ActiveRecord::StaleObjectError ``` [更多範例](http://railscasts.com/episodes/59-optimistic-locking-revised) --- --- ### 悲觀鎖 - 利用 `SELECT ... FOR UPDATE` 鎖定 - 其他的 thread 會被 block 不能 select - https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html - 要搭配 transaction 服用 - commit 後才會解鎖 --- ### 悲觀鎖 ```ruby Account.transaction do # select * from accounts where name = 'shugo' limit 1 for update nowait shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo") yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko") shugo.balance -= 100 shugo.save! yuko.balance += 100 yuko.save! end ``` --- whit_lock ```ruby Account.transaction do account = Account.first account.with_lock do # This block is called within a transaction, # account is already locked. account.balance -= 100 account.save! end end ``` --- 參考資料 - https://devfun.tw/t/db-lock/529 - https://ruby-china.org/topics/28963 - https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html --- ### Multi DB in Rails --- ### Rails 6 很簡單 --- _database.yml_ ```yml production: primary: database: my_primary_database user: root adapter: mysql primary_replica: database: my_primary_database user: root_readonly adapter: mysql replica: true animals: database: my_animals_database user: animals_root adapter: mysql migrations_paths: db/animals_migrate animals_replica: database: my_animals_database user: animals_readonly adapter: mysql replica: true ``` --- model ```ruby class AnimalsBase < ApplicationRecord self.abstract_class = true connects_to database: { writing: :animals, reading: :animals_replica } end ``` --- ### before Rails 6 --- ### establish_connection ```ruby ActiveRecord::Base.establish_connection( :adapter => "mysql", :host => "localhost", :username => "myuser", :password => "mypass", :database => "somedatabase" ) ``` https://apidock.com/rails/ActiveRecord/Base/establish_connection/class --- 不要這樣幹 ```ruby class SecondBaseUser < ActiveRecord::Base establish_connection DB_SECOND end ``` 每次 new 的時候會建一次 connection --- 在上層繼承 ```ruby module Second class Base < ActiveRecord::Base self.abstract_class = true establish_connection DB_SECOND end class User < Second::Base end end ``` --- 隨機附贈 second db migration rake https://medium.com/@primazp/multiple-databases-in-rails-part-1-setup-48f6d207685a --- ### Doorkeeper 隱藏版設定 _config/initialize/doorkeeper.rb_ ```ruby Doorkeeper.configure do orm :active_record # ... active_record_options(establish_connection: "portal_#{Rails.env}".to_sym) end ``` --- ### 另外最近發生的事 --- ### index 問題 - index 怎麼建 - 怎麼下 index 比較好 --- ### NULL KEY --- ## 以上
{"metaMigratedAt":"2023-06-15T05:33:29.194Z","metaMigratedFrom":"YAML","title":"工作一年半我踩過 Active Record 的雷","breaks":true,"slideOptions":"{\"theme\":\"solarized\",\"transition\":\"fade\"}","contributors":"[{\"id\":\"6d2ca7b4-3051-4add-b84a-86374db18602\",\"add\":10387,\"del\":1230}]"}
    936 views