- 投稿日:2021-01-21T16:56:54+09:00
Formオブジェクトを用いて作成したデータを、編集・更新する方法
要点
- 初心者がアプリを作るときの参考に
- formオブジェクトを用いた編集・更新機能は複雑なので詳しく知りたいorおさらいしたい
- 下記のエラーを解消する方法
param is missing or the value is empty: 'Formオブジェクト名'はじめに
メルカリのようなフリマアプリを作成中で、Formオブジェクトを用いて商品にタグ付けして編集・更新(edit・update)する機能を実装まで行いました
各モデルとコントローラーは以下のようになります
- Item/商品
- Tag/タグ
- TagItemRelation/商品とタグの中間テーブル
- TagsItem/ ItemとTagを同時に保存するためのFormオブジェクト
/app/model/item.rbclass Item < ApplicationRecord has_many :tag_item_relations, foreign_key: :item_id, dependent: :destroy has_many :tags, through: :tag_item_relations end/app/model/tag.rbclass Tag < ApplicationRecord has_many :tag_item_relations has_many :items, through: :tag_item_relations validates :tag_name, uniqueness: true end/app/model/tag_item_relation.rbclass TagItemRelation < ApplicationRecord belongs_to :item belongs_to :tag end/app/form/tags_item.rbclass TagsItem include ActiveModel::Model attr_accessor :item_name, :tag_name with_options presence: true do validates :item_name end def save item = Item.create(item_name: item_name) tag = Tag.where(tag_name: tag_name).first_or_initialize tag.save TagItemRelation.create(item_id: item.id, tag_id: tag.id) end endコントローラー
/app/controller/items_controller.rbclass ItemsController < ApplicationController def index @items = Item.all.order('created_at ASC') end def edit @item = current_user.items.find(params[:id]) @tegs_item = TagsItem.new(item: @item) end def update @item = current_user.items.find(params[:id]) @tags_item = TagsItem.new(update_items_params, item: @item) if @tags_item.valid? @tags_item.save redirect_to root_path else render :edit end end private def update_items_params params.require(:item).permit( :item_name, :tag_name ) end endしかし実際に商品を編集・更新をしてみると、、、
param is missing or the value is empty: 'tags_item'のエラーが出てしまい、商品の更新ができませんでした
調べたこと
エラー内容を読んでみると生成したFormオブジェクトの'tag_item_relation'が空になっているとのこと
そして、binding.pryなどを使って調べてみると、そもそもこの記述では'tags_item'が編集・更新では機能していないことが分かります
さらに原因を調べてみると新規投稿(new・create)と編集更新(edit・update)の機能を仕分けしていなかったことだと分かりましたFormオブジェクトで編集更新
今回のエラーの原因はFormオブジェクトに記載したsaveが新規投稿と編集更新で仕分けされていなかったことだと分かりました
/app/form/tags_item.rbclass TagsItem include ActiveModel::Model attr_accessor :item_name, :tag_name with_options presence: true do validates :item_name end def save item = Item.create(item_name: item_name) #ここがcreateアクションのみ tag = Tag.where(tag_name: tag_name).first_or_initialize tag.save TagItemRelation.create(item_id: item.id, tag_id: tag.id) end endこの記述を
/app/form/tags_item.rbclass TagsItem include ActiveModel::Model attr_accessor :item_name, :tag_name with_options presence: true do validates :item_name end # itemがすでに保存されているものか、新規のものかで、PUTとPATCHを分ける delegate :persisted?, to: :item # initializeでFormオブジェクトの値を初期化し、更新の際はdefault_attributesを呼び出す設定 def initialize(attributes = nil, item: Item.new) @item = item attributes ||= default_attributes super(attributes) end def save return if invalid? ActiveRecord::Base.transaction do # mapメソッドを使いsplit_tag_namesをtagの情報に変換 tags = split_tag_names.map { |tag_name| Tag.find_or_create_by!(tag_name: tag_name) } item.update!(item_name: item_name, tags: tags) end rescue ActiveRecord::RecordInvalid false end # formを飛ばす場所を(#createか#updateか)を判別して、切り替えている def to_model item end private attr_reader :item def default_attributes { item_name: item.item_name, tag_name: item.tags.pluck(:tag_name).join(',') } end def split_tag_names tag_name.split(',') end endに変更することで無事にタグ付け機能の編集更新を行うことができました
参考にしたサイト
関連記事
- 投稿日:2021-01-21T16:30:46+09:00
DockerのMySQLコンテナに外部からアクセスしたい
docker ps
docker exec -it 7c10 /bin/bashroot@7c10ef625739:/#
な表示になるので、
mysql -u root -p
でログインする
- 投稿日:2021-01-21T02:51:53+09:00
Dockerを用いてRuby on Railsの環境構築をする方法( Docker初学者向け )
Dockerを初めて勉強するとき、多くの方が「参考書」や「入門Dockerなどのサイト」で基礎的なところから学習すると思います。
しかし、Dockerとは何なのか、何のイメージも沸いていない状態で、様々な単語を羅列されても、さっぱりわからないという方も多いと思います。( 少なくとも私はそうでした。)
なので極論、基礎すっ飛ばしてまず実際にDockerで仮想環境を構築しながら学んでいこうという話です。
そして何となくでいいのでイメージを掴んでから、基礎を学べば良いと思います。
Dockerを初めて学習する方の中で、Ruby on Railsで開発したアプリケーションをポートフォリオとしている方も多いのではないでしょうか。
なので今回は、Dockerを用いてRuby on Railsの環境構築をする方法 をご紹介いたします。ですが、超基本的なことだけ先に説明させてください。
( とにかく早く環境構築した人は 前提 まで飛んでください )Dockerとは
- Dockerエンジンとは -
Dockerエンジンとは Dockerイメージの作成 や コンテナの起動 などを行う、いわばDockerの要です。
- コンテナとは -
コンテナとはDockerエンジン上で構築される仮想環境そのものです。
コンテナは、CentOS
やUbunts
などの OS、Nginx
やMySQL
などの ミドルウェア、Rails
やWordPress
などアプリケーションまで、様々な環境を構築することができます。- Dockerイメージとは -
Dockerイメージは、コンテナを作成するための レシピ のようなものです。
CentOS、MySQL、Ruby などなど、他にも数えきれないほどのDockerイメージがあります。- Dockerfileとは -
Dockerfileは、オリジナルのDockerイメージを作成するための設計書のようなものです。
既存のDockerイメージをベースに、パッケージをインストールしたり、ファイルを書き換えたりした、新しいイメージを作成することができます。- Docker composeとは -
Docker compose とは、複数のコンテナの構築、実行する手順を自動化するツールです。
その手順などを記したファイルがdocker-compose.yml
で、これのおかげで少ないコマンドの実行で複数のコンテナを起動することができます。環境
- MacOS
- Docker 20.10.2
- docker-compose 1.27.4
- Ruby 2.5.7
- Rails 5.2.4
- MySQL 5.7
前提
Docker
およびDocker compose
のインストールを済ませてください- これ以降の説明の
myapp
の部分はご自身のアプリケーション名に変えてください- とにかく早く環境構築したい方は、説明を飛ばし、コピペして コマンドを実行してください。
アプリケーションの作業ディレクトリを作成
mkdir
で作業ディレクトリを作成します。$ mkdir myapp必要なファイルを用意
ファイル構成は以下のようになります。
myapp |-- Dockerfile |-- docker-compose.yml |-- Gemfile |-- Gemfile.lockそれでは、先ほど作成したディレクトリの中に以下の各ファイルを作成していきます。
① Gemfile
Gemfilesource 'https://rubygems.org' gem 'rails', '~> 5.2.4', '>= 5.2.4.2'③で作成する
Dockerfile
でbundle install
する項目があるのですが、そのときにこのGemfile
を使います。② Gemfile.lock
Gemfile.lock
Gemfile.lock
の中身は空でOKです。
Gemfile.lock
はGemfile
とセットで必要なので、用意します。③ Dockerfile
DockerfileFROM ruby:2.5.7 # ベースにするイメージを指定 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs default-mysql-client vim # RailsのインストールやMySQLへの接続に必要なパッケージをインストール RUN mkdir /myapp # コンテナ内にmyappディレクトリを作成 WORKDIR /myapp # 作成したmyappディレクトリを作業用ディレクトリとして設定 COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock # ローカルの Gemfile と Gemfile.lock をコンテナ内のmyapp配下にコピー RUN bundle install # コンテナ内にコピーした Gemfile の bundle install COPY . /myapp # ローカルのmyapp配下のファイルをコンテナ内のmyapp配下にコピー
Dockerfile
は自分オリジナルの イメージ を作成するための設計書のようなものです。
今回はruby:2.5.7
という元々誰かが作ったイメージをベースに、Railsのインストールに必要なパッケージをインストールしたり、myapp
をコピーしたり、bundle install
したりと、様々なもの加えているというわけです。④ docker-compose.yml
docker-compose.ymlversion: '3' # docker-composeの書式のバージョンを指定します。(原則、最新を指定する) services: db: image: mysql:5.7 environment: MYSQL_USER: root MYSQL_ROOT_PASSWORD: pass ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql web: build: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp/ ports: - 3000:3000 depends_on: - db volumes: mysql_data:
docker-compose.yml
は複数のコンテナを定義し、実行するためのファイルです。
ファイルに書かれている単語をひとつずつ解説してきます。services:
services
の中でコンテナを定義します。
今回はMySQL
とRails
をコンテナ化するので、名前をわかりやすくdb
、web
とします。
実際に作成されるコンテナ名は「アプリケーション名 + サービス名 + 連番」でmyapp_db_1
、myapp_web_1
となります。image:
利用するイメージを指定し、コンテナを構築します。
enviroment:
MySQLに関する環境変数を設定します。(今回はパスワードのみ)
パスワードはご自身で決めて構いません。ports:
ポート番号に関するを設定します。
MySQL は3306
、 Rails は3000
というポート番号がデフォルト値として設定されています。
Railsに関してはおなじみのlocalhost:3000
の 3000 ですね。
Dockerを使わず、ローカルで環境構築した場合は直接このlocalhost:3000
にアクセスすれば良かったわけですが、Dockerを使った場合、コンテナに外部からアクセスすることができないので、ちょっとした工夫が必要です。
そこで登場するのがこのports:
です。
ports:
は- ローカル側のポート番号 : コンテナ側のポート番号
で表します。
今回、ports: - 3000:3000
としていますが、説明をわかりやすくするため、仮にもしports: - 9000:3000
に設定するとします。
これはコンテナ側の3000ポート
に9000ポート
でのアクセスを許可するという意味になります。
つまりこの場合、localhost:9000
でアクセス可能になるということになります。volumes:
ボリュームに関する設定をします。
ボリューム
とはコンテナにおいて生成されるデータを永続的に保存するためのものです。
MySQLを例にとると、MySQLをコンテナ化し、そこに テーブル や カラム などのデータを保存したとします。
しかし、そのMySQLのコンテナを削除してしまうと、保存してあったテーブル、カラムなどのデータも一緒に削除されてしまいます。
それはまずいので、データだけローカルにも保存しておく、この仕組みをボリューム
といいます。
このボリュームを利用すると、仮にコンテナを削除したとしても、新しくコンテナを立ち上げたときに、そのデータを再利用することができます。ボリュームを保存する方法は大きく分けて2つあります。
1つ目は、保存したいディレクトリをマウントする方法、2つ目は、名前付きボリュームを利用する方法 です。今回
MySQL
は2つ目の名前付きボリュームを利用する方法
を使います。この方法は、ローカルに名前付きのボリュームを作成し、そこにコンテナ側の指定したディレクトリを保存するというものです。
名前付きボリュームの場合、volumes:
は- ボリューム名 : コンテナ側の保存させたいディレクトリ
で表します。
今回、ボリューム名
をmysql_data
とし、コンテナ側の保存させたいディレクトリ
は MySQLのデータ保存場所である/var/lib/mysql
を指定します。
結果、volumes: - mysql_data:/var/lib/mysql
という書き方になります。
そして、version: や services: と同じ段落(トップレベル)の、一番下に書いているvolumes:
にボリューム名を記載し、名前付きボリュームであることを明示します。次に
Rails
は、1つ目の保存したいディレクトリをマウントする方法
を使います。
この方法は、「ローカル側の指定したディレクトリ」と「コンテナ側の指定したディレクトリ」を同期させて、両者を常に同じ状態に保つというものです。
マウントする場合、volumes:
は- ローカル側の同期させたいディレクトリ : コンテナ側の同期させたいディレクトリ
で表します。
今回、volumes: - .:/myapp/
としています。
つまり、.
( docker-compose.ymlのあるディレクトリ、つまりローカル側のmyapp配下
) と/myapp/
( コンテナ側のmyapp配下
) を同期するということです。
これにより、コンテナ起動後、ローカルのmyapp配下のファイルなどを編集すると、コンテナ側にも編集が反映されます。build:
Dockerfileのあるディレクトリをして指定します。( docker-compose.ymlから見て同じディレクトリにDockerfileが存在するので
.
)
ここのステップで ③で説明したDockerfile
を利用しオリジナルのイメージを作成し、コンテナを構築します。command:
コンテナ起動時の実行されるコマンドを設定します。
今回設定するコマンドは、bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
です。
bash -c ""
は コンテナの中に入り""
内のコマンドを実行するという命令です。
rm -f tmp/pids/server.pid
で rails server が起動していた場合、停止します。( 何らかの原因でrails server がすでに起動していた場合、新しいrails serverが起動できないため。)
bundle exec rails s -p 3000 -b '0.0.0.0'
で rails server を起動します。
&&
は複数のコマンドを実行するときに用います。depends_on:
コンテナの作成順序の設定です。
depends_on: - db
は、MySQLのコンテナ
が起動してからRailsのコンテナ
を起動するという意味です。rails new を実行
$ docker-compose run web rails new . --force --database=mysql --skip-bundleアプリケーションの作業ディレクリに移動し、
docker-compose run
コマンドでrails new
を実行します。
--force
: 同じファイルがある場合、上書きする
--database=mysql
: データベースにMySQLを指定する
--skip-bundle
: bundle install をスキップする。( bundle installはあとで行います。)このコマンドを実行すると、
Dockerfile
を元に、イメージとコンテナを作成し、そのコンテナの中でrails new
を実行します。
よって、ローカルにも見慣れたファイルが作成されたと思います。docker-compose build を実行
$ docker-compose build
rails new
でGemfile
が更新されたので、bundle install
するため、docker-compose build
を実行します。
先ほどのdocker-compose run
で作成されたイメージはrails new
される前のGemfile
をbundle install
したイメージなので、更新されたGemfile
をbundle install
したイメージを再度作成する必要があります。database.yml を編集
/config/database.yml# MySQL. Versions 5.1.10 and up are supported. # # Install the MySQL driver # gem install mysql2 # # Ensure the MySQL gem is defined in your Gemfile # gem 'mysql2' # # And be sure to use new-style password hashing: # https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html # default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root # 以下2行を編集 password: pass host: db development: <<: *default database: myapp_development # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: myapp_test # As with config/secrets.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # # On Heroku and other platform providers, you may have a full connection URL # available as an environment variable. For example: # # DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase" # # You can use this database configuration with: # # production: # url: <%= ENV['DATABASE_URL'] %> # production: <<: *default database: myapp_production username: myapp password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
password:
とhost:
を編集します。
password:
にはdocker-compose.yml
のMYSQL_ROOT_PASSWORD:
に書いたパスワードを記載します。
host:
にはdocker-compose.yml
で命名したMySQLのコンテナ名db
を記載します。docker-compose up -d でコンテナを起動
$ docker-compose up -dこのコマンドでコンテナを起動します。
-d
をつけることでバックグラウンドで起動します。これでコンテナが起動できました。
以下のコマンドでちゃんと起動しているか確認しましょう。$ docker-compose ps Name Command State Ports ---------------------------------------------------------------------------------------- myapp_db_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp, 33060/tcp myapp_web_1 bash -c rm -f tmp/pids/ser ... Up 0.0.0.0:3000->3000/tcpこのように、それぞれのコンテナが
UP
になっていれば起動成功です。データベースを作成
以下のコマンドでデータベースを作成します。
$ docker-compose exec web rails db:createlocalhost:3000にアクセス
localhost:3000
にアクセスし、以下のページが表示されれば DockerによるRailsの環境構築 は完了です。
環境構築後の開発の仕方
これまで通り、ファイルの編集などは基本ローカル環境で行います。
しかしrails g
やrails db:migrate
などの railsコマンド はコンテナ内で実行します。
なので、コンテナに入る必要があります。
以下のコマンドでRailsのコンテナに入ります。$ docker-compose exec web bashするとプロンプト( ターミナルの左側の部分 )が以下のように変わると思います。( @の後ろはコンテナのID )
root@97b8e3430f3f:/myapp#これでコンテナの中に入れました。ここで railsコマンド を実行してください。
これまでのローカル環境での開発で、ターミナルで実行していたコマンドは全てこのコンテナ内で行います。( gitコマンドはローカル環境で行います )コンテナを抜けるには、以下のコマンドを実行してください。( もしくは
control + d
)$ exit- ログの表示 -
これまでは
rails s
でサーバーを起動し、ログが表示されていたと思います。
コンテナ内において、ログを表示させる方法は以下の通りです。コンテナ内に入り、以下のコマンドを実行します。
$ tail -f log/development.logこれでログが表示されます。
ターミナルでタブを3つ開き、「ローカルの操作をするタブ」、「コンテナ内でrailsコマンドなどを実行するタブ」、「ログを表示するタブ」に分けると効率的な開発ができるかもしれません。
- デバッグに関して -
開発中にエラーが発生し、controllerを編集し、デバッグしたとします。
この場合、一度サーバーを再起動させると思います。Dockerの場合、Railsのコンテナ( myapp_web_1 )ごと以下のコマンドで再起動します。
$ docker-compose restart webしかし、デバッグするたびにいちいちコンテナを再起動するのは非効率です。
なので、サーバー起動中もコードの更新をチェックしてくれるように設定します。
config/environments/development.rb
の一番下に記載されている以下をconfig.file_watcher = ActiveSupport::EventedFileUpdateChecker次のように変更します。
config.file_watcher = ActiveSupport::FileUpdateCheckerこれで再起動を行わずにデバッグが可能になります。
その他の基本的なDockerのコマンド
・ コンテナの停止、削除
$ docker-compose down・ コンテナの停止、削除および紐づいている名前付きボリュームの削除
$ docker-compose down -v※ 永続化しているデータが消えるの要注意 !
・ docker-composeのログを出力
$ docker-compose logsコンテナがうまく起動しなかったときなどにログを見ると、原因がわかったりします。
・ イメージの一覧
$ docker images・ イメージの削除
$ docker rmi イメージID・ 名前付きボリュームの一覧
$ docker volume ls・ 名前付きボリュームの削除
$ docker volume rm ボリュームの名前まとめ
最初の内は理解できないことも多いと思います。
ひとつずつ着実に理解していくのではなく、全体的に漠然と理解していくと良いと思います。この記事を読んでわからないことがあれば、コメントしていただければお答えいたします !
最後まで読んでいただきありがとうございました。
参考
- 投稿日:2021-01-21T00:54:01+09:00
MySQL8 の TIMESTAMP, DATETIME におけるExplicit Default な式定義について
MySQL8.0.13 からDEFAULT値に簡単な式を書けるようになりました。超便利です。
timestampでデフォルト値を書いたときに
ALTER TABLE ADD COLUMN
やCREATE INDEX
などテーブル加工をするとエラーになることがあります。
挙動が罠ではあるが、バグではないです。https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html#data-type-defaults-implicit
The exception is that, for TIMESTAMP and DATETIME columns, you can specify the CURRENT_TIMESTAMP function as the default, without enclosing parentheses. See Section 11.2.5, “Automatic Initialization and Updating for TIMESTAMP and DATETIME”.リファレンスマニュアルを見ても、TIMESTAMP と DATETIME だけは例外と書いてあります。
この2つだけは規定のデフォルト値に従わなければいけません、
詳細は、 https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html
基本は、ここに記述されてるもののみデフォルトにできます。TIMESTAMPやDATETIMEはデフォルトを指定しないときのImplicit Default の挙動が他の型と異なるため、NOT NULLのときにトラブルになることがあります。
INSERT や UPDATEなどデータを入れるときはExplicit Defaultの式がおかしくてもエラーにならずに良い感じに補正されて動きますが、ALTERなどのときはNOT NULL型ではエラーになります。mysql> create table tt1 (t timestamp not null default (CURRENT_TIMESTAMP + INTERVAL 1 YEAR), i int); mysql> create table tt2 (t timestamp not null default CURRENT_TIMESTAMP, i int); mysql> create table tt3 (t timestamp null default (CURRENT_TIMESTAMP + INTERVAL 1 YEAR), i int); mysql> create table tt4 (t timestamp null default CURRENT_TIMESTAMP, i int);mysql> desc tt1; +-------+-----------+------+-----+---------------------------+-------------------+ | Field | Type | Null | Key | Default | Extra | +-------+-----------+------+-----+---------------------------+-------------------+ | t | timestamp | NO | | (now() + interval 1 year) | DEFAULT_GENERATED | | i | int | YES | | NULL | | +-------+-----------+------+-----+---------------------------+-------------------+ 2 rows in set (0.00 sec) mysql> desc tt2; +-------+-----------+------+-----+-------------------+-------------------+ | Field | Type | Null | Key | Default | Extra | +-------+-----------+------+-----+-------------------+-------------------+ | t | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | | i | int | YES | | NULL | | +-------+-----------+------+-----+-------------------+-------------------+ 2 rows in set (0.00 sec) mysql> desc tt3; +-------+-----------+------+-----+---------------------------+-------------------+ | Field | Type | Null | Key | Default | Extra | +-------+-----------+------+-----+---------------------------+-------------------+ | t | timestamp | YES | | (now() + interval 1 year) | DEFAULT_GENERATED | | i | int | YES | | NULL | | +-------+-----------+------+-----+---------------------------+-------------------+ 2 rows in set (0.00 sec) mysql> desc tt4; +-------+-----------+------+-----+-------------------+-------------------+ | Field | Type | Null | Key | Default | Extra | +-------+-----------+------+-----+-------------------+-------------------+ | t | timestamp | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | | i | int | YES | | NULL | | +-------+-----------+------+-----+-------------------+-------------------+ 2 rows in set (0.00 sec)mysql> insert tt1 set i=0; mysql> insert tt2 set i=0; mysql> insert tt3 set i=0; mysql> insert tt4 set i=0;普通に入る。
mysql> alter table tt1 add j int; ERROR 1067 (42000): Invalid default value for 't' mysql> alter table tt2 add j int; Query OK, 0 rows affected (0.00 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> alter table tt3 add j int; Query OK, 0 rows affected (0.01 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> alter table tt4 add j int; Query OK, 0 rows affected (0.00 sec) Records: 0 Duplicates: 0 Warnings: 0tt1 だけALTERがエラーになります。
NULLを許可してる場合はエラーになりませんが、たまたま動いてると考えたほうが良さそうです。