このエントリーをはてなブックマークに追加

2016年12月21日水曜日

Rails UPDATE or INSERT

 こんにちは、Hiroです。
 以前のブログで、既にレコードが存在していればUPDATEをし、存在していなければINSERTをする方法として、MySQLの「on duplicate key update」を紹介しました。(以前のブログもぜひ、あわせてお読みください。)
 ただ、以前のブログでも触れているように「on duplicate key update」には「auto_increment」に関わる注意点があります。
 そこで、今回は「on duplicate key update」を使わずに、Railsのプログラムで同様な処理を実現させようと思います。


テーブルの説明


テーブルは以前と同じテーブルを使いますが、こちらで再掲しておきます。
idカラムがPRIMARY KEYで「auto_increment」として定義しています。mst_spot_idがスポットを特定するためのidで、UNIQUEインデックスとなっています。
また、更新対象となるカラムは、like_countでデフォルト値が0としています。
mysql> desc spot_like_counts;
+----------------------------+-----------+------+-----+---------+----------------+
| Field                                | Type      | Null   | Key | Default | Extra             |
+----------------------------+-----------+------+-----+---------+----------------+
| id                                    | int(11)   | NO    | PRI  | NULL    | auto_increment |
| mst_spot_id                 | int(11)   | NO    | UNI | NULL    |                        |
| like_count                     | int(11)   | NO    |         | 0          |                       |
| created_at                    | datetime | NO   |         | NULL   |                       |
| updated_at                   | datetime | NO   |         | NULL   |                       |
+----------------------------+----------+-------+-----+---------+----------------+


RailsプログラムでのUPDATE or INSERT


処理としては、likeをする処理です。
実際には、1ユーザで同じ場所に複数回likeできないように、履歴なども管理する必要がありますが、説明を単純化するためにそのあたりは省略しています。
ポイントは、下記に記載してきます。
  def add_like(mst_spot_id)
    begin
      spot_like_count = SpotLikeCount.find_or_create_by(mst_spot_id: mst_spot_id)
    rescue ActiveRecord::RecordNotUnique => ex
      # SELECTとINSERTの間で万が一別のトランザクションでINSERTが走った場合を考慮
      spot_like_count = SpotLikeCount.find_by(mst_spot_id: mst_spot_id)
    end
    check_in_spot_like_count.increment!(:like_count)
  end

find_or_create_byとActiveRecord::RecordNotUnique

find_or_create_byは、まずSELECTでレコードを取得しようとし、存在しなければINSERTを発行します。
SQLとしては、下記が発行されます。
SELECT  `spot_like_counts`.* FROM `spot_like_counts` WHERE `spot_like_counts`.`mst_spot_id` = 1 LIMIT 1
INSERT INTO `spot_like_counts` (`mst_spot_id`, `created_at`, `updated_at`) VALUES (1, '2016-12-20 08:48:15', '2016-12-20 08:48:15')
上記のとおり、SQLが2本分かれて実行されることから、仮にSELECT文とINSERT文の間に別のアクセスでINSERTが実行された場合、mst_spot_idはUNIQUEインデックスとなっていることから、INSERTに失敗して、例外が発生します。
そこで、rescueで「ActiveRecord::RecordNotUnique」を捕捉し、再度SELECT文で更新対象となるレコードを取得するようにします。

increment!

increment!(:like_count)でlike_countを1つインクリメントするUPDATE文を実行します。
発行されるSQLは下記になります。
UPDATE `spot_like_counts` SET `like_count` = COALESCE(`like_count`, 0) + 1 WHERE `spot_like_counts`.`id` = 1
これで、既にレコードが存在していても、していなくてもlike_countを1つインクリメントすることができます。

本日のブログはいかがでしたでしょうか。他にも方法はあると思いますので、ぜひもっとよい方法があれば、コメントをいただければと思います。
また、末尾になりますが、ぜひ以前のブログもご参照ください。

0 件のコメント:

コメントを投稿