assign_attributesメソッド

記事投稿アプリで公開日が過去の日付なら「公開」、未来の日付なら「公開待ち」と状態を変えるのにつまづいたので忘備録として残しておきます。
前提として記事の状態stateenumで定義されています。

# draft: 下書き, published: 公開, publish_wait: 公開待ち
enum state: { draft: 0, published: 1, publish_wait: 2 }

article_controller.rbのupdateアクションがこのようになっています。

before_action :set_article, only: %i[edit update destroy]

def update
  authorize(@article)

  if @article.update(article_params)
    flash[:notice] = '更新しました'
    redirect_to edit_article_path(@article.uuid)
  else
    render :edit
  end
end

def article_params
  params.require(:article).permit(
    :title, :description, :state, :published_at, :author_id
  )
end

def set_article
  @article = Article.find_by!(uuid: params[:uuid])
end

@article.update(article_params)で情報を受け取り更新処理しています。
しかし下記のように、状態を公開日付を未来の日付にして更新した場合は、状態は公開ではなく公開待ちになるようにしたいのですが、現状のupdateアクションでは公開日がまだ来ていないにも関わらず、公開されてしまいます。

Image from Gyazo Image from Gyazo

これは@article.update(article_params)updateメソッドでarticle_paramsを受け取ったときにDBにstateが公開という情報が保存されるからです。
ユーザーが、公開日を未来の日付、状態を公開で更新してしまうことは考えられるので対策が必要です。
ここでassign_attributesメソッドを使います。

assign_attributesメソッドとは

引数で指定したattributes(属性)を変更するメソッドです。
ただしDBへの保存はされないので別途saveメソッドで保存します。

これを踏まえ、このような流れにしたいです。
① assign_attributesメソッドで変更を受け取る
現在時刻が公開日を過ぎているか、いないかでstateを公開、公開待ち、と振り分ける
③ saveメソッドで保存

ではまずupdateアクションに①、③を書いていきます。

def update
  authorize(@article)

  @article.assign_attributes(article_params) # ①
  # ②
  if @article.save # ③
    flash[:notice] = '更新しました'
    redirect_to edit_article_path(@article.uuid)
  else
    render :edit
  end
end

こうなりました。
あとは②の処理をどのようにするかですが、FatControllerにならないようmodelにメソッドとして切り出しました。

models/article.rb

def publishable? # 現在時刻が公開日を過ぎていればtrueを返す
  Time.current >= published_at
end

def change_state
  return if draft? # draft?はstateがdraftかどうか判定する trueならreturnによりここで処理が終わる(nilが返る)
  self.state = if publishable? # stateがpublishedなら:published(公開)を返す
                 :published
               else # それ以外(publish_waitしか残ってないのでpublish_wait)なら:publish_wait(公開待ち)を返す
                 :publish_wait
               end
end

enumで定義しているのでdraft?のように直感的にstateの状態を確認できます。(draftならtrue、違えばfalseを返す)
あとはこれを②の位置に入れます。

def update
  authorize(@article)

  @article.assign_attributes(article_params) # ①
  @article.change_state # ②
  if @article.save # ③
    flash[:notice] = '更新しました'
    redirect_to edit_article_path(@article.uuid)
  else
    render :edit
  end
end

ではこれで再度確認してみます。
未来の日付で状態を公開にし

Image from Gyazo

更新すると

Image from Gyazo Image from Gyazo

無事、状態が公開待ちに変わりました。

最後に

updateメソッドとはassign_attributes + saveだったんですね。
間に何か処理を挟みたいときにassign_attributesを使うと良さそうです。
updateもupdate_attributesのalias(別名)なので
・update_attributes(DB更新あり)
・assign_attributes(DB更新なし)
のように並べてみると分かりやすいですね。
では今回は以上です。ありがとうございました。

参考サイト

まとめてオブジェクトの属性を変更したい時に便利!assign_attributesメソッド - その辺にいるWebエンジニアの備忘録

ActiveModel::AttributeAssignment

Active Recordのattributesの更新メソッド | 酒と涙とRubyとRailsと

ActiveRecord の attribute 更新方法まとめ - Qiita