Day 18: What is Transaction like?

post on

from Qiita: トランザクションって

「トランザクション」とは何か?を超わかりやすく語ってみた!

What is Transaction like?

コンピュータ内で実行される、分けることのできない一連の情報処理の一単位。

トランザクション処理における永続性記憶資源の管理では、複数のデータ項目の更新操作列をすべて実行するか、まったく実行しないように制御する必要がある。

ACID 標準

また、トランザクション処理システムは4つの属性の機能をサポートしており、頭文字からACID標準という。

  • A : Atomic 不可分性
  • C : Consistency 一貫性
  • I : Isolation 独立性
  • D : Durability 永続性

実際に動かしてみる

DB内で操作

MySQLはデフォルトで、自動コミットモードが有効になった状態で動作し、実行するとすぐに、ディスクに格納されて永続的になります。この変更はロールバックできない。

自動コミットモードを暗黙的に無効にするには、START TRANSACTIONをし、その後、COMMITまたはROLLBACK で終了するまで、自動コミットは無効のままになります。そのあと、自動コミットモードはその以前の状態に戻ります。

実作業

今回はMySQLと、以前に作成した大学生徒データAppのデータを再利用する

mysql -u root -p
-- 使用するデータベース情報の選択
USE cebu_college_development;

-- 使用するstudentsテーブルの構造を確認
DESC students;
-- +------------+--------------+------+-----+---------+----------------+
-- | Field      | Type         | Null | Key | Default | Extra          |
-- +------------+--------------+------+-----+---------+----------------+
-- | id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
-- | name       | varchar(255) | YES  |     | NULL    |                |
-- | email      | varchar(255) | YES  |     | NULL    |                |
-- | gender     | int(11)      | YES  |     | NULL    |                |
-- | age        | int(11)      | YES  |     | NULL    |                |
-- | opinion    | text         | YES  |     | NULL    |                |
-- | created_at | datetime     | NO   |     | NULL    |                |
-- | updated_at | datetime     | NO   |     | NULL    |                |
-- +------------+--------------+------+-----+---------+----------------+
-- id=3のtaro-2さんを使ってみる
SELECT name FROM students WHERE id = 3;
-- +--------+
-- | name   |
-- +--------+
-- | taro-2 |
-- +--------+
COMMITするパターン
START TRANSACTION;

-- id=3のnameを"tran-sakuko"に更新
UPDATE students SET name = "tran-sakuko" WHERE id = 3;
-- Query OK, 1 row affected (0.01 sec)
-- Rows matched: 1  Changed: 1  Warnings: 0

SELECT name FROM students WHERE id =3;
-- +-------------+
-- | name        |
-- +-------------+
-- | tran-sakuko |
-- +-------------+

-- コミットする
COMMIT;
-- Query OK, 0 rows affected (0.00 sec)

-- COMMITされてるか確認
-- select name from students where id =3;
-- +-------------+
-- | name        |
-- +-------------+
-- | tran-sakuko |
-- +-------------+
-- 1 row in set (0.00 sec)
ROLLBACKするパターン
START TRANSACTION;
-- Query OK, 0 rows affected (0.00 sec)

-- id=3のnameを"tran sakutarou"に更新
UPDATE students SET name = "tran sakutarou" WHERE id =3;
-- Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

-- id3のnameに新しいデータの"tran sakutarou"がセットされてる
SELECT name FROM students WHERE id = 3;
-- +----------------+
-- | name           |
-- +----------------+
-- | tran sakutarou |
-- +----------------+

-- ロールバックしてみる
ROLLBACK;
-- Query OK, 0 rows affected (0.01 sec)

-- セットした"trans sakutarou"というデータはDBに格納されない。
SELECT name FROM students WHERE id = 3;
-- +-------------+
-- | name        |
-- +-------------+
-- | tran-sakuko |
-- +-------------+

try on RubyonRails

新しく、アプリを作成する。今回はDBの操作だけなので、rails g modelコマンドのみ使用。テーブルはUserとReviewの2つ。

rails new transact_self -d mysql
# database.ymlを編集後
rails db:create
# Userモデル作成
rails g model User name:string approved:boolean deleted:boolean
# Reviewモデル作成
rails g model Review user:references rate:integer approved:boolean
rails db:migrate
# input data
# user
(1..5).each do |i|
  User.create(name: "taro-#{i}", approved: true, deleted: false)
end

(1..5).each do |i|
  user = User.first
  Review.create!(user_id: user.id, rate: i, approved: true)
end

DB内で確認

SELECT * FROM users;
-- +----+--------+----------+---------+---------------------+---------------------+
-- | id | name   | approved | deleted | created_at          | updated_at          |
-- +----+--------+----------+---------+---------------------+---------------------+
-- |  1 | taro-1 |        1 |       0 | 2019-04-03 09:01:31 | 2019-04-03 09:01:31 |
-- |  2 | taro-2 |        1 |       0 | 2019-04-03 09:01:31 | 2019-04-03 09:01:31 |
-- |  3 | taro-3 |        1 |       0 | 2019-04-03 09:01:31 | 2019-04-03 09:01:31 |
-- ...

SELECT * FROM reviews;
-- +----+---------+------+----------+---------------------+---------------------+
-- | id | user_id | rate | approved | created_at          | updated_at          |
-- +----+---------+------+----------+---------------------+---------------------+
-- |  1 |       1 |    1 |        1 | 2019-04-03 09:01:38 | 2019-04-03 09:01:38 |
-- |  2 |       1 |    2 |        1 | 2019-04-03 09:01:38 | 2019-04-03 09:01:38 |
-- |  3 |       1 |    3 |        1 | 2019-04-03 09:01:38 | 2019-04-03 09:01:38 |
-- ...

userとreviewとの、関連付け

# app/models/user.rb
class User < ApplicationRecord
  has_many :reviews
end

reviewモデルにvalidation追加
approvedカラムを空欄不可にしておく。

# app/models/review.rb
class Review < ApplicationRecord
  belongs_to :user
  validates :approved, presence: true
end

コンソールで、トランザクション処理の挙動を確認

  1. トランザクション処理に成功し、commitされるパターン
user = User.first
User.transaction do
  user.update!(approved: false)
  user.reviews.each { |review| review.update!(approved: true) }
end

# =>(0.1ms)  BEGIN
#   User Update (0.3ms)  UPDATE `users` SET `approved` = FALSE, `updated_at` = '2019-04-03 09:10:46' WHERE `users`.`id` = 1
#   Review Load (0.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`user_id` = 1
#    (6.8ms)  COMMIT
# => <Review id: 1, user_id: 1, rate: 1, approved: true, created_at: "2019-04-03 09:01:38", updated_at: "2019-04-03 09:01:38">, 
#<Review id: 2, user_id: 1, rate: 2,approved: true, created_at: "2019-04-03 09:01:38", updated_at: "2019-04-03 09:01:38">, 
#<Review id: 3, user_id: 1, rate: 3, approved: true, created_at: "2019-04-03 09:01:38", updated_at: "2019-04-03 09:01:38">,

userテーブルのid1のtaro-1が、更新されてる

select * from users;
-- +----+--------+----------+---------+---------------------+---------------------+
-- | id | name   | approved | deleted | created_at          | updated_at          |
-- +----+--------+----------+---------+---------------------+---------------------+
-- |  1 | taro-1 |        0 |       0 | 2019-04-03 09:01:31 | 2019-04-03 09:10:46 |
  1. トランザクション処理に失敗し、rollbackされるパターン
user = User.first
User.transaction do
  user.update!(approved: true)
  user.reviews.each { |review| review.update!(approved: false) }
end
=>(0.1ms)  BEGIN
  User Update (0.2ms)  UPDATE `users` SET `approved` = TRUE, `updated_at` = '2019-04-03 09:14:44' WHERE `users`.`id` = 1
  Review Load (0.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`user_id` = 1
   (6.6ms)  ROLLBACK
Traceback (most recent call last):
        3: from (irb):2
        2: from (irb):4:in `block in irb_binding'
        1: from (irb):4:in `block (2 levels) in irb_binding'
ActiveRecord::RecordInvalid (Validation failed: Approved can't be blank)

トランザクション処理に失敗し、rollbackしたため、DBに変化はない。

modelファイルを編集して実装

# app/models/user.rb
class User < ApplicationRecord
  has_many :reviews

  def suspend!
    self.class.transaction do
      disapprove_user!
      disapprove_reviews!
    end
  end

private
  def disapprove_user!
    self.update!(approved: false)
  end
  def disapprove_reviews!
    reviews.each { |review| review.update!(approved: false) }
  end
end