【Ruby on Rails】画面読み込み時にレコードがDELETEされる

## はじめに
Ruby on Railsを学習しているのですが、画面読み込み時の処理（詳細は後述）で登録したレコードが消えるという現象が発生しました。
結論から言うと私の勉強不足、仕様の理解不足によるものでした。
解決にあたり、モデル、アソシエーションの理解が深まったところもあったので、記録として残すことにしました。
コードも冗長ですが、ご容赦ください。

## 開発環境
* Docker
* Ruby 3.3.3
* Rails 7.1.2

## 起：機能の実装

### ■機能の概要
ブックマークの管理機能を作成していた。
![スクリーンショット 2024-07-09 17.03.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/737553/45217d30-5df2-4e22-b5de-ac2f9c2bee96.png)
（グレーの背景がカテゴリ、グリーンの背景がフォルダの中身）

これらはカテゴリ、フォルダ、ブックマークで要素が構成されている。
また階層イメージとしては以下の2種類が挙げられる。
* カテゴリ ＞ フォルダ ＞ ブックマーク
  →フォルダ内にブックマークが存在するパターン 
* カテゴリ ＞ フォルダ、ブックマーク
  →フォルダ内にブックマークが内包されていないパターン

■アソシエーション
・カテゴリはフォルダを複数持つ（一対多）
・カテゴリはブックマークを複数持つ（一対多）
・フォルダはブックマークを複数持つ（一対多）
カテゴリとフォルダにはそれぞれ`depndent: :destroy`を設定している。

■データ取得処理を実装
以下の取得処理を作りました。
```Ruby: app/controllers/dashboards_controller.rb
class DashboardsController < ApplicationController
  before_action :require_login, only: %i[index]
  def index
    @home_layouts = current_user.home_layouts.order(position: :asc)
    @home_layouts.each do |layout|
    
      # カテゴリ情報取得
      layout.categories = layout.categories.order(position: :asc)
      layout.categories.each do |category|

        #▼▼▼▼▼▼▼▼ここから下が本題▼▼▼▼▼▼▼▼
      
        # フォルダなしのブックマークを取得
        category.bookmarks = category.bookmarks.where(folder_id: nil).order(position: :asc)

        # フォルダ情報取得
        category.folders = category.folders.order(position: :asc)
        category.folders.each do |folder|
          # フォルダ内のブックマークを取得
          folder.bookmarks = folder.bookmarks.order(position: :asc)
        end
      end
    end
  end
end
```

## 承：問題発生

データ登録処理を作成し正常に動作していた。
ログは何てことなくコミットまでされている。
```
2024-07-09 16:10:39   Bookmark Maximum (1.9ms)  SELECT MAX("bookmarks"."position") FROM "bookmarks" WHERE "bookmarks"."category_id" = $1 AND "bookmarks"."folder_id" = $2  [["category_id", 3], ["folder_id", 1]]
2024-07-09 16:10:39   ↳ app/models/bookmark.rb:54:in `get_new_position_bookmark'
2024-07-09 16:10:39   TRANSACTION (0.4ms)  BEGIN
2024-07-09 16:10:39   ↳ app/controllers/bookmarks_controller.rb:14:in `create'
2024-07-09 16:10:39   Bookmark Create (12.3ms)  INSERT INTO "bookmarks" ("title", "url", "type", "folder_id", "category_id", "group_id", "user_id", "position", "description", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING "id"  [["title", "Copilot"], ["url", "https://copilot.microsoft.com/?wlexpsignin=1#"], ["type", 0], ["folder_id", 1], ["category_id", 3], ["group_id", nil], ["user_id", 28], ["position", 1], ["description", ""], ["created_at", "2024-07-09 07:10:39.050132"], ["updated_at", "2024-07-09 07:10:39.050132"]]
2024-07-09 16:10:39   ↳ app/controllers/bookmarks_controller.rb:14:in `create'
2024-07-09 16:10:39   TRANSACTION (2.6ms)  COMMIT
```

しかし登録後画面をリロードすると、登録したデータが画面に表示されていない。
DBからもレコードが消えている。（？？？？）

### ■再現する
もう一度同じ手順で確認したところ、画面リロード時にログがでていた。
```
2024-07-09 16:10:51 Started GET "/dashboards" for 192.168.65.1 at 2024-07-09 07:10:51 +0000
2024-07-09 16:10:51 Cannot render console from 192.168.65.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
2024-07-09 16:10:51 Processing by DashboardsController#index as HTML
~~~省略~~~
2024-07-09 16:10:51   TRANSACTION (0.3ms)  BEGIN
2024-07-09 16:10:51   ↳ app/controllers/dashboards_controller.rb:10:in `block (2 levels) in index'
2024-07-09 16:10:51   Bookmark Destroy (5.5ms)  DELETE FROM "bookmarks" WHERE "bookmarks"."id" = $1  [["id", 12]]
2024-07-09 16:10:51   ↳ app/controllers/dashboards_controller.rb:10:in `block (2 levels) in index'
2024-07-09 16:10:51   TRANSACTION (0.9ms)  COMMIT
2024-07-09 16:10:51   ↳ app/controllers/dashboards_controller.rb:10:in `block (2 levels) in index'
~~~省略~~~
```
なぜか`Destroy`処理が走っている。

コードで見ると以下の処理が原因らしい。
```Ruby: app/controllers/dashboards_controller.rb
# フォルダなしのブックマークを取得
category.bookmarks = category.bookmarks.where(folder_id: nil).order(position: :asc)
```

### ■調査
色々と調べてみたが、
・アソシエーションは適切に設定されている。
・callback等の記載はなし
・その他コントローラの`destroy`は呼び出されていない。

考えられるのはモデルに設定した`depndent: :destroy`によりアソシエーションの問題が発生した可能性である。

## 転：原因
原因は`Destroy`処理の実行箇所である以下のコードの書き方が問題であった
```Ruby:
category.bookmarks = category.bookmarks.where(folder_id: nil).order(position: :asc)
```

1.`category.bookmarks`に直接値を設定している
2.アソシエーションによって関連づけられた、レコード（`folder_id`が`null`ではない）が関連から切り離されてしまう。

:::note info
Railsの仕様上`has_many`の関連づけに対して、
直接値を設定することで`Category`から関連が切り離され、
結果として`folder`と関連のある`bookmark`のレコードが削除されていた。
:::

## 結：解決方法
`Category`モデルに以下の記述を追加
`attr_accessor`を使うことで、`setter` `getter`を自動定義してくれる。
```Ruby:app/models/category.rb
  # フォルダを持たないブックマークを格納
  attr_accessor :bookmarks_without_folder
```

コントローラを以下のように修正
```diff_ruby: app/controllers/dashboards_controller.rb
  def index
    @home_layouts = current_user.home_layouts.order(position: :asc)
    @home_layouts.each do |layout|
      # カテゴリ情報取得
      layout.categories = layout.categories.order(position: :asc)
      layout.categories.each do |category|
        # フォルダなしのブックマークを取得
-       category.bookmarks = category.bookmarks.where(folder_id: nil).order(position: :asc)
+       bookmarks_without_folder = category.bookmarks.where(folder_id: nil).order(position: :asc)
        # フォルダなしのブックマークを格納する
+       category.bookmarks_without_folder = bookmarks_without_folder

        # フォルダ情報取得
        category.folders = category.folders.order(position: :asc)
        category.folders.each do |folder|
          # フォルダ内のブックマークを取得
          folder.bookmarks = folder.bookmarks.order(position: :asc)
        end
      end
    end
  end
```


ここで私が勘違いしていたのは、Railsのモデルにはテーブルに存在しないカラムを定義できないのではないかと勘違いしていたことである。


## 最後に
アソシエーションについての理解が足りてないと改めて実感しました。
アソシエーションが苦手でもどうやって理解を深めていくのかがわからないので、課題です。
あとは冗長なコードを書いてしまっているので、書籍等で理解を深めていきたいと思います。