Day 18: トランザクションとは

/
#cebu#ruby#mysql

What is Transaction like?

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

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

ACID 標準

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

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

実際に動かしてみる

DB内で操作

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

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

実作業

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

undefined
1
mysql -u root -p
undefined
1
-- 使用するデータベース情報の選択
2
USE cebu_college_development;
3
4
-- 使用するstudentsテーブルの構造を確認
5
DESC students;
6
-- +------------+--------------+------+-----+---------+----------------+
7
-- | Field | Type | Null | Key | Default | Extra |
8
-- +------------+--------------+------+-----+---------+----------------+
9
-- | id | bigint(20) | NO | PRI | NULL | auto_increment |
10
-- | name | varchar(255) | YES | | NULL | |
11
-- | email | varchar(255) | YES | | NULL | |
12
-- | gender | int(11) | YES | | NULL | |
13
-- | age | int(11) | YES | | NULL | |
14
-- | opinion | text | YES | | NULL | |
15
-- | created_at | datetime | NO | | NULL | |
16
-- | updated_at | datetime | NO | | NULL | |
17
-- +------------+--------------+------+-----+---------+----------------+
undefined
1
-- id=3のtaro-2さんを使ってみる
2
SELECT name FROM students WHERE id = 3;
3
-- +--------+
4
-- | name |
5
-- +--------+
6
-- | taro-2 |
7
-- +--------+
COMMITするパターン
undefined
1
START TRANSACTION;
2
3
-- id=3のnameを"tran-sakuko"に更新
4
UPDATE students SET name = "tran-sakuko" WHERE id = 3;
5
-- Query OK, 1 row affected (0.01 sec)
6
-- Rows matched: 1 Changed: 1 Warnings: 0
7
8
SELECT name FROM students WHERE id =3;
9
-- +-------------+
10
-- | name |
11
-- +-------------+
12
-- | tran-sakuko |
13
-- +-------------+
14
15
-- コミットする
16
COMMIT;
17
-- Query OK, 0 rows affected (0.00 sec)
18
19
-- COMMITされてるか確認
20
-- select name from students where id =3;
21
-- +-------------+
22
-- | name |
23
-- +-------------+
24
-- | tran-sakuko |
25
-- +-------------+
26
-- 1 row in set (0.00 sec)
ROLLBACKするパターン
undefined
1
START TRANSACTION;
2
-- Query OK, 0 rows affected (0.00 sec)
3
4
-- id=3のnameを"tran sakutarou"に更新
5
UPDATE students SET name = "tran sakutarou" WHERE id =3;
6
-- Query OK, 1 row affected (0.00 sec)
7
Rows matched: 1 Changed: 1 Warnings: 0
8
9
-- id3のnameに新しいデータの"tran sakutarou"がセットされてる
10
SELECT name FROM students WHERE id = 3;
11
-- +----------------+
12
-- | name |
13
-- +----------------+
14
-- | tran sakutarou |
15
-- +----------------+
16
17
-- ロールバックしてみる
18
ROLLBACK;
19
-- Query OK, 0 rows affected (0.01 sec)
20
21
-- セットした"trans sakutarou"というデータはDBに格納されない。
22
SELECT name FROM students WHERE id = 3;
23
-- +-------------+
24
-- | name |
25
-- +-------------+
26
-- | tran-sakuko |
27
-- +-------------+

try on RubyonRails

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

undefined
1
rails new transact_self -d mysql
2
# database.ymlを編集後
3
rails db:create
4
# Userモデル作成
5
rails g model User name:string approved:boolean deleted:boolean
6
# Reviewモデル作成
7
rails g model Review user:references rate:integer approved:boolean
8
rails db:migrate
undefined
1
# input data
2
# user
3
(1..5).each do |i|
4
User.create(name: "taro-#{i}", approved: true, deleted: false)
5
end
6
7
(1..5).each do |i|
8
user = User.first
9
Review.create!(user_id: user.id, rate: i, approved: true)
10
end

DB 内で確認

undefined
1
SELECT * FROM users;
2
-- +----+--------+----------+---------+---------------------+---------------------+
3
-- | id | name | approved | deleted | created_at | updated_at |
4
-- +----+--------+----------+---------+---------------------+---------------------+
5
-- | 1 | taro-1 | 1 | 0 | 2019-04-03 09:01:31 | 2019-04-03 09:01:31 |
6
-- | 2 | taro-2 | 1 | 0 | 2019-04-03 09:01:31 | 2019-04-03 09:01:31 |
7
-- | 3 | taro-3 | 1 | 0 | 2019-04-03 09:01:31 | 2019-04-03 09:01:31 |
8
-- ...
9
10
SELECT * FROM reviews;
11
-- +----+---------+------+----------+---------------------+---------------------+
12
-- | id | user_id | rate | approved | created_at | updated_at |
13
-- +----+---------+------+----------+---------------------+---------------------+
14
-- | 1 | 1 | 1 | 1 | 2019-04-03 09:01:38 | 2019-04-03 09:01:38 |
15
-- | 2 | 1 | 2 | 1 | 2019-04-03 09:01:38 | 2019-04-03 09:01:38 |
16
-- | 3 | 1 | 3 | 1 | 2019-04-03 09:01:38 | 2019-04-03 09:01:38 |
17
-- ...

user と review との、関連付け

app/models/user.rb
1
class User < ApplicationRecord
2
has_many :reviews
3
end

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

app/models/review.rb
1
class Review < ApplicationRecord
2
belongs_to :user
3
validates :approved, presence: true
4
end

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

トランザクション処理に成功し、commit されるパターン

undefined
1
user = User.first
2
User.transaction do
3
user.update!(approved: false)
4
user.reviews.each { |review| review.update!(approved: true) }
5
end
6
7
# =>(0.1ms) BEGIN
8
# User Update (0.3ms) UPDATE `users` SET `approved` = FALSE, `updated_at` = '2019-04-03 09:10:46' WHERE `users`.`id` = 1
9
# Review Load (0.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`user_id` = 1
10
# (6.8ms) COMMIT
11
# => <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">,
12
#<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">,
13
#<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 が、更新されてる

undefined
1
select * from users;
2
-- +----+--------+----------+---------+---------------------+---------------------+
3
-- | id | name | approved | deleted | created_at | updated_at |
4
-- +----+--------+----------+---------+---------------------+---------------------+
5
-- | 1 | taro-1 | 0 | 0 | 2019-04-03 09:01:31 | 2019-04-03 09:10:46 |

トランザクション処理に失敗し、rollback されるパターン

undefined
1
user = User.first
2
User.transaction do
3
user.update!(approved: true)
4
user.reviews.each { |review| review.update!(approved: false) }
5
end
6
=>(0.1ms) BEGIN
7
User Update (0.2ms) UPDATE `users` SET `approved` = TRUE, `updated_at` = '2019-04-03 09:14:44' WHERE `users`.`id` = 1
8
Review Load (0.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`user_id` = 1
9
(6.6ms) ROLLBACK
10
Traceback (most recent call last):
11
3: from (irb):2
12
2: from (irb):4:in `block in irb_binding'
13
1: from (irb):4:in `block (2 levels) in irb_binding'
14
ActiveRecord::RecordInvalid (Validation failed: Approved can't be blank)

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

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

app/models/user.rb
1
class User < ApplicationRecord
2
has_many :reviews
3
4
def suspend!
5
self.class.transaction do
6
disapprove_user!
7
disapprove_reviews!
8
end
9
end
10
11
private
12
def disapprove_user!
13
self.update!(approved: false)
14
end
15
def disapprove_reviews!
16
reviews.each { |review| review.update!(approved: false) }
17
end
18
end

参照