- 投稿日:2020-01-15T23:48:06+09:00
Rails modelのjoin戦略についての理解を深める1歩
こちらの記事。よく参考にしてます。
https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58ここで言っているキャッシュとは何か?
同記事の最後の表についてなんとなくふわっとした理解でした。
特にキャッシュする・しないについて。
それについてまとめました。Rails modelって
そのmodelに定義されたリレーションに則ってデータを取得できます
class User < ApplicationRecord has_many :invoices ...というUserモデルがあったとして
user = User.first invoice = user.invoicesというふうに取得できますね
その時、invoicesはどのタイミングで取得する(SQLが発行される)かというとuser.invoicesとしたタイミングです
結合してみよう
先程のmodelを使って結合してみよう
joins
user = User.joins(:invoices).first user.invoices1行目のタイミングで
SELECT "users".* FROM "users" INNER JOIN "invoices" ON "invoices"."user_id" = "users"."id" ORDER BY "users"."id" ASC LIMIT $12行目で
SELECT "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1のようなSQLが発行されました。
invoicesを検索するタイミングは変わってませんね。
なるほど、これがキャッシュされたかどうかってことか。preload
user = User.preload(:invoices).first user.invoicesincludes
preload/eager_loadをいい感じに判断して使用してくれる
1行目の時点でinvoiceも検索しに行ってます
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]] Invoice Load (0.3ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."user_id" = $1 [["user_id", 1]]2行目では特にSQLは発行されていません。
キャッシュされてますね。
そして、SQLの結合は使わずに2回SELECTすることでuser.invoicesを取得してます。eager_load
user = User.eager_load(:invoices).first user.invoicesUsersに対してInvoicesをLEFT OUTER JOINで結合してました。
user.invoicesではキャッシュされてるので何も発行しません。キャッシュしないということ
joinsメソッドでは、結合先のデータを必要としないが、INNER JOINで絞り込みをしたいときに使用する。
なので、user = User.joins(:invoices).first user.invoicesのようなコードは無駄があるということ。
includesとwhereで絞り込みとキャッシュを行うべき。以上
という感じ
- 投稿日:2020-01-15T23:22:57+09:00
ログアウト、アカウント削除の実装 【初学者のReact×Railsアプリ開発 第13回】
やったこと
- ログアウトとアカウント削除の実装
- ログアウトでは、ブラウザのLocalStorageに保存してあるtokenなどの情報を消去している。
- アカウント削除はRailsでdestroy。
成果物
実装手順(Rails)
users_controller
- 消去するアカウントはログイン中のアカウントなので、current_userを使っている
users_controller.rbdef destroy @user = current_api_v1_user @user.destroy render json: { status: 'SUCCESS', message: 'Delete the user', data: @user} end実装手順(React)
Logout.js
- ログアウトといっても、LocalStorageの情報を消去するだけ。
- localStorage.clear();
Logout.jsclass Logout extends React.Component { constructor(props) { super(props); } Logout() { localStorage.clear(); window.location.href = process.env.REACT_APP_BASE_URL; } notLogout() { window.history.back() } render() { const { classes } = this.props; return ( <div> <h3>ログアウトしますか?</h3> <Button variant="contained" size="large" color="secondary" className={classes.button} onClick={this.Logout}> する </Button> <Button variant="contained" size="large" color="primary" className={classes.button} onClick={this.notLogout}> しない </Button> </div> ) } }DeleteAccount.js
- HTTPのDELETEメソッドを使って消去している
DeleteAccount.jsclass Deleteaccount extends React.Component { constructor(props) { super(props); this.Deleteaccount = this.Deleteaccount.bind(this); } Deleteaccount() { const { CurrentUserReducer } = this.props; const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid axios.delete(process.env.REACT_APP_API_URL + `/api/v1/users`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) localStorage.clear(); window.location.href = process.env.REACT_APP_API_URL; } notDeleteaccount() { window.history.back() } render() { const { classes } = this.props; return ( <div> <h3>アカウントを削除しますか?</h3> <Button variant="contained" size="large" color="secondary" className={classes.button} onClick={this.Deleteaccount}> する </Button> <Button variant="contained" size="large" color="primary" className={classes.button} onClick={this.notDeleteaccount}> しない </Button> </div> ) } }
- 投稿日:2020-01-15T22:43:15+09:00
GemのソースをTracePointを使って効率的に読む
Rubyには様々な便利なGemがあるので有効活用しない手はないですよね。
ただ使っているGemが予期せぬ挙動をした時やドキュメントに載ってないような詳細仕様を知りたい時などにソースを読みたくなることがあります。Gemのソースを読みたい場合、GithubなどWeb上に公開されていることが多いのでブラウザでソースを見たり、ローカルにソースを落としてきて見たりすると思います。
ただ愚直にソースを読み始めるとソース量が膨大だったり、メタプロが多用されていたりなどで読解がかなり大変です。
そこでこの記事ではTracePointを使って効率的にソースを読む方法を紹介します。ソースを読んでみよう
具体例があった方が良いので、今回はrailsの
find_or_create_by
を使った場合に呼ばれるソースを探すことにしましょう。
https://github.com/rails/rails今回はローカルマシンにチェックアウトして読むことにします。
この記事では6-0-stableブランチ(2020/01/14時点)を使っています。愚直にやってみよう
該当箇所の探し方は人それぞれだと思いますが、私は最初はメソッド名でgrepすることが多いです。
今回はactiverecord配下にあることが明白なのでactiverecord/配下でgit grep find_or_create_by
しました。activerecord % git grep find_or_create_by CHANGELOG.md: `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database. lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/associations.rb: # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson") lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/associations.rb: # def find_or_create_by_name(name) lib/active_record/associations.rb: # find_or_create_by(first_name: first_name, last_name: last_name) lib/active_record/querying.rb: :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, lib/active_record/relation.rb: # User.find_or_create_by(first_name: 'Penélope') lib/active_record/relation.rb: # User.find_or_create_by(first_name: 'Penélope') lib/active_record/relation.rb: # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') lib/active_record/relation.rb: # User.find_or_create_by(first_name: 'Scarlett') do |user| lib/active_record/relation.rb: def find_or_create_by(attributes, &block) lib/active_record/relation.rb: # Like #find_or_create_by, but calls lib/active_record/relation.rb: def find_or_create_by!(attributes, &block) lib/active_record/relation.rb: # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT lib/active_record/relation.rb: # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by, lib/active_record/relation.rb: # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] lib/active_record/relation/finder_methods.rb: # Person.find_or_create_by(name: 'Spartacus', rating: 4) test/cases/finder_respond_to_test.rb: assert_not_respond_to Topic, :fail_to_find_or_create_by_title test/cases/finder_respond_to_test.rb: assert_not_respond_to Topic, :find_or_create_by_title? test/cases/finder_test.rb: assert_raise(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") } test/cases/finder_test.rb: assert_raise(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") } test/cases/nested_attributes_test.rb: ).find_or_create_by!( test/cases/relation/delegation_test.rb: :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, test/cases/relations_test.rb: def test_find_or_create_by test/cases/relations_test.rb: bird = Bird.find_or_create_by(name: "bob") test/cases/relations_test.rb: assert_equal bird, Bird.find_or_create_by(name: "bob") test/cases/relations_test.rb: def test_find_or_create_by_with_create_with test/cases/relations_test.rb: bird = Bird.create_with(color: "green").find_or_create_by(name: "bob") test/cases/relations_test.rb: assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob") test/cases/relations_test.rb: def test_find_or_create_by! test/cases/relations_test.rb: assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") } test/cases/statement_cache_test.rb: def test_find_or_create_by test/cases/statement_cache_test.rb: a = Book.find_or_create_by(name: "my book") test/cases/statement_cache_test.rb: b = Book.find_or_create_by(name: "my other book")たくさんヒットしましたが、テストコードやコメントアウトされている行は無視すると下記の2行になります。
lib/active_record/querying.rb: :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, lib/active_record/relation.rb: def find_or_create_by(attributes, &block)どちらが呼ばれているのでしょうか?
これだけの情報ではわからないのでさらにソースを読む必要がありそうです。TracePointを使ってみよう
机上でソースを辿るのがツラくなってきたのでTracePointを使って呼び出されたメソッドをトレースしてみます。
動作環境は下記の通り
- Ruby: 2.6.5
- Rails: 6.0.2.1
Rails consleで下記を実行しました。
# TracePointのトレース開始 # :callを指定することでメソッド呼び出しをトレースします trace = TracePoint.trace(:call) do |tp| # 何も指定しないと全Gemがトレースされるのでactiverecordのみ出力するようにしました p tp.inspect if tp.path.include?('activerecord-6') end # トレースされるようになったのでfind_or_created_byを実行 User.find_or_create_by(name: 'hoge') # trace情報が大量に出力されます "#<TracePoint:call `find_or_create_by'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/querying.rb:21>" "#<TracePoint:call `all'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/named.rb:26>" "#<TracePoint:call `current_scope'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:26>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:74>" "#<TracePoint:call `value_for'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:79>" "#<TracePoint:call `raise_invalid_scope_type!'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping.rb:99>" "#<TracePoint:call `base_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:99>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `relation'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:295>" "#<TracePoint:call `create'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:114>" "#<TracePoint:call `relation_class_for'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:120>" "#<TracePoint:call `relation_delegate_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:8>" "#<TracePoint:call `arel_table'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:266>" "#<TracePoint:call `table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:195>" "#<TracePoint:call `reset_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:226>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:195>" "#<TracePoint:call `reset_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:226>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `table_name='@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:205>" "#<TracePoint:call `compute_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:528>" "#<TracePoint:call `base_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:113>" "#<TracePoint:call `base_class'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:99>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `full_table_name_prefix'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:236>" "#<TracePoint:call `undecorated_table_name'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:522>" "#<TracePoint:call `full_table_name_suffix'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:240>" "#<TracePoint:call `table_name='@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/model_schema.rb:205>" "#<TracePoint:call `type_caster'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:280>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/type_caster/map.rb:6>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/arel/table.rb:16>" "#<TracePoint:call `predicate_builder'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:276>" "#<TracePoint:call `table_metadata'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:306>" "#<TracePoint:call `arel_table'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/core.rb:266>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/table_metadata.rb:7>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:7>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/basic_object_handler.rb:6>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/base_handler.rb:6>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/range_handler.rb:8>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/array_handler.rb:8>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder/array_handler.rb:8>" "#<TracePoint:call `register_handler'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation/predicate_builder.rb:46>" "#<TracePoint:call `initialize'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:27>" "#<TracePoint:call `finder_needs_type_condition?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:86>" "#<TracePoint:call `descends_from_active_record?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:76>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `descends_from_active_record?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:76>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `default_scoped'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/named.rb:57>" "#<TracePoint:call `build_default_scope'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/scoping/default.rb:103>" "#<TracePoint:call `abstract_class?'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/inheritance.rb:161>" "#<TracePoint:call `find_or_create_by'@/rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:168>" ・・・(省略)これで最初に呼ばれているのは
lib/active_record/querying.rb#find_or_create_by
ということがサクッと分かりました。
ちなみに候補に上がっていたlib/active_record/relation.rb#find_or_create_by
も58個目に呼ばれていました。TracePointを使うとこのように速く、そして正確にメソッド呼び出しの順を知ることができます。
この情報をインプットとしてソースを読むとスムーズに処理の流れが読めます。ちなみに今回は
:call
を指定して呼び出されたメソッドだけを抽出しましたが、パラメーターや戻り値もトレースできるので、障害の原因調査などでも活躍すると思います。
詳細は"参照"に載せているリンクなどをご覧ください。参照
以下、TracePointの仕様を知る上でとても参考になったリンクです。
すごく参考になった記事
https://qiita.com/siman/items/9426ff6c113247088f7eRubyリファレンス
https://docs.ruby-lang.org/ja/latest/class/TracePoint.html
- 投稿日:2020-01-15T21:03:41+09:00
AWS EC2でCould not find pg-1.1.4 in any of the sources Run `bundle install` to install missing gems.と表示される
こんにちは@yukifreeworld12です。
EC2でエラーが出て解決したのでメモとして、そして後世の同じエラーにハマった人の為にも...1. エラー文
EC2でGitと連携しクローンしていた終盤
$ rake secret Could not find pg-1.1.4 in any of the sources Run `bundle install` to install missing gems. $ gem install pg Building native extensions. This could take a while... ERROR: Error installing pg: ERROR: Failed to build gem native extension. <略> To see why this extension failed to compile, please check the mkmf.log which can be found here: /home/name/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/extensions/x86_64-linux/2.4.0/pg-1.2.2/mkmf.log extconf failed, exit code 1と長いエラー文。
2. 解決策
解決したコマンド
sudo yum install -y postgresql-devel
このコマンドでpostgresql-develをインストールした後
bundle install
でちゃんといけました。3.しかしその後
やりたかった
rake secret
を実行したところ...$ rake secret rake aborted! Gem::LoadError: You have already activated rake 13.0.1, but your Gemfile requires rake 12.3.3. Prepending `bundle exec` to your command may solve this.
bundle exec
と書いてあるのでbundle execを付けて
bundle exec rake secret
としたところいけましたとさ。
- 投稿日:2020-01-15T19:49:19+09:00
Rails (with sorcery)+ React でOAuth認証を実装してみた
sorceryでRails +SPA構成のOauthサンプルがあまりなさそうなので
参考になれば。
環境
backend
- rails 6系
front
- react:16.12
- react-router: v5系
以下、sorceryのwikiに沿いつつ、SPA用でカスタムしたところを中心に記載
sorcery wiki今回はgithub と連携してみる
開発の概要
Backend(Rails)
- oauthからのcallbackを受け取り、ユーザー作成及びログイン処理
Front(React)
- githubAPIへQueryString形式でパラメーターをもたせてアクセス。
- 認証後callBackURLにリダイレクトし、Backendに取得したパラメーターを送信
Backend側開発
external module関連のセットアップ
# external moduleのインストール bundle exec rails g sorcery:install external --only-submodules # migration bundle exec rails db:migrate # 認証用モデル作成 bundle exec rails g model Authentication --migration=false
sorceryのgithub認証関連設定変更
認証用情報の取得
github側の認証設定は以下から行う
https://github.com/settings/developersinitializers/sorcery.rbRails.application.config.sorcery.submodules = [:external] #:external追加 Rails.application.config.sorcery.configure do |config| ... config.github.key = "your github key" config.github.secret = "your github secret" config.github.callback_url = "" config.github.user_info_mapping = {:email => "email" } config.github.scope = "user:email" end
Oauth用controllerの作成
oauths_controllerclass OauthsController < ApplicationController # Frontで取得したToken情報をもとにユーザー認証をするMethod def callback provider = params[:provider] # loginできた場合はここで200を返す if @user = login_from(provider) render json: { status: 'OK' } else begin # loginできない場合は送られてきた情報をもとにユーザー作成 @user = create_from(provider) reset_session auto_login(@user) render json: { status: 'OK' } rescue render json: { status: 'NG' }, status: 400 end end end end
ルーティングの設定
- wiki記載の内容と異なり、oauth用tokenが送信されてくるAPIのみでOK
config/routes.rbRails.application.routes.draw do ... post "oauth/callback" => "oauths#callback" end
Front側開発
関連するルーティングの定義
router.jsxconst AppRouter = () => ( <Router> <Switch> <Route path="/callback/:provider/" component={ExternalAuthCallback} /> <Route path="/sign_in" component={SignIn} /> <Route component={NotFound} /> </Switch> </Router> ); export default AppRouter;
サインインページ
- サインインページの1機能としてGithub認証があるイメージ
- 通常のRailsのOauthと異なり、callbackされるURLはReactで構成されたSPAのURL(=>"/callback/:provider/")が叩かれることに注意
SignIn.jsx// CONST.GITHUB.REDIRECT_URL = "http://localhost:3001/callback/github/" const GITHUB_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CONST.GITHUB.APP_ID}&redirect_url=${CONST.GITHUB.REDIRECT_URL}&scope=user:email`; // ただ queryStringの付与したURLのリンクを踏ませるだけ const signInForm = () => ( <div className={styles.submitBox}> <Button href={GITHUB_AUTH_URL}>GITHUBで認証</Button> </div> ); export default signInForm;
CallbackURLコンポーネント
- callback時に叩かれるURLで利用するコンポーネント
- URLパラメーターでprovider名(今回は"github")を取得
- callbackURLのquesyStringに付与された認証情報(code=XXXX)を取得
- ReactからバックエンドAPIへPostする
oAuthCallback/index.jsximport React, { useState } from "react"; import queryString from "query-string"; import { useHistory } from "react-router"; import { useParams, useLocation } from "react-router-dom"; import { api } from "../../../modules/user"; import Circular from "../../atoms/circular"; const BEFORE = "BEFORE"; const DOING = "DOING"; const ExternalAuth = () => { const location = useLocation(); const history = useHistory(); const { code = "" } = queryString.parse(location.search); const { provider = "" } = useParams(); const [requestStatus, setRequestStatus] = useState(BEFORE); const request = () => { setRequestStatus(DOING); api.sendExternalAuthRequest({ code, provider }).then(isSuccess => { if (isSuccess) { history.push("/member/dashboard"); // login後ページ } else { history.push(PAGE_PATH.AUTH_SIGN_IN); //認証失敗した場合 } }); }; if (requestStatus === BEFORE) { request(); } return ( <div className={styles.container}> <Circular /> </div> ); }; export default ExternalAuth;
Rails APIへのリクエスト
api.jsexport const sendExternalAuthRequest = async ({ code, provider }) => { const requester = requestManager.get(); return requester .post( "/oauth/callback", { code, provider }, ) .then(() => true) .catch(() => false); };
- 投稿日:2020-01-15T19:44:20+09:00
検索画面の実装【初学者のReact✗Railsアプリ開発 第12回】
やったこと
- 投稿の検索画面を作成し、検索結果を表示できるようにした。
- reduxを利用して検索結果を管理。(ページを移動して戻ってくるときに、前の検索結果を表示させるようにしたかった)
- ページネーションの実装にkaminariを使っている
- フォームの実装はredux-form
成果物
実装手順(Rails API)
posts_controller
- 検索を行うコアとなる処理の記述を行っています。
- content LIKE?の使い方を初めて学びました。
- %をつけると、あいまい検索になる。無いと、完全一致。検索ワードはクエリでもらってる。
- ページネーションを使っているので、page_length(何ページまであるか)も返しています。
posts_controllerdef search posts = Post.where("content LIKE ?", "%#{params[:q]}%").page(params[:page] ||= 1).per(10).order('created_at DESC') page_length = Post.where("content LIKE ?", "%#{params[:q]}%").page(params[:page] ||= 1).per(10).total_pages json_data = { posts: posts, page_length: page_length, } render json: { status: 'SUCCESS', message: 'Loaded the posts', data: json_data} endroute.rb(ルートの編集)
- 追加します。
route.rbget 'search', to: 'posts#search'実装手順(React)
Search.js(render)
- 大きく分けて、3つの部品(検索フォーム、検索結果表示部分、ページネーション部分)に分けてレンダリングしている。
- 検索結果表示部分をthis.renderResults()で、初回訪問かどうか(doneFetch)、結果が存在しているかどうか(noResults)で制御している。
Search.jsclass SearchPage extends React.Component { render() { const { SearchResultsReducer } = this.props; const { classes } = this.props; return ( <div> <h3>テーマを検索する</h3> <SearchForm onSubmit={this.searchPost} /> {this.renderResults(SearchResultsReducer.noResults, SearchResultsReducer.doneFetch)} <MuiThemeProvider theme={pagitheme}> <CssBaseline /> <Pagination limit={10} offset={SearchResultsReducer.offset} total={SearchResultsReducer.page_length * 10} onClick={(e, offset) => this.handlePaginationClick(offset)} /> </MuiThemeProvider> </div> ) } }Search.js(function)
- doneFetchの取り扱いが頭を使いました。結局、reduxで管理するのが良いと思います。ページ遷移するだけではreduxのstateは変更されないから。
- 表示の制御は少し頭を使いました。
Search.jsclass SearchPage extends React.Component { constructor(props) { super(props); this.searchPost = this.searchPost.bind(this); } componentDidMount() { const { form } = this.props; const { SearchResultsReducer } = this.props; this.props.actions.getSearchResults(SearchResultsReducer.searchWord, SearchResultsReducer.offset, SearchResultsReducer.doneFetch); } searchPost = values => { const { form } = this.props; this.props.actions.getSearchResults(form.SearchForm.values.notes, 0, true); } handlePaginationClick(offset) { const { form } = this.props; this.props.actions.getSearchResults(form.SearchForm.values.notes, offset, true); } renderResults(noResults, doneFetch) { const { SearchResultsReducer } = this.props; const { classes } = this.props; if (!noResults && doneFetch) { return ( <ul className={classes.ul}> {SearchResultsReducer.items.map((post) => ( <Link className={classes.link} to={"/posts/" + post.id}> <li className={classes.li} key={post.id}> <div className={classes.licontent}> <h3 className={classes.lih3}>{post.content}</h3> </div> </li> </Link> ))} </ul> ) } else if (!doneFetch) { return ( <h3>検索ワードを入力してください</h3> ) } else { return ( <h3>検索結果はありません。</h3> ) } } } }SearchResultsReducer.js
- どのタイミングでdoneFetchとnoResultsの状態を変更するかでレンダリング結果が変わってきます。そこに頭を使いました。
SearchResultsReducer.jsconst initialState = { isFetching: false, items: [], offset: "", page_length: "", noResults: false, searchWord: "", doneFetch: false, }; const SearchResultsReducer = (state = initialState, action) => { switch (action.type) { case 'GET_SEARCHRESULTS_REQUEST': return { ...state, isFetching: true, }; case 'GET_SEARCHRESULTS_SUCCESS': if (action.items.length === 0) { return { ...state, isFetching: false, items: action.items, offset: action.offset, page_length: action.page_length, noResults: true, searchWord: action.searchWord, doneFetch: action.doneFetch, searchWord: action.searchWord, }; } else { return { ...state, isFetching: false, items: action.items, offset: action.offset, page_length: action.page_length, noResults: false, doneFetch: action.doneFetch, searchWord: action.searchWord, }; } case 'GET_SEARCHRESULTS_FAILURE': return { ...state, isFetching: false, error: action.error, searchWord: action.searchWord, doneFetch: action.doneFetch, }; default: return state; } }; export default SearchResultsReducer;actions/index.js
- 前に記述したときと大まかには変わりませんが、doneFetchとかsearchWordといった引数の数が増えているので、そこが注意ですかね。ページネーションと並び替えに対応した投稿一覧画面とAPIの実装【初学者のReact×Railsアプリ開発 第10回】
index.jsexport const getSearchResults = (keyword, offset, doneFetch) => { return (dispatch) => { dispatch(getSearchResultsRequest()) const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid const page_url = offset / 10 + 1 return axios.get(process.env.REACT_APP_API_URL + `/api/v1/search?q=${keyword}&page=${page_url}`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then(response => dispatch(getSearchResultsSuccess(response.data.data.posts, keyword, offset, response.data.data.page_length, doneFetch))) .catch(error => dispatch(getSearchResultsFailure(error, keyword, doneFetch))) }; }; export const getSearchResultsRequest = () => ({ type: 'GET_SEARCHRESULTS_REQUEST', }) export const getSearchResultsSuccess = (json, keyword, offset, page_length, doneFetch) => ({ type: 'GET_SEARCHRESULTS_SUCCESS', items: json, offset: offset, page_length: page_length, searchWord: keyword, doneFetch: doneFetch, }) export const getSearchResultsFailure = (error, keyword, doneFetch) => ({ type: 'GET_SEARCHRESULTS_FAILURE', items: error, searchWord: keyword, doneFetch: doneFetch, })rootReducer.js, SearchForm.js
- 投稿日:2020-01-15T18:41:23+09:00
【Rails】id以外の値を主キーに設定して、他のテーブルから参照する
やりたいこと
・usersテーブルの主キーに
user_no
を設定(idというフィールドは使わない)
・billsテーブルにcustomer_no
を設定する
・userモデルとbillモデルを1対多の関係で紐付ける既存の(他の人が作った)システムと連動する必要があったので、、、
実装
user
app/model/user.rbclass Customer < ApplicationRecord has_many :billsusersテーブルのmigrationファイルclass CreateUsers < ActiveRecord::Migration[5.2] def change create_table :users, primary_key: :user_no, autoincrement: false do |t| t.string :name t.string :email t.timestamps end end endbill
app/model/bill.rbclass Bill < ApplicationRecord belongs_to :user endbillsテーブルのmigrationファイルclass RegenerateBills < ActiveRecord::Migration[5.2] def change create_table :bills do |t| t.references :user #外部キー t.string :item_name t.integer :amount t.integer :total t.timestamps end end end
- 投稿日:2020-01-15T18:16:34+09:00
vagrant環境でも、rails s でRailsを動かしたい
環境
Ubuntu 16.04.5 LTS
Windows10
Vagrant手順
Mac環境でRailsを動かすには、これでいいのだが、
rails sVagrant環境では、このように、いちいちコマンドが面倒くさい。
rails s -b 192.168.33.10 -p 3000履歴から検索して、!してもいいのだが、もっと、サクッと動かしたい。
history | grep 33.10.bashrcをviで開いて、Shift + gで一番下に移動。
vi ~/.bashrc下記の関数を追加する
function rails () { if [[ "$@" =~ ^s$ ]]; then command rails s -b 192.168.33.10 -p 3000 else command rails "$@" fi }変更した、.bashrcを反映させます。
source ~/.bashrcrails sで、rails s -b 192.168.33.10 -p 3000が動いてくれました。
c'est fini
- 投稿日:2020-01-15T16:39:12+09:00
Rails 初歩
Rails 学んでみて
ターミナル実行を行うと、、
テキストの通り再現しているつもりでもlocalhost画面にエラーがよく出てしまいました。
それを正常にするのに手間取ってしまいました。白い画面のエラー
?ターミナル側に問題がある
赤い画面のエラー
?VScode側に問題がある
(参考までに)自分が困ってしまった例db migrate
カラムを作成せずに、そのまま実行を押してしまった場合
↓
カラム作成のコードを書いてからそのまま再実行しても正常に戻らない。。。解決策
rails db:migrate:status = マイグレーションファイルの状態確認
rails db:rollback = マイグレーションファイルをdownへ変更
rails db:migrate = マイグレーションファイルをupに変更
↓
カラムを作成できた!最後に
どの部分が原因のエラーなのか、わからないと詰まる。
それを分かって改善するためにはMVCをしっかり理解していないとできない。
- 投稿日:2020-01-15T15:44:30+09:00
railsでレコードに保存している「連続した改行」を改行を保持したままviewに表示する
環境
Rails 5.2.3
Ruby 2.6.3実現したいこと
例えばArticleモデルのcontentカラムに保存している、
「あいうえおかきこけこ」
をviewに表示したい。解決策
safe_join関数を利用し、改行文字をbrに変換する。
simple_format 関数もあるのだが連続改行が認識されないため、
safe_join関数を利用する。show.html.haml= safe_join(@article.content.split("\n"), tag(:br))参照
https://api.rubyonrails.org/classes/ERB/Util.html#method-c-html_escape
- 投稿日:2020-01-15T13:44:37+09:00
あっさり読むrails③(クラス)
はじめに
前回の記事にて、
もちろんこのままだと味気ないので、実際にはclassを設定したり、他のメソッドを使用することになります
と書きました。
本記事では、そのclassを設定する
の部分を実行しようと思います。実行
前回使用したコードを改良しようと思います。今の所、コードは下記の通りです。
- @products.each do |product| = product.name = product.price = image_tag(product.image)まずは、
name
,price
,image
をひとまとめにするクラスを作ります。index.html.haml- @products.each do |product| .product = product.name = product.price = image_tag(product.image)これで、テーブルに登録してあるだけの、
name,price,image
の3要素を持ったproduct
classのオブジェクトが作られます。
この状態では、name
とprice
でフォントを変えたり、image
の大きさを変更したり、といったことが難しいので、要素毎にclassを設定します。index.html.haml- @products.each do |product| .product = product.name, class: "product__name" = product.price, class: "product__price" = image_tag(product.image, class: "product__image")
- 投稿日:2020-01-15T12:31:25+09:00
Rails6 のちょい足しな新機能を試す 116(MySQL データベース存在チェック編)
はじめに
Rails 6 に追加された新機能を試す第116段。 今回は、
MySQL データベース存在チェック
編です。
Rails 6 では、MySQLのデータベースを存在するかどうかをチェックする方法が少し変わりました。
データベースが存在しないときに、bin/rails db:migrate
を実行した場合、 MySQLのエラーメッセージが英語以外でも、ActiveRecord::NoDatabaseError
が発生するようになりました。Ruby 2.6.5, Rails 6.0.2.1, Rails 5.2.4.1 MySQL 8.0.16 で確認しました。 (Rails 6.0.0 でこの修正が入っています。)
$ rails --version Rails 6.0.2.1今回は、MySQL のエラーメッセージを ja_JP にして起動して、
bin/rails db:migrate
コマンドを使って Rails 6.0.2.1 と Rails 5.2.4.1 の違いを確認してみます。今回、MySQL のエラーメッセージを日本語に切り変えるために mysqld コマンドに
--lc_messages_dir=/usr/share/mysql-8.0 --lc_messages=ja_JP
オプションを追加しています。MySQL 側で直接、データベースが存在しないことを確認する
mysql コマンドでデータベースが存在しないことを確認します。
ここで、メッセージの先頭が 1049 となっていることに注意してください。$ mysql -u root app_development ERROR 1049 (42000): 'app_development' は不明なデータベースです。db:migrate を実行する
エラーを確認するために、
db:create
しないでdb:migrate
を実行するとActiveRecord::NoDatabaseError
が発生します。$ bin/rails db:migrate rails aborted! ActiveRecord::NoDatabaseError: 'app_development' は不明なデータベースです。 /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/mysql2_adapter.rb:28:in `rescue in mysql2_connection'ちなみに MySQL のエラーメッセージが英語の場合も
ActiveRecord::NoDatabaseError
となります。$ bin/rails db:migrate rails aborted! ActiveRecord::NoDatabaseError: Unknown database 'app_development' /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/mysql2_adapter.rb:28:in `rescue in mysql2_connection'Rails 5 では
MySQL のエラーメッセージが日本語の場合は、
Mysql2::Error
となります。 英語の場合は、ActiveRecord::NoDatabaseError
になります。日本語の場合:
$ bin/rails db:migrate rails aborted! Mysql2::Error: 'app_development' は不明なデータベースです。 /usr/local/bundle/gems/mysql2-0.5.2/lib/mysql2/client.rb:90:in `connect'英語の場合
$ bin/rails db:migrate rails aborted! ActiveRecord::NoDatabaseError: Unknown database 'app_development' /usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/connection_adapters/mysql2_adapter.rb:26:in `rescue in mysql2_connection'何が変わったのか
Rails 5 までは、 エラーメッセージに
Unknown database
が含まれた場合にActiveRecord::NoDatabaseError
を発生させていました。
Rails 6 では、 エラーメッセージではなく、エラーの番号が 1049 (最初に mysqlコマンドでデータベースが存在しないことを確認したときの値)であるときに、ActiveRecord::NoDatabaseError
を発生させるようにしています。試したソース
https://github.com/suketa/rails_sandbox/tree/try116_mysql_locale
参考情報
- 投稿日:2020-01-15T12:30:33+09:00
deviseで認証メールのリンククリック時に認証しログイン画面に飛ばす
deviseで認証処理を実装時、confirmableのデフォルトだと、認証メールクリック時に
ActionController::UnknownFormat
エラーが出た。
confirmations/show
のviewが無い。
認証成功時にログイン画面に飛ばしつつ、認証失敗時にconfirmations/show
をエラー画面として表示する。versionは以下。
ruby: 2.6.5 rails: 6.0.1 devise: 4.7.1confirmations_controllerのshowメソッドをoverride
controllers/users/confirmations_controller.rbclass Users::ConfirmationsController < Devise::ConfirmationsController ... # GET /resource/confirmation?confirmation_token=abcdef def show self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token]) if resource.nil? || resource.confirmed? # トークンが不正な場合、アカウント登録(パスワード登録)が済んでいる場合 self.resource = resource_class.confirm_by_token(params[:confirmation_token]) render :show elsif resource.is_confirmation_period_expired? # アカウント登録メールの期限が切れた場合 resource.errors.add(:email, :confirmation_period_expired, period: Devise::TimeInflector.time_ago_in_words(resource_class.confirm_within.ago)) render :show else # activate self.resource = resource_class.confirm_by_token(params[:confirmation_token]) redirect_to new_user_session_path, notice: 'メールアドレスを確認しました。' end end ... endメソッド追加
models/users.rbdef is_confirmation_period_expired? # メールアドレス確認メール有効期限チェック(期限はconfig/initializers/devise.rbのconfirm_withinで設定) self.confirmation_period_expired? endcontrollers/show のviewを作成
views/users/confirmations/show.html.erb<h2>エラー画面</h2> <% if resource.errors.any? %> <article class="message is-danger"> <div class="message-header"> <p><%= pluralize(resource.errors.count, "error") %> prohibited this service from being saved:</p> <button class="delete" aria-label="delete"></button> </div> <div class="message-body"> <ul> <% resource.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> </article> <% end %>routesも忘れずに
config/routes.rb... devise_for :users, controllers: { ... confirmations: 'users/confirmations', ... }, ... devise_scope :user do put 'confirmation', to: 'users/confirmations#show', as: :back_confirmation ...
- 投稿日:2020-01-15T10:27:00+09:00
【Ruby on Rails】タイムゾーンの設定を日本時間に変更する方法
- 投稿日:2020-01-15T08:57:49+09:00
【Rails】開発環境で送信したメールを確認する
letter_opener_web
という、ローカル開発環境で送信したメールを確認するgemがすごく便利だったのでまとめます。サーバーのログを辿らずに、ブラウザ上で良い感じにメールを見れます。Gemfilegroup :development do gem 'letter_opener_web'$ bundle installconfig/environments/development.rb# 以下2行を追加 config.action_mailer.default_url_options = { host: 'localhost:3000' } config.action_mailer.delivery_method = :letter_opener_webconfig/routes.rb# 以下3行を追加 if Rails.env.development? mount LetterOpenerWeb::Engine, at: "/letter_opener" end
localhost:3000/letter_opener
にアクセスすると、Gmail風の画面が表示されて、送信メール一覧、詳細を確認することができます。
参考
- 投稿日:2020-01-15T08:50:52+09:00
超初心者が bundle exec を調べた結果
bundle user って何なんだ?
何かしらをインストールするときにこのコマンドを打っているけど、どういう意味なのかしら?
と思ったので、超初心者なりに調べて要約してみました。
この記事は以下の記事を参考にしています。
https://qiita.com/dawn_628/items/1821d4eef22b9f45eea8#bundler%E3%81%A7%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%97%E3%81%9Fgem%E3%82%92%E7%A2%BA%E8%AA%8D前提
僕のスペック
- プログラミング学習を始めて1ヶ月
- HTML/CSS/Javascript/Ruby/Railsをprogateで一応学習済
- Ruby on rails でポートフォリオを作成中
開発環境
- 端末 : LENOVO ideapad 530S-14ARR
- OS : Windows 10 Home ver.1809
- シェル : PowerShell 5.1.17763.771
- Ruby : 2.6.4
- rails : 5.2.4.1
そもそも bundle とは?
bundler のコマンドの1つだそうで。
bundlerっていうのは「gemの依存関係とバージョンを管理するためのツール」だそうで。
ある1つのgemを使うには、他のgemもインストールする必要がある場合があるらしく、そのような時に、必要なgemもまとめてインストールしてくれる便利なツールがbundlerなのだとか。(参考:https://www.sejuku.net/blog/19426)
それじゃあ、bundle exec は?
exec
はbundle
コマンドのオプションで、このオプションをつけると
「自分のあるアプリに対してだけ実行を行ってくれる」
が、exec
つけないと
「自分の全アプリ(ローカル環境)に対して実行を行う」
ということみたいである。具体例を考えてみた。
例えば、
bundle install
を実行すると、gemfileに記載したgemをインストールしてくれる。
ただ、このインストール先はローカル環境ということになる。
一方、
bundle exec install
を実行すると、このインストール先は自分のあるプロジェクト専用のgem格納BOXにぶち込まれる。
ということなのだと思う。さらに具体例を考えてみよう
今、僕が「tweet_app」と「instagram_app」という2つのアプリを作っていたとしよう。
「tweet_app」にはAというgemを、「instagram_app」にはBというgemを使いたい。
そこで「tweet_app」のgemfileにgem A
、「instagram_app」のgemfileにgem B
と記述し、ターミナルからbundle install
を行ったとしよう。するとローカル環境にAとBがインストールされてしまうので、それぞれのアプリにはAとB両方のgemの効果が発動してしまうことになる。
この時に、このAとBが揃うと何かしらの悪影響が出てしまうのであれば、これはよくないことである。このことから基本的には
bundle exec
を使う方が好ましい気がする。まとめ
bundle exec install
をアプリごとに行うと、それぞれのアプリのgem格納BOXにぶち込まれる。一方でbundle exec install
を行うと、すべてのアプリが共有できるところにgemが保管される。多分
bundle exec install
を使う方が安全。重複したgemファイルが増えることにもなるので記憶容量の問題がでてくるのかもしれないが。
- 投稿日:2020-01-15T08:34:55+09:00
Elastic Beanstalk デプロイする時にハマったところ(Rails)
はじめに
Rails アプリケーションを Elastic Beanstalk を使ってデプロイするときにハマって時間がかかってしまったことを箇条書きにしてみました。
"Elastic Beanstalkはデプロイがすぐできる。"なんて嘘だーと一瞬思いましたが、ポイントさえ押さえておけば、本当に一瞬でデプロイできるようになりますので、ぜひ使ってみてください。関連リンク
Elastic Beanstalk 関連のリンクを下記に載せておくので、必要であれば参考にしてください。。
- ElasticBeanstalk Blue-Green Deployment
- AWS ElasticBeanstalk 環境を切り替える方法(EB CLI)
Elastic Beanstalk
ハマったところ
- 文字コード(日本語の場合、utf8 へ変更必要)
- セキュリティグループ (Mysql2::Error: Can't connect to MySQL server on '**********************' (4))
- EC2 と RDS を別のVPC/サブネット上に置く方法
- RDS のセキュリティグループに EC2 からのアクセスを許可する。
rails db:createをやってくれない?
(Mysql2::Error: Unknown Databese'********')
- 下記コマンドにて、自分でMySQLに接続して、DB作成。
- MySQL への接続(EC2上で(eb ssh))
- mysql -h ****MySQLのエンドポイント?(RDS)***** -P 3306 -u sakaes -p
- DB作成
- create database **************;
initializers/carrierwave.rb 用に beanstalk へS3設定を追記
まとめ
ただエラーを時系列に羅列しただけになってしまいましたが、同じような羅列された情報に自分が助けられたことがあるので、
困っている他の方の役に立てばと思い投稿させていただきました。
- 投稿日:2020-01-15T07:57:01+09:00
Rails MySQL データ型をdecimalにしているのに小数点を扱ってくれなくて困った話
目的
- DBに格納されている数値に少数の値を足そうとして詰まりまくったところをまとめる
困った箇所
- viewファイルで表している入力formの少数の値が入力できない。
- こちらで解決した→HTML 少数入力時に「有効な値を入力してください」と出たときの話
- 入力してDBのカラムに保存しようとしてもカラムに反映されない。
save
でのエラーは出ない。rails console
で当該カラムに少数を入力しsave
するとDBのカラムに反映されるが、formから少数を入力しても足し算されない、整数の足し算はできる。「viewファイルで表している入力formの少数の値が入力できない」の解決方法
- 下記の方法で解決した。
「入力してDBのカラムに保存しようとしてもカラムに反映されない。
save
でのエラーは出ない。」の原因と解決方法調査と原因
- 数値を保存するカラム
stady_time
のデータ型は少数を扱うためにdecimal
にしている。- 下記にカラム
stady_time
を作成したときのマイグレーションファイルを下記に記載する。class AddStudyTimeHashTagToPosts < ActiveRecord::Migration[6.0] def change add_column :posts, :study_time, "decimal" end end
- 原因はカラム作成時にデータ型"decimal"を設定するときにオプションで格納する数値の全桁数と少数部分の桁数のを指定する必要があったようである。
解決方法
- カラム
study_time
を削除して再度オプション付きで作成し直すか、現在存在するstudy_time
カラムのデー型を設定し直すかの二通りがある。- 今回は個人プロダクトであるが、デプロイして使用されているサービスで重要なカラムを一旦削除することはありえないため、データ型を設定し直す方法をとる。
下記の修正を加える
修正前 修正後 修正可否 カラム名 study_time study_time 修正しない データ型 decimal decimal 修正しない データ型のオプション 指定なし 全桁数12桁 少数点以下桁数2桁 修正する 下記にカラムのデータ型設定を修正する手順を記載する。
下記コマンドを実行してマイグレーションファイル(DBの仕様変更を命令するファイル)を作成する。
$ rails g migration change_study_time
db/migrate/
に存在するマイグレーションファイル20200112011439_change_study_time.rb
に下記の記載を行う(#
の行はコメントなので実際はなくても良い)class AddPasswordToUsers < ActiveRecord::Migration[6.0] def change #カラムの変更なのでchenge_column :テーブル名, :カラム名, :データ型, :オプション(数値の全桁数), :オプション(数値の小数点以下の桁数、第二位まで欲しいので2とした) change_column :posts, :study_time, :decimal, precision: 12, scale: 2 end end下記コマンドを実行してマイグレート(DBにマイグレーションファイル通りの仕様変更を行う命令)を行う。
$ rails db:migrateもしマイグレーションファイルの記載を間違えてマイグレートしてしまったら下記の方法でマイグレート前の状態に戻す。
下記方法でカラムの方が正常に設定されているか確認する。
- Rails6 rails consoleからカラムのデータ型を確認する
今回の場合Postモデルのstudy_timeカラムなので下記のコマンドになる。
- rails consoleを起動し下記コマンドを実行する。
>Post.columns_hash['study_time'].type => :decimal「
rails console
で当該カラムに少数を入力しsave
するとDBのカラムに反映されるが、formから少数を入力しても足し算されない、整数の足し算はできる。」の原因と解決方法調査と原因
- rails consoleでの確認と切り分けにより足し算を行なっているコントローラファイルに問題があると感じた。
- 少数は表示できるのに足されないということはformから送られてきた値を数値に変換している箇所に間違いがあると予想した。
下記にpostコントローラの当該処理のメソッドを抜粋して記載する。
def update @post = Post.find_by(id: params[:id]) @post.study_time += params[:study_time].to_i @post.save redirect_to("/posts/#{@post.id}") end
params[:study_time]
にはフォームで入力した少数値が入っている。.to_i
で入力した文字列を数値に変更している。整数を表現する場合は
.to_i
でも良いが、少数を扱い際は別の表現にしないといけない。解決方法
- コントローラでフォームで入力した値を数値をして扱う処理で整数に変換してしまっているので少数も含む数値に変換してあげる記載をする。
.to_i
を.to_f
とする。下記に正しいコードを記載する。
def update @post = Post.find_by(id: params[:id]) @post.study_time += params[:study_time].to_f @post.save redirect_to("/posts/#{@post.id}") end
- 投稿日:2020-01-15T05:19:54+09:00
Rechartsを使って円グラフを表示させるポスト詳細画面の実装【初学者のReact×Railsアプリ開発 第11回】
やったこと
- Reactでポスト詳細画面を実装した。
- rechartsを用いて円グラフを表示させた。
- ログイン中のユーザー情報によって、表示をコントロールした。
- 詳細画面の中で、投票の変更を行えるようにした。
成果物
Rails実装手順
route.rb
route.rbRails.application.routes.draw do namespace :api, defaults: { format: :json } do namespace :v1 do delete 'posts/:id', to: 'posts#destroy' end end root 'home#about' endlikes_controller, posts_controller
すでに実装済み。
Ruby on Rails APIモードのCRUD実装 【初学者のReact✗Railsアプリ開発 第5回】
Ruby on Rails APIモードでいいね機能を実装する【初学者のReact×Railsアプリ開発 第6回】React実装手順
App.js(ルーティング)
App.jsimport PostsDetail from './containers/PostDetail'; <Auth> <Switch> <Route exact path="/" component={Home} /> <Route path='/create' component={Create} /> <Route path='/postslist' component={PostsList} /> <Route exact path="/posts/:id" component={PostsDetail} /> </Switch> </Auth>containers/PostDetail.js(render)
- 条件によって何を表示するかを分けています。
- renderGraphWithConditionでは、投票数が1票以上あるときと0票のときで表示内容を分けています。
- renderButtonWithConditionでは、ログイン中ユーザーのその投稿に対する投票情報で表示を分けています。
- renderDeleteButtonでは、自分が作成した投稿のとき削除ボタンを表示します。
- Scrollbars: react-custom-scrollbarsモジュールはめちゃ便利。
PostDetail.jsrender() { const { CurrentUserReducer } = this.props; const isloggedin = CurrentUserReducer.isLoggedin; const { classes } = this.props; return ( <Scrollbars> <div className={classes.textLeft}> {this.renderGraphWithCondition(this.state.all_count)} {this.renderButtonWithCondition(this.state.user_answer_suki)} {this.renderDeleteButton()} </div> </Scrollbars> ); }containers/PostDetail.js(function)
- 難しいことはやっていないけど、コードを書き切るのはなかなか大変でした。
- ボタンを押したときに呼び出す関数に引数を渡したい。記述例:
<Button onClick={() => this.ChangeLike(1)}>
(【React】イベントハンドラで引数を使いたい【備忘録】)PostDetail.jsconstructor(props) { super(props); this.state = { user_answer_suki: [] }; const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid const { CurrentUserReducer } = this.props; axios.get(process.env.REACT_APP_API_URL + `/api/v1/posts/${this.props.match.params.id}`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then((response) => { const postdata = response.data.data; this.setState({ suki_percent: postdata.post.suki_percent, kirai_percent: 100 - postdata.post.suki_percent, suki_count: postdata.post.suki_count, kirai_count: postdata.post.kirai_count, content: postdata.post.content, created_at: postdata.post.created_at, all_count: postdata.post.all_count, username: postdata.user.name }); }) .catch(() => { this.props.history.push('/') }); axios.get(process.env.REACT_APP_API_URL + `/api/v1/likes/post/${this.props.match.params.id}/user/${uid}`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then((response) => { const answereddata = response.data.data; this.setState({ user_answer_suki: answereddata.suki, user_answer_updatedat: answereddata.updated_at, }) }) this.ChangeLike = this.ChangeLike.bind(this); this.DeletePost = this.DeletePost.bind(this); this.submitLike = this.submitLike.bind(this); } renderGraphWithCondition(all_count) { const { classes } = this.props; if (all_count != 0) { return ( <Paper className={classes.root} elevation={1}> <Typography variant="headline" component="h1" className={classes.content}> {this.state.content} </Typography> <Typography component="p" style={{ fontWeight: 'bold' }}> created by {this.state.username} </Typography> <PieChart suki_percent={this.state.suki_percent} kirai_percent={this.state.kirai_percent} /> <Typography component="p" style={{ fontWeight: 'bold', fontSize: '1.2rem' }}> スキ: {this.state.suki_percent}% ({this.state.suki_count}人) </Typography> <Typography component="p" style={{ fontWeight: 'bold', fontSize: '1.2rem' }}> キライ: {this.state.kirai_percent}% ({this.state.kirai_count}人) </Typography> <Typography component="p" style={{ fontWeight: 'bold' }}> 投票数: {this.state.all_count}人 </Typography> </Paper> ) } else { return ( <Paper className={classes.root} elevation={1}> <Typography variant="headline" component="h1" className={classes.content}> {this.state.content} </Typography> <Typography component="p" style={{ fontWeight: 'bold' }}> created by {this.state.username} </Typography> <Typography component="p" style={{ fontWeight: 'bold' }}> まだ誰も投票してません。 </Typography> <Typography component="p" style={{ fontWeight: 'bold' }}> 投票数: {this.state.all_count}人 </Typography> </Paper> ) } } renderButtonWithCondition(user_answer_suki) { const { classes } = this.props; if (user_answer_suki == 3) { return ( <Paper className={classes.root}> <Button variant="contained" size="large" color="secondary" className={classes.button} onClick={() => this.submitLike(1)}> スキ </Button> <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.submitLike(0)}> キライ </Button> </Paper> ) } else if (user_answer_suki == 2) { return ( <Paper className={classes.root}> <Button variant="contained" size="large" color="secondary" className={classes.button} onClick={() => this.ChangeLike(1)}> スキ </Button> <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.ChangeLike(0)}> キライ </Button> </Paper > ) } else if (user_answer_suki == 1) { return ( <Paper className={classes.root}> スキで回答済み。 <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.ChangeLike(0)}> キライに変更する </Button> </Paper> ) } else if (user_answer_suki == 0) { return ( <Paper className={classes.root}> キライで回答済み。 <Button variant="contained" size="large" color="primary" className={classes.button} onClick={() => this.ChangeLike(1)}> スキに変更する </Button> </Paper> ) } } renderDeleteButton() { const { CurrentUserReducer } = this.props; const { classes } = this.props; if (CurrentUserReducer.items.name === this.state.username) { return ( <Button variant="contained" size="large" color="primary" className={classes.button} onClick={this.DeletePost}> このテーマを削除する </Button> ) } else { } } ChangeLike(suki) { const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid axios.put(process.env.REACT_APP_API_URL + `/api/v1/likes/post/${this.props.match.params.id}`, { 'suki': suki }, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then((response) => { const postdata = response.data.data; this.setState({ suki_percent: postdata.post.suki_percent, kirai_percent: 100 - postdata.post.suki_percent, suki_count: postdata.post.suki_count, kirai_count: postdata.post.kirai_count, content: postdata.post.content, created_at: postdata.post.created_at, all_count: postdata.post.all_count, username: postdata.user.name }); const answereddata = response.data.data.like; this.setState({ user_answer_suki: answereddata.suki, user_answer_updatedat: answereddata.updated_at, }) }) } DeletePost() { const { CurrentUserReducer } = this.props; const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid axios.delete(process.env.REACT_APP_API_URL + `/api/v1/posts/${this.props.match.params.id}`, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) window.history.back(-2) } submitLike(suki) { const { CurrentUserReducer } = this.props; const auth_token = localStorage.auth_token const client_id = localStorage.client_id const uid = localStorage.uid const data = { user_id: CurrentUserReducer.items.id, post_id: this.props.match.params.id, suki: suki, } axios.post(process.env.REACT_APP_API_URL + '/api/v1/likes', data, { headers: { 'access-token': auth_token, 'client': client_id, 'uid': uid } }) .then((response) => { const postdata = response.data.data; this.setState({ suki_percent: postdata.post.suki_percent, kirai_percent: 100 - postdata.post.suki_percent, suki_count: postdata.post.suki_count, kirai_count: postdata.post.kirai_count, content: postdata.post.content, created_at: postdata.post.created_at, all_count: postdata.post.all_count, username: postdata.user.name }); const answereddata = response.data.data.like; this.setState({ user_answer_suki: answereddata.suki, user_answer_updatedat: answereddata.updated_at, }) }) }components/SimplePieChart.js
- 円グラフを表示させるモジュールとして、rechartsを用いました。
- rechartsのコードはこちらを参考にしました。http://recharts.org/en-US/examples/PieChartWithCustomizedLabel
SimplePieChart.jsimport React, { PureComponent } from 'react'; import { PieChart, Pie, Sector, Cell, } from 'recharts'; const COLORS = ['#FF8042', '#0088FE',]; const RADIAN = Math.PI / 180; const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent, index, name }) => { const radius = innerRadius + (outerRadius - innerRadius) * 0.5; const x = cx + radius * Math.cos(-midAngle * RADIAN); const y = cy + radius * Math.sin(-midAngle * RADIAN); return ( <text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central" style={{ fontWeight: 'bold', whiteSpace: 'pre-line' }}> {`${(percent * 100).toFixed(0)}%`} </text> ); }; export default class SimplePieChart extends PureComponent { //static jsfiddleUrl = 'https://jsfiddle.net/alidingling/c9pL8k61/'; constructor(props) { super(props) } render() { const { suki_percent, kirai_percent } = this.props; const data = [ { name: 'スキ', value: suki_percent }, { name: 'キライ', value: kirai_percent }, ]; return ( <PieChart width={300} height={300}> <Pie startAngle={90} endAngle={-270} data={data} cx={120} cy={120} labelLine={false} label={renderCustomizedLabel} outerRadius={100} fill="#8884d8" dataKey="value" > { data.map((entry, index) => <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />) } </Pie> </PieChart> ); } }
- 投稿日:2020-01-15T02:57:29+09:00
画像の複数枚投稿と編集とプレビューと私
以前の記事にて
初投稿につき緊張ながら投稿したのをよく覚えています。
たくさん見てくださってありがとうございました。
画像の複数投稿??プレビュー表示??え??しかし......
「投稿のみやん。editは?はよ。」と多数ご指摘をいただきました。
しかし.......
何の成果もあげられませんでした。
編集することができませんでした。
藁にもすがる思いでメンターさんにコードを見てもらい、アドバイスを仰ぎましたが、
「僕こんな実装したことないし、こんなやり方見たことない。
メンテナンス性も悪いし可読性もよろしくない。
他に効率のいい方法あるから.....やり直そうか」と、ありがたくも残酷なご指摘をいただきました。
参考にしてくださった方々ごめんなさい。
メンテナンス性が悪く可読性もよくないお粗末なコードを書いてしまいました......なので今回改良版をやります。意地で。
仕様
- 画像は5枚投稿できる
- 投稿した画像は1枚ずつプレビューされる
- 5枚目を投稿すると投稿欄が消える
- ドラッグ&ドロップは非実装
- 削除を押すとプレビューが消える
前回の反省
前回は新規投稿に注力しすぎて編集する時のことを考えられていなかったことが敗因でした。
新規投稿が完成した時点でやりきった感がありました。
スプリントレビューで「編集は?」と言われて「Oh.......」ってなったものの対応できず、
「1枚画像が投稿できる状態にする」というレビューOKの最低ラインに合わせるべくJSファイルを消去しました。悲しい。「後のことを考えて実装する」というもっとも重要な設計思想が抜けていました。
大いに反省するきっかけとなったのでよしとしましょう。しかし僕は諦めが悪いので、
今回は編集機能の実装を前提として考えていきたいと思います。
ではLET'S GO.編集するには....
まず、編集ページへアクセスすると、
登録済みの写真についてはプレビューが表示されている状態にしなければいけません。
前回は、画像登録の際にdataTransferというデータの箱を使って、1つのfile_fieldにデータを追加していく形で複数投稿を実装していました。
しかし、いざeditを実装するとなった際に、dataTransferにデータを追加することができず断念する結果となりました......
1つのfile_fieldを酷使する実装にはやはり無理があったようです。無理させてごめんねfile_field。そこで今回は別々のidを持ったfile_fieldを5つ作成し、そのそれぞれにデータを入れていく形で実装することにしました。
これなら画像の編集も削除もできそうです。
では画像投稿機能から実装していきましょう。プレビュー表示と削除
前回も利用したsample_appを使用します。
items/new.haml.main %section.main__block = form_with model:@item, local:true do |f| %h2.sell__block__head 商品の情報を入力 .sell__block__form .sell__block__form__upload %h3.sell__block__form__upload__head 出品画像 %span.require 必須 %p 最大5枚までアップロードできます .post__drop__box__container .prev-content .label-content %label{for: "item_images_attributes_0_image", class: "abel-box", id: "label-box--0"} %pre.label-box__text-visible クリックしてファイルをアップロード .hidden-content = f.fields_for :images do |i| = i.file_field :image, class: "hidden-field" %input{class:"hidden-field", type: "file", name: "item[images_attributes][1][image]", id: "item_images_attributes_1_image" } %input{class:"hidden-field", type: "file", name: "item[images_attributes][2][image]", id: "item_images_attributes_2_image" } %input{class:"hidden-field", type: "file", name: "item[images_attributes][3][image]", id: "item_images_attributes_3_image" } %input{class:"hidden-field", type: "file", name: "item[images_attributes][4][image]", id: "item_images_attributes_4_image" } .sell__block__form__name .form-group__name %label 商品名 %span.require 必須 %div = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name" .sell__block__form__btn %div = f.submit "出品する",class: "btn-default__btn-red"items.scss//投稿欄以外のCSSは省略します //image投稿欄のCSS .post__drop__box__container{ display: block; margin: 16px auto 0; display: flex; flex-wrap: wrap; //プレビュー表示欄のCSS .prev-content { display: flex; .preview-box { height: 162px; width: 112px; margin: 0 15px 10px 0; .upper-box { height: 112px; width: 100%; img{ width: 112px; height: 112px; } } .lower-box { display: flex; text-align: center; .update-box { color: #00b0ff; width: 50%; height: 50px; line-height: 50px; border: 1px solid #eee; background: #f5f5f5; cursor: pointer; } .delete-box { color: #00b0ff; width: 50%; height: 50px; line-height: 50px; border: 1px solid #eee; background: #f5f5f5; cursor: pointer; } } } } //投稿クリックエリアのCSS .label-content{ margin-bottom: 10px; width: 620px; .label-box { display: block; border: 1px dashed #ccc; position: relative; background: #f5f5f5; width: 100%; height: 162px; cursor: pointer; &__text-visible { position: absolute; top: 50%; left: 16px; right: 16px; text-align: center; font-size: 14px; line-height: 1.5; font-weight: bold; -webkit-transform: translate(0, -50%); transform: translate(0, -50%); pointer-events: none; white-space: pre-wrap; word-wrap: break-word; } } } //file_fieldのcss .hidden-content { .hidden-field { display: none; } .hidden-checkbox { display: none; } } }今回は画像を5枚投稿するので、file_fieldを5つ作りました。
全てdisplay: none;で隠しています。
こんな感じ。
5つのfile_fieldには別のidが振ってあります。
したがって、file_fieldに画像が入るたびにlabel側のforを変更していけば1枚ずつ複数の画像を投稿することができますね。では、JSで操作してきましょう。
item_new.js$(document).on('turbolinks:load', function(){ $(function(){ //プレビューのhtmlを定義 function buildHTML(count) { var html = `<div class="preview-box" id="preview-box__${count}"> <div class="upper-box"> <img src="" alt="preview"> </div> <div class="lower-box"> <div class="update-box"> <label class="edit_btn">編集</label> </div> <div class="delete-box" id="delete_btn_${count}"> <span>削除</span> </div> </div> </div>` return html; } // ラベルのwidth操作 function setLabel() { //プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定 var prevContent = $('.label-content').prev(); labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, '')); $('.label-content').css('width', labelWidth); } // プレビューの追加 $(document).on('change', '.hidden-field', function() { setLabel(); //hidden-fieldのidの数値のみ取得 var id = $(this).attr('id').replace(/[^0-9]/g, ''); //labelボックスのidとforを更新 $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); //選択したfileのオブジェクトを取得 var file = this.files[0]; var reader = new FileReader(); //readAsDataURLで指定したFileオブジェクトを読み込む reader.readAsDataURL(file); //読み込み時に発火するイベント reader.onload = function() { var image = this.result; //プレビューが元々なかった場合はhtmlを追加 if ($(`#preview-box__${id}`).length == 0) { var count = $('.preview-box').length; var html = buildHTML(id); //ラベルの直前のプレビュー群にプレビューを追加 var prevContent = $('.label-content').prev(); $(prevContent).append(html); } //イメージを追加 $(`#preview-box__${id} img`).attr('src', `${image}`); var count = $('.preview-box').length; //プレビューが5個あったらラベルを隠す if (count == 5) { $('.label-content').hide(); } //ラベルのwidth操作 setLabel(); //ラベルのidとforの値を変更 if(count < 5){ //プレビューの数でラベルのオプションを更新する $('.label-box').attr({id: `label-box--${count}`,for: `item_images_attributes_${count}_image`}); } } }); // 画像の削除 $(document).on('click', '.delete-box', function() { var count = $('.preview-box').length; setLabel(count); //item_images_attributes_${id}_image から${id}に入った数字のみを抽出 var id = $(this).attr('id').replace(/[^0-9]/g, ''); //取得したidに該当するプレビューを削除 $(`#preview-box__${id}`).remove(); console.log("new") //フォームの中身を削除 $(`#item_images_attributes_${id}_image`).val(""); //削除時のラベル操作 var count = $('.preview-box').length; //5個めが消されたらラベルを表示 if (count == 4) { $('.label-content').show(); } setLabel(count); if(id < 5){ //削除された際に、空っぽになったfile_fieldをもう一度入力可能にする $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); } }); }); })プレビュー表示の流れとしては、
(1) id = 0 〜 5のfile_fieldと、for = 0のlabelがある
(2) id = 0のfile_fieldに画像を放り込む
(3) labelのforがJS側で for = 1に変更される
(4) label部をクリックするとid = 1のfile_fieldに画像が入るようになる
(5) 繰り返し 5枚投稿するとlabelが消える
って感じです。
GIFで見るとわかりやすいかも。
......7秒は短い。
しかし、1枚目を投稿したあとでfile_fieldの判定が2つ目に移動しているのがわかりますね。また、プレビュー削除の流れとしては
(1) プレビューが2つある。それぞれが格納されているfile_fieldのidは0, 1。
(2) id = 0の方の削除ボタンを押す
(3) プレビューが消え、id = 0のfile_fieldの中身が消える
(4) labelのforオプションを0として更新
(5) id = 0のfile_fieldが再度入力可能になる
って感じで実装しています。
GIFで見ると......
削除したfile_fieldが再度入力可能になっているのがわかりますね。
プレビュー表示側ではプレビュー数をもとにlabelのforオプションを更新するよう設定しているので、この後ファイルを追加する際にはちゃんと次の空のfile_fieldに判定が移動する実装になっています。controller側はというと......
items_controller.rbclass ItemsController < ApplicationController def new @item = Item.new @images = @item.images.build end def create @item = Item.new(item_params) @item.save end private def item_params params.require(:item).permit( :name, [images_attributes: [:image]]) end endこんな感じです。サンプルなのでめちゃくちゃシンプルに書いてます。お許しを。
また、DBのカラム名ですが
imagesテーブルに関しては「image」カラムにファイル名が保存される形で実装しています。これで画像の複数投稿ができるようになりました。良かったね。
編集機能
さて今回の本題です。
編集ページへのアクセス時にどうやってプレビューを出すか......ですが、
わかりやすくeach文で表示することにしました。edit.haml.main %section.main__block = form_with model:@item, local:true do |f| %h2.sell__block__head 商品の情報を入力 .sell__block__form .sell__block__form__upload %h3.sell__block__form__upload__head 出品画像 %span.require 必須 %p 最大5枚までアップロードできます .post__drop__box__container .prev-content //JSで挿入したhtmlと同じ形 each文でのプレビュー表示 - @item.images.each do |image| .preview-box .upper-box = image_tag image.image.url, width: "112", height: "112", alt: "preview" .lower-box .update-box %label.edit-btn 編集 .delete-box .delete-btn %span 削除 .label-content //プレビューの数に合わせてforオプションを指定 = f.label :"images_attributes_#{@item.images.length}_image", class: "label-box", id: "label-box--#{@item.images.length}" do %pre.label-box__text-visible クリックしてファイルをアップロード .hidden-content = f.fields_for :images do |i| //プレビューが出ている分のfile_fieldとdelete用のチェックボックスを設置 = i.file_field :image, class: "hidden-field" = i.check_box:_destroy, class: "hidden-checkbox" //5つのfile_fieldを準備するに当たって、足りない分を表示 - @item.images.length.upto(4) do |i| %input{name: "item[images_attributes][#{i}][image]", id: "item_images_attributes_#{i}_image", class:"hidden-field", type:"file"} .sell__block__form__name .form-group__name %label 商品名 %span.require 必須 %div = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name" .sell__block__form__btn %div = f.submit "出品する",class: "btn-default__btn-red"file_field関連のdisplay: none;を外すとこんな感じになります。
ちょっとわかりにくいですが、現在投稿済みのプレビューに対応するfile_fieldの横に削除用のチェックボックスが追加されています。編集の流れとしては、
(1) id = 0, 1のfile_fieldが投稿済みのものと対応しているものとする
(2) id = 0の削除ボタンを押す
(3) プレビューが削除されるとともに、id = 0のfile_fieldに対応する削除用チェックボックスにチェックが入る
(4) 投稿時と同様、labelをクリックすると削除済みのid = 0のfile_fieldがアクティブになる
(5) 再度id = 0に新しい画像が入ったら、削除用チェックボックスのチェックを外すこんな感じで実装していきたいと思います。
(4)を見るに、「item_new.js」に追記、場合分けする形でeditも一緒に実装できそうですね。完成コードはこちら。
item_new.js$(document).on('turbolinks:load', function(){ $(function(){ //プレビューのhtmlを定義 function buildHTML(count) { var html = `<div class="preview-box" id="preview-box__${count}"> <div class="upper-box"> <img src="" alt="preview"> </div> <div class="lower-box"> <div class="update-box"> <label class="edit_btn">編集</label> </div> <div class="delete-box" id="delete_btn_${count}"> <span>削除</span> </div> </div> </div>` return html; } // 投稿編集時 //items/:i/editページへリンクした際のアクション======================================= if (window.location.href.match(/\/items\/\d+\/edit/)){ //登録済み画像のプレビュー表示欄の要素を取得する var prevContent = $('.label-content').prev(); labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, '')); $('.label-content').css('width', labelWidth); //プレビューにidを追加 $('.preview-box').each(function(index, box){ $(box).attr('id', `preview-box__${index}`); }) //削除ボタンにidを追加 $('.delete-box').each(function(index, box){ $(box).attr('id', `delete_btn_${index}`); }) var count = $('.preview-box').length; //プレビューが5あるときは、投稿ボックスを消しておく if (count == 5) { $('.label-content').hide(); } } //============================================================================= // ラベルのwidth操作 function setLabel() { //プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定 var prevContent = $('.label-content').prev(); labelWidth = (620 - $(prevContent).css('width').replace(/[^0-9]/g, '')); $('.label-content').css('width', labelWidth); } // プレビューの追加 $(document).on('change', '.hidden-field', function() { setLabel(); //hidden-fieldのidの数値のみ取得 var id = $(this).attr('id').replace(/[^0-9]/g, ''); //labelボックスのidとforを更新 $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); //選択したfileのオブジェクトを取得 var file = this.files[0]; var reader = new FileReader(); //readAsDataURLで指定したFileオブジェクトを読み込む reader.readAsDataURL(file); //読み込み時に発火するイベント reader.onload = function() { var image = this.result; //プレビューが元々なかった場合はhtmlを追加 if ($(`#preview-box__${id}`).length == 0) { var count = $('.preview-box').length; var html = buildHTML(id); //ラベルの直前のプレビュー群にプレビューを追加 var prevContent = $('.label-content').prev(); $(prevContent).append(html); } //イメージを追加 $(`#preview-box__${id} img`).attr('src', `${image}`); var count = $('.preview-box').length; //プレビューが5個あったらラベルを隠す if (count == 5) { $('.label-content').hide(); } //プレビュー削除したフィールドにdestroy用のチェックボックスがあった場合、チェックを外す============= if ($(`#item_images_attributes_${id}__destroy`)){ $(`#item_images_attributes_${id}__destroy`).prop('checked',false); } //============================================================================= //ラベルのwidth操作 setLabel(); //ラベルのidとforの値を変更 if(count < 5){ $('.label-box').attr({id: `label-box--${count}`,for: `item_images_attributes_${count}_image`}); } } }); // 画像の削除 $(document).on('click', '.delete-box', function() { var count = $('.preview-box').length; setLabel(count); var id = $(this).attr('id').replace(/[^0-9]/g, ''); $(`#preview-box__${id}`).remove(); //新規登録時と編集時の場合分け========================================================== //新規投稿時 //削除用チェックボックスの有無で判定 if ($(`#item_images_attributes_${id}__destroy`).length == 0) { //フォームの中身を削除 $(`#item_images_attributes_${id}_image`).val(""); var count = $('.preview-box').length; //5個めが消されたらラベルを表示 if (count == 4) { $('.label-content').show(); } setLabel(count); if(id < 5){ $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); } } else { //投稿編集時 $(`#item_images_attributes_${id}__destroy`).prop('checked',true); //5個めが消されたらラベルを表示 if (count == 4) { $('.label-content').show(); } //ラベルのwidth操作 setLabel(); //ラベルのidとforの値を変更 //削除したプレビューのidによって、ラベルのidを変更する if(id < 5){ $('.label-box').attr({id: `label-box--${id}`,for: `item_images_attributes_${id}_image`}); } } //============================================================================= }); }); });items_controller.rbclass ItemsController < ApplicationController #省略 def edit @item = Item.find(params[:id]) end def update @item = Item.find(params[:id]) @item.update(item_update_params) end private def item_params params.require(:item).permit( :name, [images_attributes: [:image]]) end def item_update_params params.require(:item).permit( :name, [images_attributes: [:image, :_destroy, :id]]) end endJSに関しては、コメントアウトの「============================」で囲んであるところが追記箇所です。
わかりにくかったらすみません。動作はこのようになります。
削除を押すとチェックボックスにチェックが入ります。
このまま『編集する』ボタンを押すと、画像が削除されます。
画像を選択した場合、チェックボックスからチェックが外れます。
この状態で『編集する』ボタンを押すと、画像が差替わります。
編集後のリンク先を指定していないので微妙な感じですが、
each文で引っ張り出される画像が更新されているのがわかりますね。ね??一旦これで完成です。
はぁ、疲れた。
最後に
とりあえず形はなんとかできました。
まだエラーは残っていると思うので、完璧なものは保証できません。
自分で試した中ではエラー起こらなかったですが、
もし見落としがあったら教えていただけると嬉しいです。僕ができるのはここまでです。参考にして頂けましたら幸いです。
あとドラッグ&ドロップやら10枚投稿への対応やら2段目のドロップボックス出現やら削除やら色々あると思いますが、
皆さんの手でよしなに実装してください。また気が向いたら何か書きます。
おわり。
- 投稿日:2020-01-15T02:12:45+09:00
【Rails6】gem `rails-erd`を使おうとしたらエラー`Warning: Ignoring invalid association`
はじめに
コマンド入力で手軽にER図が出力できるgem
rails-erd
を使ってみたときに発生した以下エラーについて解決法を記載します。$ bundle exec erd --filetype=dot Loading application in 'app_name'... Generating entity-relationship diagram for 1 models... Warning: Ignoring invalid association :posts on User (model Post exists, but is not included in domain) Warning: Ignoring invalid association :favorites on User (model Favorite exists, but is not included in domain) Diagram saved to 'erd.dot'.※使用説明については公式ドキュメントをご参照下さい。(しばらく更新されてないようです)
環境
OS: macOS Catalina 10.15.1 zsh: 5.7.1 Ruby: 2.6.5 Rails: 6.0.2.1症状
users
テーブルposts
テーブルfavorites
テーブル元々は上記のようにモデルが3つあり、メインの
users
テーブルにその他2つが関連付けされている状態です。そのテーブル間の関係をER図で確認したかったのですが、冒頭のエラーが発生し、
users
テーブルだけのER図(むしろRelationがないのでE図)が出力される状態。エラーメッセージ
エラーメッセージを詳しく見ると、ここが問題のようです。(Postモデルで抜粋)
Warning: Ignoring invalid association :posts on User (model Post exists, but is not included in domain)
特に以下が問題。
「Postモデルは存在するけど、ドメインに含まれてないよ!」
と書いています。model Post exists, but is not included in domain
つまり、どうにかしてドメインに含めてしまえば解決しそうです。
ドメインとは?
自分はインターネット上の住所という認識ばかり持っていましたが、そもそもは領域・定義域という意味です。
つまり、今回は
rails-erd
が認識してくれる領域に該当モデルが入ってくれていないということになります。なぜ認識してくれないのでしょう?
結論:解決策
config/environments/development.rbRails.application.configure do #... config.eager_load = true #元はfalse #... end上記の設定で、無事に全てのモデルが認識され、読み込めるようになります。
理由
こちらの記事より引用させて頂きます。
Railsアプリケーションで、config.eager_load = falseになっていると、そのクラスが存在するか?(定数が存在するか?)を確認しようとしても、クラスにアクセスする前ならばfalseが返ってきます。
どうやら
rails-erd
が存在確認しようとしても、config.eager_load = falseだと存在しないよ!と答えてしまい、冒頭のエラーメッセージに繋がってしまうようです。
そのため、
config.eager_load = trueにすると
クラスにアクセスする前から、クラスの存在確認(定数定義の確認)をできるようになっています。
ということですね。
それぞれの挙動の違いもわかりやすく書かれていた記事だったので、ご興味ある方はご確認下さい。
助かりましたおわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2020-01-15T00:11:27+09:00
Unknown action ,The action 'create' could not be found for MessagesControllerの一例
1.どんなエラー
MessagesControllerにcreateが定義されていませんよという内容です。
ちなみに筆者は下記の通りcreateは定義していました。<エラー文>
<エラーに該当するファイル>
messages_controller.rbclass MessagesController < ApplicationController before_action :set_group def index @message = Message.new @messages = @group.messages.includes(:user) end end def create @message = @group.messages.new(message_params) if @message.save redirect_to group_messages_path(@group), notice: 'メッセージが送信されました' else @messages = @group.messages.includes(:user) flash.now[:alert] = 'メッセージを入力してください。' render :index end end private def message_params params.require(:message).permit(:content, :image).merge(user_id: current_user.id) end def set_group @group = Group.find(params[:group_id]) end2.原因
今回の場合は'class MessagesController'の'end'の位置が'create'の定義の前で記入してしまっていたため、定義されていないという状態となっていました。
なので八行目の'end'を一番下にカットアンドペーストすることで解決します。3.類似ケース
ちなみに下記のurlでは変数名を間違っていたため、同様なエラーとなっていますが、筆者も変数'message'を'messages'としているというミスがあったため、そちらも確認していただければいいのかなと思います
<参考にした記事>
https://teratail.com/questions/83406<筆者が変数で間違えていた箇所>
_message.html.haml.messages ←この変数が複数形になっていました .chat-main__message-list .chat-main__message-list__name =message.user.name .chat-main__message-list__name__date =message.created_at.strftime("%y年%m月%d日%H時%M分") .chat-main__message-list__comment - if message.content.present? %p.chat-main__message-list__comment__content = message.content = image_tag message.image.url, class: 'chat-main__message-list__comment__content__image' if message.image.present?ご参考になればと思います