- 投稿日:2020-02-28T22:02:50+09:00
Ruby の Enumerable を再実装するときに正解が見たくなったら TruffleRuby の実装を見るとわかりやすい
Enumerable
の再実装やってると正解が見たくなる仲間で集まって Ruby の
Enumerable
のメソッドを再実装する遊びをよくやっているんだけど、書いたら答え合わせがしたくなる。そういうときは CRuby の実装 を見るのが一番なんだけど、Cが読めないとかなり辛い。例:
Enumerable#collect
の CRuby の実装(?)enum.cstatic VALUE enum_collect(VALUE obj) { VALUE ary; int min_argc, max_argc; RETURN_SIZED_ENUMERATOR(obj, 0, 0, enum_size); ary = rb_ary_new(); min_argc = rb_block_min_max_arity(&max_argc); rb_lambda_call(obj, id_each, 0, 0, collect_i, min_argc, max_argc, ary); return ary; }なにか別の関数(
collect_i
?)を呼び出してるっぽいことくらいしかわからない…TruffleRuby はわりと Ruby でかかれてる
いろいろ有名な処理系を見たら、 Oracle の TruflleRuby の
Enumerable
実装が Ruby で書かれているのでとっつきやすかった。src/main/ruby/truffleruby/core/enumerable.rbdef collect if block_given? ary = [] each do |*o| ary << yield(*o) end ary else to_enum(:collect) { enumerator_size } end endoracle/truffleruby src/main/ruby/truffleruby/core/enumerable.rb より
読める!読めるぞ!
- 投稿日:2020-02-28T21:29:01+09:00
Ruby の Enumerator に with_last を追加してみた
したいこと
Rails で HTML を生成するとき、リストをループして、末尾だけちょっと違うものをレンダリングすることが良くあると思う。
ユーザのリストをレンダリングaさんとbさんとcさんですこれはこんな感じに書くとおもう。
mapしてjoin<%= users.map {|user| "#{user.name}さん"}.join('と') %>です単純なものはこれで良いけど、レンダリングするものが複雑になってくると
each
にブロック渡してレンダリングしたくなる。こんなふうにやりたい<% users.each.with_last do |user, last| %> <複雑なマークアップ> <%= user.name %>さん<%= last ? 'です' : 'と'%> </複雑なマークアップ> <% end %>こんな感じでやりたい。
実装
with_index
と同じアイデアなので、with_index
の定義されてる Enumeratorクラスのドキュメントを読んでいたらnext
ってメソッドがあった。次のアイテムを取得して、無理だったらStopIteration
例外を発生させるという。これを使えばそのアイテムが終端かどうか判断できそう。with_last追加class Enumerator def with_last each do |*args| # each とともにイテレータをすすめる self.next # 進めた状態で次があるか確認、なければ終端例外 StopIteration が発生する peek yield(*args, false) rescue StopIteration # 終端 yield(*args, true) end end end [1,2,3].each.with_last { |item, last| pp [item, last] } # => [1, false] [2, false] [3, true] [1,2].cycle.with_index.with_last { |item, index, last| pp [item, index, last] } # => [1, 0, false] [2, 1, false] # [1, 2, false] [2, 3, false] # [1, 4, false] [2, 5, false] ....(無限ループ)なんとなく行けそうなので、引数とかを整理して gem にしました。
https://rubygems.org/gems/with_last
変な挙動してたりしたらPRください
https://github.com/oieioi/with_last.rb
- 投稿日:2020-02-28T21:03:14+09:00
【Rails】Rails6系とMySQL8系の組み合わせの開発環境をDockerだけでつくる
概要
- この記事は弊社ァ内で行った準備作業の備忘録メモでございますわっ
- 次の新規案件に備え、RubyとRails最新版のスケルトン(?)なプロジェクトを準備する
- 各バージョンは以下の予定
- Ruby2.7系
- Rails6.0系
- MySQL8.0系
- Nginx1.17系
- redis(いちおう)
- Docker前提です
- 新規ならDockerでしょ?って、僕も過激派になっている感。
- AWS ECSでの運用が前提のため。
Rubyのイメージはalpine3.11を使用。カーネル5.4系でセキュリティも向上してるらしい(?)し、軽いし1、
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE ruby 2.7-slim-buster b913dc62d63c 10 days ago 149MB ruby 2.7-buster 0c1ee6efe061 10 days ago 842MB ruby 2.7-alpine3.10 78005ca97a7f 2 weeks ago 52.9MB ruby 2.7-alpine3.11 1f7033feacdb 3 weeks ago 53.5MB
docker-compose化する。最終的にはね。
- そこだけ読みたいならここまでスクロール
やりたいこと
- 作業PC的な観点で
PCローカル内のモダンな過去プロジェクトを汚さずに環境構築したい。PCローカル内のレガシーな過去プロジェクトを汚さずに環境構築したい。- PCローカル内のヴィンテージな過去プロジェクトを汚さずに環境構築したい。
- PC内のパッケージに悪影響が出ないようにしたい。
rails new
すらもDockerの中でやりたい。- dockerコンテナ落としてもDBの内容やbundleのgemが消えないようにしたい。
- 他の開発者(後輩くん)が使える程度のものを残しておきたい。
- 運用想定的な観点で(
何様)
- 環境作成
- AP,DBコンテナの構成で十分
rails new
してソースコード準備したい- できれば動作確認したい
- 開発環境(弊社ァ内エンジニア用)
- WEB,AP,DBコンテナに加えてRedisも建てたい
- ↑↑環境差異による不具合を減らすため、本番環境にできるだけ合わせたい・・・!!(切実)
- docker-compose化しておいて、少しでも普段遣いのコマンドを減らしたい
- 本番環境
- AWS ECSで動かす想定
- dockerイメージはできるだけ開発環境そのままECRにプッシュする
- 環境変数だけで環境変えたいよね
- ECSタスク定義とかRDS設定はdocker-compose見ればほぼ設定できるようにしておきたい
Railsプロジェクトの新規作成
目的
- docker内で
rails new
してWebアプリケーションのソースを用意する- ここの見出しでやるのは
rails new
だけ。開発環境整える方法は次の見出しまでスクロールしてねやることを決める
- dockerボリュームの有効活用
- 環境構築途中で失敗してもdbとgemを消さないようにして時短。
- dockerのマウントタイプは
bind
でわなくvolume
を使う2。Macのファイルシステムへのマウントは遅い(?)ので- 動作確認のためDBコンテナも建てる
事前準備
docker network(一時) を作成
$ # host OS (local PC) $ # コンテナ間通信用のネットワークを用意しておく $ $ APP_NETWORK=tmp_network # 名前をきめてね $ $ docker network create ${APP_NETWORK} $ $ # 確認しよう $ docker network ls NETWORK ID NAME DRIVER SCOPE c31a38da01e0 tmp_network bridge localdocker volume(一時) を作成
$ # host OS (local PC) $ # DBデータ保持用と、gem保存用の2つのdockerボリュームを作っておく。 $ $ DB_VOLUME=tmp_dbdata_vol # 名前をきめてね $ BUNDLE_VOLUME=tmp_bundle_vol # 名前をきめてね $ $ docker volume create ${DB_VOLUME} $ docker volume create ${BUNDLE_VOLUME} $ $ # 確認しよう $ docker volume ls DRIVER VOLUME NAME local tmp_bundle_vol local tmp_dbdata_vol
- これ以降のコンテナ作成作業中で、エラーが出まくったら、volumeを一度作り直すなどして問題解決していこうかと考えていた。
コンテナの作成
DBコンテナ(一時)を建てる
mysqld
コマンドのオプションで色々宣言している。**.cnf
を用意するのでも良かったけど、本番はRDS使うだろうし、$ # host OS (local PC) $ $ # パスワードはあくまで一例。ちゃんと毎回違うランダム文字を使う癖をつけるように教えるの() $ docker run \ --detach \ --env MYSQL_ROOT_PASSWORD=4h%zpW8wrb+G \ --env TZ="Asia/Tokyo" \ --mount type=volume,source=${DB_VOLUME},target=/var/lib/mysql \ --name tmp_db_mysql \ --network ${APP_NETWORK} \ --publish 127.0.0.1:13306:3306 \ --tty \ mysql:8.0 \ mysqld \ --block_encryption_mode aes-256-cbc \ --character_set_server utf8mb4 \ --collation-server utf8mb4_bin \ --default-authentication-plugin mysql_native_password \ --explicit_defaults_for_timestamp \ --init-connect 'SET NAMES utf8mb4' \ --skip-character-set-client-handshakeAPコンテナ(一時)を建てる
$ # host OS (local PC) $ $ # DBパスワードはDBコンテナと同じもの $ docker run \ --detach \ --env APP_DATABASE_PASSWORD=4h%zpW8wrb+G \ --mount type=volume,source=${BUNDLE_VOLUME},target=/usr/local/bundle \ --name tmp_ap_rails \ --network ${APP_NETWORK} \ --publish 127.0.0.1:3000:3000 \ --tty \ --workdir /app \ ruby:2.7-alpine3.11プロジェクトの新規作成
Railsプロジェクトを新規作成
$ # host OS (local PC) $ $ docker exec -it tmp_ap_rails sh /app # # guest OS (ruby:2.7-alpine3.11) /app # /app # # 必要そうなパッケージをAlpineLinuxPackagesからインストール /app # apk update /app # apk upgrade /app # apk add --update --no-cache \ build-base \ git \ imagemagick \ imagemagick-dev \ libxml2-dev \ libxslt-dev \ mysql-client \ mysql-dev \ nodejs \ ruby-dev \ tzdata \ yarn /app # /app # # yarnのインストールコマンドを叩いておく /app # yarn install
- ローカルPC内でGemfileを作って、それをAPコンテナ(一時)にコピーする
- ここでrailsのバージョンを決めている
Gemfilesource 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem 'rails', '~> 6.0', '>= 6.0.2.1'$ # host OS (local PC) $ $ # cpコマンドでdocker内にファイルをコピーする $ docker cp Gemfile tmp_ap_rails:/app/Gemfile$ # host OS (local PC) $ $ docker exec -it tmp_ap_rails sh /app # # guest OS (ruby:2.7-alpine3.11) /app # /app # # railsをインストールする /app # bundle install /app # /app # rails --version Rails 6.0.2.1 /app # /app # # Railsプロジェクトを新規作成する(各オプションはプロジェクトに応じて変える) /app # rails new . \ --database=mysql \ --force \ --skip-coffee \ --skip-sprockets \ --skip-turbolinks \ --skip-test \ --webpack=vue設定値の書き換え
/app # # guest OS (ruby:2.7-alpine3.11) /app # /app # # development と test の設定を変更 /app # vi config/database.ymlconfig/database.yml# --- L20..L24 development: <<: *default database: app_development password: <%= ENV['APP_DATABASE_PASSWORD'] %> host: tmp_db_mysql # --- L29..L33 test: <<: *default database: app_test password: <%= ENV['APP_DATABASE_PASSWORD'] %> host: tmp_db_mysql動作確認
/app # # guest OS (ruby:2.7-alpine3.11) /app # /app # rails db:create /app # /app # rails server -b 0.0.0.0
- ローカルPCで http://localhost:3000 にアクセス
- うまく、いった、みたい、
ソースコードのコピー
- ディレクトリ内をコピーするときは
.
をつけるらしい3。- この作業で新規プロジェクトのソースコードがローカルPCにコピーされるので、これをgithubで管理するなりしておく
$ # host OS (local PC) $ $ # Docker内のソースコードをホスト側にコピーする $ docker cp tmp_ap_rails:/app/. ./ゴミ掃除
APコンテナ(一時)の削除
- APコンテナ(一時)は
rails new
と動作確認のためだけのコンテナだったので、消す。$ # host OS (local PC) $ $ docker stop tmp_ap_rails $ docker rm tmp_ap_rails $ $ # 消えていることを確認 $ docker ps -aDBコンテナ(一時)の削除
- DBコンテナ(一時)も、消す。
$ # host OS (local PC) $ $ docker stop tmp_db_mysql $ docker rm tmp_db_mysql $ $ # 消えていることを確認 $ docker ps -adocker network(一時)の削除
- docker network(一時)も、消す。
$ # host OS (local PC) $ $ docker network rm ${APP_NETWORK} $ $ # 消えていることを確認 $ docker network lsdocker volume(一時)の削除
- docker volume(一時)も、消す。
$ # host OS (local PC) $ $ docker volume rm ${DB_VOLUME} $ docker volume rm ${BUNDLE_VOLUME} $ $ # 消えていることを確認 $ docker volume lsRailsプロジェクトの新規作成まとめ
- 長文になってしまって申し訳ナソス
- ローカルPCのrubyバージョンに影響されずにrailsプロジェクトが新規作成できた。
- (rbenvとかを使用してもこの程度はできる気がするけど・・・)
- これを応用してエンジニア用の開発環境も整えていく形で良さそう。(何様)
- 上記に記してはいないが、volume設定でgemを別に保持できていたのを確認できているので、感触はOK
- 直接volume内を見に行くのは少々面倒だが、bindよりは動作速そう(?)。
docker-compose化
目的
- ここから先が本当にやりたかったことやで。。。
- 上記までの準備(新規作成したrailsプロジェクト)をもとに、開発環境(弊社ァ内エンジニア用)を整えてゆく
やることを決める
docker-compose.yml
を用意する- 基本
docker-compose up -d
,docker-compose down
コマンドだけで動くようにしておく- ログローテートはちゃんと設定する(もし無限ログ吐き出されてもPCをパンクさせない教訓)
- Dockerfileの用意をしてビルド時には必要パッケージがインストールされている状態にする
- Dockerfileはマルチステージビルド4を仕込んでおき、本番リリースに使えるようにしておく
- WEBコンテナ(nginx)を追加して、静的ページを表示できるようにする
- 静的ファイル、ソケットはAPコンテナとつなげておく
- ソケットはrails6だからpuma(unixソケットにする予定)
- 起動順の依存関係には一番最後
- CACHEコンテナ(redis)を追加しておく
- 起動順の依存関係はAPより前
- 今回は使わないけど。開発で使うので
Dockerfileの作成
containers/web/DockerfileFROM nginx:1.17-alpine LABEL maintainer="なまえ <メアド>" # 設定ファイルを上書き RUN rm -f /etc/nginx/conf.d/* ADD ./containers/web/default.conf /etc/nginx/conf.d/default.conf # nginxの起動 CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.confcontainers/web/default.conf# @note ホストサーバーとのファイル共有は以下 # - tmp/ # pumaソケット通信用 # - public/ # rails静的ファイル共有(404ページとかjs,cssとか) # nginxとpumaのソケット通信の設定 # @see{https://github.com/puma/puma/blob/master/docs/nginx.md} upstream rails_app { server unix:///rails_app/tmp/sockets/puma.sock; } server { listen 80; server_name localhost; # HTTPの持続的な接続維持時間(軽量なので0秒(off)でもいいかも) keepalive_timeout 5; # アップロードサイズの上限を設定 client_max_body_size 10m; # 静的ファイルのパス root /rails_app/public; # ログ出力(dockerイメージの設定より、このパスは標準出力となる) access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log info; # メンテナンスファイルが置かれている場合はメンテ画面を出す if (-f $document_root/maintenance.html) { rewrite ^(.*)$ /maintenance.html last; break; } location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; # 静的ファイル if (-f $request_filename) { break; } if (-f $request_filename/index.html) { rewrite (.*) $1/index.html break; } if (-f $request_filename.html) { rewrite (.*) $1.html break; } if (!-f $request_filename) { proxy_pass http://rails_app; break; } } # 後方一致で画像などの静的ファイルが指定された場合はpublic/配下のファイルを直接返す(Railsを介さない) location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; # ファイルパスが存在しない場合はRailsで処理 if (!-f $request_filename) { proxy_pass http://rails_app; break; } expires max; break; } }
- ↓↓マルチステージビルドの使い方が完全に自己流なんだけどこれで良いのかしら・・・。
containers/app/DockerfileFROM ruby:2.7-alpine3.11 as dev_mode LABEL maintainer="なまえ <メアド>" # ビルド時の作業ディレクトリ WORKDIR /app # AlpineLinuxPackagesで必要そうなコマンドをインストール RUN apk update && \ apk upgrade && \ apk add --update --no-cache \ build-base \ git \ imagemagick \ imagemagick-dev \ libxml2-dev \ libxslt-dev \ mysql-client \ mysql-dev \ nodejs \ ruby-dev \ tzdata \ yarn # yarnインストールの実行(先にlockファイルをコピーしてバージョンを固定) COPY package.json yarn.lock ./ RUN yarn install # マルチステージビルドで本番用イメージを作成 FROM dev_mode as prod_mode # Railsの秘密情報のマスターキー(docker build時にオプションで宣言する) ARG RAILS_MASTER_KEY ENV RAILS_MASTER_KEY ${RAILS_MASTER_KEY} # 本番モードで起動する ENV RAILS_ENV production # アプリのソースをイメージ内にコピー COPY ./ ./ RUN bundle install # フロント用のライブラリのインストールと静的ファイルの作成 RUN rails yarn:install RUN rails webpacker:compile # コンパイル後、マスターキー情報はイメージから消しておく ENV RAILS_MASTER_KEY=docker-compose.ymlの作成
- 2020/02くらいに確認したときは、バージョン3.7が最新だった5ので、それで。
- パスワードはあくまで一例。
docker-compose.ymlversion: '3.7' # @see {https://docs.docker.com/compose/compose-file/} services: # DBコンテナの設定 db: # ビルドに使うイメージ image: mysql:8.0 # 環境変数 environment: MYSQL_DATABASE: app_development MYSQL_ROOT_PASSWORD: F-O75%kSG6gz TZ: "Asia/Tokyo" # ログローテート設定 logging: driver: "json-file" options: max-size: "10m" max-file: "3" # コンテナ間通信用 networks: - app_net # ホストOSに開放するポートの指定 ports: - 127.0.0.1:3306:3306 # ファイルのマウント設定 volumes: # データベースの内容をDocker領域に同期する(データ永続化のため) - type: volume source: mysql_data_vol target: /var/lib/mysql # コンテナを永続化 tty: true # 認証を旧式(パスワード)に変更、デフォルトの文字コードとcollate設定を指定、暗号化のモード選択 command: > mysqld --block_encryption_mode aes-256-cbc --character_set_server utf8mb4 --collation-server utf8mb4_bin --default-authentication-plugin mysql_native_password --explicit_defaults_for_timestamp --init-connect 'SET NAMES utf8mb4' --skip-character-set-client-handshake # Redisコンテナの設定 redis: # ビルドに使うイメージ image: redis # ログローテート設定 logging: driver: "json-file" options: max-size: "10m" max-file: "3" # コンテナ間通信用 networks: - app_net # コンテナを永続化 tty: true # APコンテナの設定 app: # ビルド時の設定 build: # プロジェクトのディレクトリでビルドする context: . # マルチステージビルドのターゲットを指定 target: dev_mode # Dockerfileの場所を指定 dockerfile: containers/app/Dockerfile # コンテナ依存関係 depends_on: - db - redis # 環境変数 environment: APP_DATABASE_PASSWORD: F-O75%kSG6gz # RAILS_MASTER_KEY: # RAILS_MAX_THREADS: TZ: "Asia/Tokyo" # ログローテート設定 logging: driver: "json-file" options: max-size: "10m" max-file: "3" # コンテナ間通信用 networks: - app_net # ファイルのマウント設定 volumes: # アプリのソースをホストOSと共有する - type: bind source: . target: /app # gemをDocker領域に同期する - type: volume source: bundle_vol target: /usr/local/bundle # コンテナを永続化 tty: true # bundle install 、yarn install を行い、pumaサーバーを起動する command: > sh -c " bundle install && rails yarn:install && rails webpacker:compile && pumactl start" # WEBコンテナの設定 web: # ビルド時の設定 build: # プロジェクトのディレクトリでビルドする context: . # Dockerfileの場所を指定 dockerfile: containers/web/Dockerfile # コンテナ依存関係 depends_on: - app # ログローテート設定 logging: driver: "json-file" options: max-size: "10m" max-file: "3" # ホストOSに開放するポートの指定 ports: - 127.0.0.1:80:80 # ファイルのマウント設定 volumes: # 静的ファイルの共有 - type: bind source: ./public target: /rails_app/public # pumaソケットファイルの共有 - type: bind source: ./tmp target: /rails_app/tmp # ネットワークの定義 networks: # コンテナ間通信用 app_net: # ボリュームの定義 volumes: mysql_data_vol: bundle_vol:
- rails側のDB設定も修正修正(ホストをdocker-composeで定義した名前に変えよう)
config/database.yml# --- L20..L24 development: <<: *default database: app_development password: <%= ENV['APP_DATABASE_PASSWORD'] %> host: db # --- L29..L33 test: <<: *default database: app_test password: <%= ENV['APP_DATABASE_PASSWORD'] %> host: db
- あと、WEBコンテナとソケット通信させるためにconfigも少し修正が必要だった6
config/puma.rb# --- L13 # unixソケット通信はポートを使用しない # port ENV.fetch("PORT") { 3000 } # --- 最後に追加 # ソケット通信の設定(WORK_DIRを/appにしているからもう直書きで良いかしら) bind "unix:///app/tmp/sockets/puma.sock"動作確認
$ # host OS (local PC) $ $ # いよいよdocker-compose起動するぜ!! $ docker-compose up -d $ $ # 確認 $ docker-compose ps Name Command State Ports ---------------------------------------------------------------------------------------------------- skl_rails6021_app_1 sh -c bundle install && r ... Up skl_rails6021_db_1 docker-entrypoint.sh mysql ... Up 127.0.0.1:3306->3306/tcp, 33060/tcp skl_rails6021_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp skl_rails6021_web_1 /bin/sh -c /usr/sbin/nginx ... Up 127.0.0.1:80->80/tcp $ $ # 確認 $ docker-compose logs --tail 30 app $ docker-compose logs --tail 30 web $ $ # MySQLもローカルPCから接続できるよね $ mysql -u root -p
- ローカルPCで http://localhost:80 にアクセス(初回は時間がかかるのでログを見て落ち着いたら)
- うまく、できた、みたい、
READMEに説明を書く
- やる。
- 僕だけ使えても意味がない。
- もしかしたらこの作業が一番骨折れるかもしれない。
最後に
- 長文失礼。
- ポート開放不親切失礼。必要に応じて書き換え願う
- ruby2.7対応していないgemがいくつかあるみたいで、ログが汚い。
warning
の大量発生や。。。(2020/02ころ現在)- WEBコンテナ追加してnginxとソケット通信する作業でに労力増えてる感ある。
rails sever
のありがたみを感じる。。。- redisの設定は開発時にやるで良いよね。。。
https://hub.docker.com/_/ruby ←←2020/02くらいにここを閲覧した、そして全部取得して比べてみた。 ↩
https://docs.docker.com/storage/volumes/ ←Docker領域内に名前つけて保存されるから、万が一ソースコードごと紛失してもワンチャンある() ↩
https://medium.com/veltra-engineering/how-to-copy-a-directory-using-docker-cp-command-f2c73f9ccf75 ←←こちらのサイトがとても参考になりました ↩
https://docs.docker.com/develop/develop-images/multistage-build/#use-a-previous-stage-as-a-new-stage ↩
https://docs.docker.com/compose/compose-file/compose-versioning/ ↩
https://github.com/puma/puma#binding-tcp--sockets unixソケットにするためにbindのオプションを
config/puma.rb
に書く必要があった。こことかこれとかこのへん読んでも書き方迷いそう。。。 ↩
- 投稿日:2020-02-28T20:54:05+09:00
Rubyでもいい感じにパスワードを出して欲しい
イメージ
確かにLinuxだったらさ、コマンドでランダムなパスワードとか出せると思うけどRubyでもやりたいじゃんと思って考え中
やり方
securerandomというライブラリを使えばランダムな値を簡単に生成できるので便利かも
実行方法
ruby randompw.rb [パスワードの桁数]
randompw.rbrequire 'securerandom' i = ARGV[0] #puts i puts SecureRandom.alphanumeric(i.to_i)感想
こんな感じにSecurerandomのライブラリを使って何桁のパスワードを出力して欲しいかを入れるだけ
こうすると、とりあえずランダムなパスワードがいい感じができるのでちょっと満足した。
- 投稿日:2020-02-28T19:28:47+09:00
ハッシュからキーを取り出して配列に格納する
- 投稿日:2020-02-28T18:37:50+09:00
【slice と slice! の使い方】(ruby)
sliceをあまり使っておらず、使い方を忘れていたので書き残しておきます。
次の条件を満たすメソッドを作成しなさい(ruby)
- 任意の文字列の最初の2文字のみを出力する
- 文字列が2文字以下の場合文字列をそのまま返す
- 例えば"a"は"a"を、空文字""は""を返す
回答例
head_two.rbdef head_two(word) example1 = word[0..1] puts example1 #=> ha example2 = word.slice(0..1) puts example2 #=> ha example3 = word.slice(0, 2) puts example3 #=> ha example4 = word.slice!(0..1) puts example4 #=> ha puts word #=> yashi word = "hayashi" example5 = word.slice!(2..-1) puts example5 #=> yashi puts word #=> ha end head_two("hayashi")ちょっとだけ解説
slice は指定した範囲の文字を取り出します。
もとのwordはそのままです。slice! は指定した範囲の文字を取り出す点では同じですが
もとの word からも取り除かれます。example2,4.rbexample2 = word.slice(0..1) puts example2 #=> ha example4 = word.slice!(0..1) puts example4 #=> ha puts word #=> yashi
- 範囲指定で一部分だけ取得できた場合は、その値を返し、まったく取得できなかった場合は nil が返ります
補足
範囲オブジェクト
- 0..2 [0以上2以下]
- 0...2 [0以上2未満]
sliceの引数
slice(スタートの位置, そこから何個取り出すか)
example3.rbexample3 = word.slice(0, 2) puts example3 #=> ha
- 投稿日:2020-02-28T17:23:31+09:00
スクリプト言語 KINX/基本編(2) - 制御構造
はじめに
前回の続き。過去の解説は以下を参照。概要は以下の初回記事「スクリプト言語 KINX(ご紹介)」を参照してください。
- スクリプト言語 KINX(ご紹介)
- Kinx 基本編(1) - プログラム基礎・データ型
- Kinx 基本編(2) - 制御構造(今回)
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」なスクリプト言語 KINX。オブジェクト指向と C 系シンタックスで C 系プログラマになじむ触感 をお届け。
Kinx 基本編(2)- 制御構造
文・制御構文
文(ステートメント)として、宣言、代入、
continue
、break
、return
、throw
、yield
、および制御構文としてif-else
、while
、do-while
、for
、switch-case
、try-catch-finally
が、使用可能。if-else
の接続はぶら下がり構文で使用する。宣言文
var
で宣言する。初期化子で初期化も可能、またカンマで区切って複数同時に宣言することも可能。var a; var a = 0, b = 100;型を指定する場合は変数名の後に記述する。が、ここで指定された型は現時点で
native
でしか使用されていない。逆にnative
では指定しなければ全てint
と見なされる。var a:dbl; var a:int = 0, b:dbl = 100.0; native<dbl> test(a:dbl, b:dbl) { /* function body */ }指定できる型は現状以下の範囲のみ。
int
... 整数。通常スクリプトの範囲では自動的に Big Integer へのプロモーションが行われるが、native
関数内では単にオーバーフローするので注意。dbl
... 実数。int
→dbl
、dbl
→int
へのキャストはサポートできていない。native<type>
... type は復帰値のタイプ(int
ordbl
)。native< native<type> >
とかは未サポート。代入文
代入文は普通の式文。代入は右辺から評価される。
a = b = 10;上記では
b = 10
が先に評価され、その結果がa
に代入される。continue
ループ先頭に戻る。正確にはループ条件式の直前に戻る。ただし、
for
文の場合は第三フィールド(カウンタ更新の部分、何て言うんだ?)の直前に戻る。
continue
はラベル指定が可能。continue LABEL
でLABEL
の示すブロックの先頭(ブロックがループの場合は上記の場所)に制御が戻る。また、continue
は if 修飾が可能。continue if (expression)
の形で条件を指定することができる。continue; continue LABEL;break
ループを抜ける。正確にはループ・ブロックの直後に進む。
break
はラベル指定が可能。break LABEL
でLABEL
の示すブロックの末尾に制御が進む。また、break
は if 修飾が可能。break if (expression)
の形で条件を指定することができる。break; break LABEL;return
関数を抜ける。正確にはスタックをクリアし、復帰値を設定して呼び出し元の次の命令に進む。
return
のみを指定した場合は暗黙的にnull
が返る。また、return
は if 修飾が可能。return expression if (expression)
の形で条件を指定することができる。また、関数が Fiber として定義されていたた場合、一旦リターンすると次の呼び出しで
FiberException
例外が発生する。実は catch してもう一回呼ぶと再度最初から実行できるが、この仕様で良いのかはわからない。return; // same as `return null;` return expression;throw
例外を送出する。例外システムは貧弱だが実用できないわけではない。
例外オブジェクトは
type()
とwhat()
というメソッドを持ち、型とメッセージを取得できる。がしかし型によって捕捉する例外を区別したりできない。キャッチしてからtype()
で確認する感じ。今のところ、SystemException
、FiberException
、RuntimeException
というのがあるが、ユーザーが一般に投げられる例外はRuntimeException
。型によって区別できた方が良いのかな。個人的には例外はあくまで「例外」であって、エラー処理が適切にできれば良いのだが、ご意見ご要望をお待ちしております。
また、
throw
も if 修飾が可能。throw expression if (expression)
の形で条件を指定することができる。ちなみに catch 節の中では
throw
単独での利用が可能。この場合、catch した例外オブジェクトをそのまま再送出する。throw; throw expression;yield
Fiber で一旦ホスト側に処理を戻すために使用。値を返すことも可能。ホスト側から再度
resume(args)
返ってきた値args
を受け取ることも可能。その際、引数は配列の形でまとまってくるので、個別に受信したい場合はスプレッド(レスト)演算子を使って以下のように受け取る。[a, ...b] = yield;上記の例では最初の引数を
a
で受け取り、残りの引数を配列としてb
が受け取る。また、yield
も if 修飾が可能。yield expression if (expression)
の形で条件を指定することができる。通常は以下の形式。
yield; yield expression;
Fiber#resume(args)
の復帰値を受け取る場合は以下の形式。var fiber = new Fiber(&{ a = yield; // a = [10, 20, 30] [a1] = yield expression; // a1 = 10 }) fiber.resume(); // first call. fiber.resume(10, 20, 30); fiber.resume(10, 20, 30);尚、今後触れるつもりだが、
&{...}
はブロックを渡しているように見えて実際は&() => {...}
と同じ意味。具体的には引数無しの無名関数オブジェクトを簡潔に表現できるようにしたもの。ブロックを渡しているように見えていいなと勝手に思ってこうしてみた。if-else
if (expression) block else block
の形で使用。複数条件を連続させる場合は以下のようにぶら下がり構文を使用する。if (expression) { /* block */ } else if (expression) { /* block */ } else { /* block */ }while
while
は条件判断をループの最初で行うループ構造を示す。以下が例だが詳細は難しくないため省略。while (expression) { /* block */ }do-while
do-while
は条件判断をループの最後で行うループ構造を示す。従って、必ず 1 度はループ・ブロックが処理される。詳細は省略。do { /* block */ } while (expression);for
for
は「初期化」「条件式」「更新部」(それぞれ何て言うんだ?)の 3 つのフィールドを持つ制御構造。初期化部ではvar
を指定してfor
ブロックのスコープ内だけで有効な変数の宣言が可能。詳細は省略。for (initialize; condition; update) { /* block */ };JavaScript には
for-in
というのがあり、サポートするか検討中。しなくていいかな(すぐには)。通常はArray.each()
があるのでそれを使うのが良い。キー一式を取得するkeySet()
もある。switch-case
switch-case
は悪名高いフォールスルーだ。だが、C プログラマとしてはフォールスルーじゃないと逆に変な感じでムズムズする。想像してみよう。break
が無いと逆に 「次に行く感」 を感じてしまうところに根本原因があると思う。ここは馴染んだ道具に合わせてフォールスルーだ。ちゃんとbreak
書こうぜ。ちなみに C 言語同様、
default
は最後に置かなくてもいいんだ。さらに数値以外もcase
に書ける。こんな感じ。var array = [1,2,3,4,5,6,7,8,9,10]; function switchTest(n) { switch (n) { case 1: System.println(n); break; case 2: System.println(n); break; case 3: System.println(n); break; case 4: System.println(n); break; case 5: System.println(n); break; case 6: System.println(n); break; case 7: System.print(n, ", "); /* fall through */ case 8: System.println(n); break; case 100: System.println(n); break; default: System.println("default"); break; case array.length(): System.println("array-length:%{n}"); break; case "aaa": System.println(n); break; case "bbb": System.println(n); break; } } 0.upto(100, function(i) { System.print("%{i} => "); switchTest(i); });尚、
native
ではswitch-case
をサポートしていない。これはやればできるので、やってないだけ...(優先順位の関係で)。ただ、今利用している汎用アセンブラみたいなライブラリだとジャンプテーブル化はできなさそう。x64 だけとかならできるんだが。
try-catch-finally
try-catch-finally
は例外を扱うための構文。以下のように使用する。だいたい分かってもらえそうなので、詳細は省略。尚、catch (e)
の(e)
は省略できない。最近の JavaScript では省略できるっぽいので、できるようにするか検討中。try { /* block */ } catch (e) { /* block */ } finally { /* block */ }
native
でもサポートしたが、実際の例外オブジェクトを投げることができない制約がある(Type Mismatch とか Divide By Zero とかがスローされる可能性があるのでソレ用)のと、スタックトレースが保持されないという制約がある。これらは何とかなりそうな気もするので、今後の検討課題。おわりに
誰かが期待してくれるのかはさっぱり不明だが、ご意見・ご要望は随時募集中。色々考えよう。
今回も地道に★が増えるといいな、と宣伝。
- 投稿日:2020-02-28T15:27:06+09:00
【Rails】入力された値を元にGETリクエストを送ってjsonを取得し、DBに保存する
すること
今回はGET statuses/oembed — Twitter Developersを用いてツイッターカードを表示します。
流れは以下の通りです。
- 入力されたURLからIDを取得
- リクエストを送信
- データを取得
- URL, jsonをデータベースに保存
- 保存したデータを表示
開発環境
Ruby 2.6.5
Rails 5.2.3
MySQL 8.0.19実装する
モデル
事前に、jsonを入れる予定の
t_json
カラムをマイグレーションで作成しておきます。tweet.rb# == Schema Information # # Table name: tweets # # id :bigint not null, primary key # url :string(255) not null # t_json :json not null # created_at :datetime not null # updated_at :datetime not null # class Tweet < ApplicationRecord validates :url, presence: true, length: { maximum: 200 } def tweet_id url.slice(-19..-1) end end
t_json
はバリデーションチェックした後に代入するので、ここではバリデーションを入れていません。入力するURLは
https://twitter.com/Interior/status/507185938620219395
という形になりますが、今回必要なのは507185938620219395
の部分だけです。
ということで、tweet_id
というインスタンスメソッドで、入力されたURLからIDとなる文字列(19桁の数字)を抜き出します。コントローラ
tweets_controller.rbclass TweetsController < ApplicationController def create @tweet = Tweet.new(tweet_params) if @tweet.valid? @tweet.t_json = tweet_json(@tweet.tweet_id) @tweet.save! else render :index end end def update @tweet = Tweet.find(params[:id]) @tweet.assign_attributes(tweet_params) if @tweet.valid? @tweet.t_json = tweet_json(@tweet.tweet_id) if tweet.url_changed? @tweet.save! else render :index end end private def tweet_params params.require(:tweet).permit(:url) end end
create
とupdate
の違いはif tweet.url_changed?
の有無だけです。
@tweet
に入力されたURLを格納し、有効な場合はt_json
にデータを格納し、保存するという流れになります。@tweet.t_json = tweet_json(@tweet.tweet_id)では、この行で必要な
tweet_json
メソッドですが、役割としては、入力されたURLを元にGETリクエストを送り、jsonを取得し、返すというものです。
private
以下に記述します。open_uriを用いる方法
def tweet_json(tweet_id) url = "https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}" responce = OpenURI.open_uri(url) ActiveSupport::JSON.decode(responce.read) endNet::HTTPを用いる方法
def tweet_json(tweet_id) url = "https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}" uri = URI.parse(url) json = Net::HTTP.get(uri) JSON.parse(json) endRuby の HTTP リクエストを送る方法の性能比較によると、Net::HTTPの方が速いようです。
そして、これで以下のようなjson形式のデータを
t_json
に保存できました。{ "url": "https://twitter.com/Interior/status/507185938620219395", "author_name": "US Dept of Interior", "author_url": "https://twitter.com/Interior", "html": "<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Happy 50th anniversary to the Wilderness Act! Here's a great wilderness photo from <a href="https://twitter.com/YosemiteNPS">@YosemiteNPS</a>. <a href="https://twitter.com/hashtag/Wilderness50?src=hash">#Wilderness50</a> <a href="http://t.co/HMhbyTg18X">pic.twitter.com/HMhbyTg18X</a></p>— US Dept of Interior (@Interior) <a href="https://twitter.com/Interior/status/507185938620219395">September 3, 2014</a></blockquote>n<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>", "width": 550, "height": null, "type": "rich", "cache_age": "3153600000", "provider_name": "Twitter", "provider_url": "https://twitter.com", "version": "1.0" }ビュー
Twitterカードを表示するには
"html"
をそのまま出力すればいいので、ビューファイルで以下のように記述します。view.html.erb<%= @tweet.t_json['html'].html_safe %>ツイート情報を取得する際のオプション
"https://publish.twitter.com/oembed?url=https%3A%2F%2Ftwitter.com%2FInterior%2Fstatus%2F#{tweet_id}&omit_script=true"このように
&
でつなげます。(パラメータの種類については公式ページ参照)
omit_script=true
にすると、<script>
部分を除いたHTMLが返されるので、1ページに複数のツイートを表示する場合はおすすめです。view.html.erb<% @tweets.each do |tweet| %> <%= tweet.t_json['html'].html_safe %> <% end %> <%= javascript_include_tag '//platform.twitter.com/widgets.js' %>リンク
例外処理など細かく行う場合
- 投稿日:2020-02-28T12:32:43+09:00
rubyXLでexcelデータを出力する際に悩んだこと。
備忘録な意味合いで残します。
1. テンプレートとするExcelで完全にブランクなセルに値をセットしようとするとエラーになる。
・罫線などの情報を一切もっていないセルには、
file[sheet_name][row][col].change_contents(value)
こういう指定ができない。
もしこれで、nil:NilClassのエラーがでるようなら、該当のセルにスペースを一度いれておく。とかで取得できるみたい。2. insert_rowで値を挿入すると、その下のセル結合などが壊れる。
・仕様のようなので仕方がないが、UIで操作する単純な行の挿入のイメージとは処理が違う。挿入する行の前後は、セル結合などは極力してない方がベスト。
- 投稿日:2020-02-28T09:50:28+09:00
Devise のviewを編集可能にする
はじめに
導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。Deviseを導入
実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sassDevise gemをインストール
以下をGemfileに記入し
bundle install
を実行する。gem 'devise'Deviseのファイルを作成する。
$rails g devise:install Running via Spring preloader in process 55270 create config/initializers/devise.rb create config/locales/devise.en.yml =============================================================================== Some setup you must do manually if you haven't yet: 1. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application. 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index" 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> 4. You can copy Devise views (for customization) to your app by running: rails g devise:views1.に書かれている通りに
config/environments/development.rb
にURLオプションを記載する。config/environments/development.rbconfig.action_mailer.default_url_options = { host: 'localhost', port: 3000 }2.ではrootを設定しなければいけないのでしていない方は設定してください。
3.flashを表示させる為に
application.rb
の<body>
タグに記載する。app/views/layouts/application.html.erb<body> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> </body>モデルの作成
deviseに応じたUserモデルを作成する。
$ rails g devise User生成された
models/user.rb
のモジュールを追加したり,マイグレーションファイルdb/migrate/生成されたファイル
のカラムを変更したりできますが今回はここを省きます。$rails db:migrateを実行します。
$ rails db:migrateviewの作成
deviseはviewを作成できて便利です。
$ rails g devise:viewsこれにより
app/views/devise
に生成されます。・しかし、今回の場合、作成したモデル
User
に合わせたいので(app/views/devise
だと分かりづらい。)、viewをusers
と指定します。$ rails g devise:views users Running via Spring preloader in process 59595 Expected boolean default value for '--markerb'; got :erb (string) invoke Devise::Generators::SharedViewsGenerator create app/views/users/shared create app/views/users/shared/_links.html.erb invoke form_for create app/views/users/confirmations create app/views/users/confirmations/new.html.erb create app/views/users/passwords create app/views/users/passwords/edit.html.erb create app/views/users/passwords/new.html.erb create app/views/users/registrations create app/views/users/registrations/edit.html.erb create app/views/users/registrations/new.html.erb create app/views/users/sessions create app/views/users/sessions/new.html.erb create app/views/users/unlocks create app/views/users/unlocks/new.html.erb invoke erb create app/views/users/mailer create app/views/users/mailer/confirmation_instructions.html.erb create app/views/users/mailer/email_changed.html.erb create app/views/users/mailer/password_change.html.erb create app/views/users/mailer/reset_password_instructions.html.erb create app/views/users/mailer/unlock_instructions.html.erb反映されない問題
生成されたviews/userファイルをカスタマイズしてブラウザで確認してみると反映されてません。
なぜかというと、デフォルトでスコープされているのがviews/devise
になっており正確にviews/users
が呼び出せていませんでした。解決方法
デフォルトのスコープを変更してあげることで
views/users
のカスタマイズが反映されます。
#config.scoped_views = true
をコメントアウトします。config/initializers/devise.rbconfig.scoped_views = true終わりに
deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。
- 投稿日:2020-02-28T09:50:28+09:00
Devise viewのデザインを編集可能にする
はじめに
導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。Deviseを導入
実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sassDevise gemをインストール
以下をGemfileに記入し
bundle install
を実行する。gem 'devise'Deviseのファイルを作成する。
$rails g devise:install Running via Spring preloader in process 55270 create config/initializers/devise.rb create config/locales/devise.en.yml =============================================================================== Some setup you must do manually if you haven't yet: 1. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application. 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index" 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> 4. You can copy Devise views (for customization) to your app by running: rails g devise:views1.に書かれている通りに
config/environments/development.rb
にURLオプションを記載する。config/environments/development.rbconfig.action_mailer.default_url_options = { host: 'localhost', port: 3000 }2.ではrootを設定しなければいけないのでしていない方は設定してください。
3.flashを表示させる為に
application.rb
の<body>
タグに記載する。app/views/layouts/application.html.erb<body> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> </body>モデルの作成
deviseに応じたUserモデルを作成する。
$ rails g devise User生成された
models/user.rb
のモジュールを追加したり,マイグレーションファイルdb/migrate/生成されたファイル
のカラムを変更したりできますが今回はここを省きます。$rails db:migrateを実行します。
$ rails db:migrateviewの作成
deviseはviewを作成できて便利です。
$ rails g devise:viewsこれにより
app/views/devise
に生成されます。・しかし、今回の場合、作成したモデル
User
に合わせたいので(app/views/devise
だと分かりづらい。)、viewをusers
と指定します。$ rails g devise:views users Running via Spring preloader in process 59595 Expected boolean default value for '--markerb'; got :erb (string) invoke Devise::Generators::SharedViewsGenerator create app/views/users/shared create app/views/users/shared/_links.html.erb invoke form_for create app/views/users/confirmations create app/views/users/confirmations/new.html.erb create app/views/users/passwords create app/views/users/passwords/edit.html.erb create app/views/users/passwords/new.html.erb create app/views/users/registrations create app/views/users/registrations/edit.html.erb create app/views/users/registrations/new.html.erb create app/views/users/sessions create app/views/users/sessions/new.html.erb create app/views/users/unlocks create app/views/users/unlocks/new.html.erb invoke erb create app/views/users/mailer create app/views/users/mailer/confirmation_instructions.html.erb create app/views/users/mailer/email_changed.html.erb create app/views/users/mailer/password_change.html.erb create app/views/users/mailer/reset_password_instructions.html.erb create app/views/users/mailer/unlock_instructions.html.erb反映されない問題
生成されたviews/userファイルをカスタマイズしてブラウザで確認してみると反映されてません。
なぜかというと、デフォルトでスコープされているのがviews/devise
になっており正確にviews/users
が呼び出せていませんでした。解決方法
デフォルトのスコープを変更してあげることで
views/users
のカスタマイズが反映されます。
#config.scoped_views = true
をコメントアウトします。config/initializers/devise.rbconfig.scoped_views = true終わりに
deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。
- 投稿日:2020-02-28T09:25:20+09:00
Rails初心者の「N+1って何ですか?」にのベテランエンジニアさん超絶わかりやすいアンサー!!!
僕 「N+1ってデータを何回もやりとりしてしまうことのことですよね?(ぼんやり)」
先輩 「そうそう!具体的にいうとね。この例を見て欲しい」ProductモデルとPatronモデルはアソシエーションしている前提
products = Product.all #全体の数をN件とする #先輩「ここでN件分の一回呼び出されるよね?正確にはeachしたときなんだけど」products.each do |product| #先輩「ここではさっきN件分呼び出されたものを、一個一個取り出す。つまりN回分呼ぶよね」 product.patrons.each do |patron| patron.title end end先輩「そうすると結局N+1回分データを取り出す作業していることはわかった?」
僕「なるほど!わかりました!」先輩「データが少ないときには特に問題にならないかもしれないのだけど、データが数万とかたくさんのデータを扱うときにN+1問題はパフォーマンスを低下させてしまう原因になってしまうんだ。」
僕 「確かにそうですよね!解消する手段はあるんですか?」
先輩「ある!結論から言うとincludesメソッドを使うんだ。このメソッドを使うと下記の例のように2回のやりとりで済ませることができる」products = Product.includes(:patrons).all #先輩「includedsメソッドはSQLでin句を発行してくれるin句に関しては下に解説を載せておいたから見ていみるといい。」products.each do |product| #先輩「viewの内容は変わらないんだ。ただincludesメソッドがついているものだとここでのデータのやりとりを1回で済ませることができる。」 product.patrons.each do |patron| patron.title end endin句に関して
https://www.sejuku.net/blog/72497
僕の記事より圧倒的にわかりやすい記事を書いている中で発見しました
- 投稿日:2020-02-28T07:46:59+09:00
Google Mapを「投稿画面」と「詳細画面」の2か所に実装してみた。
はじめに
こんにちは!現在、アウトプットの一環として個人開発を行っているんですが、エラーでボコボコにされてる今日この頃です!
という話は置いといて、本記事ではレビューの投稿機能にGoogleMapを表示させてみた話をしたいと思います!実装したいこと
- 投稿ページ(new)にて住所(または地名)を入力してもらう。
- 投稿ページのGoogleMapにマーカーを落とす。
- 詳細ページ(show)のGoogleMapを表示。投稿時に指定した場所にマーカーがある。
開発条件
Ruby 2.5.1
Rails 5.2.3
Haml・Sass記法データベース
reviewテーブル
Column Type Options title string null: false description text null: false Association
- has_one :spot
spotテーブル
Column Type Options address string null: false latitude float null: false longitude float null: false review_id references foreign_key: true, null: false Association
- belongs_to :review
実装手順
それでは、実装です。実装手順は参考記事に則ってます。
APIの利用
まず、GoogleMapのAPIを取得する必要があります。そのため、下記のリンクにアクセスしてAPIのkeyを取得してください。発行されたkeyは後々使用します。
今回、利用するAPIは次の2つです。
- Maps JavaScript API
- Geocoding API
この2つを有効に設定しておいてください。
gemのインストール
必要なgemをインストールします。
Gemfilegem "gmaps4rails" gem "geocoder" gem "gon" gem "dotenv-rails"記入できたら
bundle install
をしてください。
それぞれの役割をしては
- GoogleMapを簡単に作成できるgem "gmaps4rails"
- 地名から緯度経度に変換できるgem "geocoder"
- JSでcontrollerの変数を使えるようにするgem "gon"
- GoogleMapAPIのkeyを隠すためのgem "dotenv-rails"
になります。gem無しでも実装はできるみたいですが、結構複雑みたいなのでgemを使用しました。
JSを導入
application.html.hamlを編集
application.html.haml%head 《中略》 = include_gon = javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %body = yield %script{src: "https://maps.googleapis.com/maps/api/js?key=#{ENV["GOOGLE_MAP_KEY"]}&callback=initMap"} %script{src: "//cdn.rawgit.com/mahnunchik/markerclustererplus/master/dist/markerclusterer.min.js"} %script{src: "//cdn.rawgit.com/printercu/google-maps-utility-library-v3-read-only/master/infobox/src/infobox_packed.js", type:"text/javascript"}
%head
にはgem "gon"
を使えるようにするための記述をします。
また、%body
にはJSを使うための記述をしています。はじめは%script
も%head
に記述していましたが、GoogleMapが表示されないというエラーが起きたので%body
の最後に記述しました。おそらくは読み込みの順番の問題かと思います。
ENV["GOOGLE_MAP_KEY"]
には.env
ファイルに隠したAPIkeyを入れています。.envGOOGLE_MAP_KEY = "あなたが取得したkeyを記述してください"underscore.jsを作成
その後、app/assets/javascripts下にunderscore.jsを作成してください。作成したファイルに次のリンク先のコードをコピペして貼り付けるらしい。
application.jsで読み込み設定
application.jsに次の記述を追記します。
application.js//= require underscore //= require gmaps/googlemodelを編集
modelを次のように編集していきます。
review.rbclass Review < ApplicationRecord has_one :spot, dependent: :destroy accepts_nested_attributes_for :spot endspot.rbclass Spot < ApplicationRecord belongs_to :review geocoded_by :address after_validation :geocode endviewを編集
投稿ページを作成します。GoogleMapを表示させる記述以外は省略してます。
new.html.haml= form_with(model: @review, local: true, multipart: true) do |f| .spot = f.fields_for :spot do |s| = s.label :address, "レビュー場所(Google Mapで検索)", class: 'spot__title' = s.text_field :address, placeholder: "スポットを入力", id: "address", class: 'spot__text' %input{onclick: "codeAddress()", type: "button", value: "検索する"} .map{id: "map", style: "height: 320px; width: 640px;"}次に、投稿したレビューを表示させるページを作成します。
show.html.haml.show .show__address = @review.spot.address .show__maps{id: "show_map", style: "height: 320px; width: 400px;"}controllerを編集
controllerも編集しておきます。
reviews_controller.rbdef new @review = Review.new @review.spot.build end def create @review = Review.new(review_params) if @review.save redirect_to root_path else redirect_to new_review_path end end def show @review = Review.find(params[:id]) @lat = @review.spot.latitude @lng = @review.spot.longitude gon.lat = @lat gon.lng = @lng end private def review_params params.require(:review).permit( :title, :description, spot_attributes: [:address] ) endshowアクションで記述している
@lat = @review.spot.latitude
@lng = @review.spot.longitude
gon.lat = @lat
gon.lng = @lngでは、controllerで定義した
@lat
と@lng
の変数をJavaScriptでも扱えるように、それぞれgon.lat
とgon.lng
に代入しています。JavaScriptの編集
いよいよJavaScriptでGoogleMapを表示させていきます。
googlemap.jslet map //変数の定義 let geocoder //変数の定義 function initMap(){ //コールバック関数 geocoder = new google.maps.Geocoder() //GoogleMapsAPIジオコーディングサービスにアクセス if(document.getElementById('map')){ //'map'というidを取得できたら実行 map = new google.maps.Map(document.getElementById('map'), { //'map'というidを取得してマップを表示 center: {lat: 35.6594666, lng: 139.7005536}, //最初に表示する場所(今回は「渋谷スクランブル交差点」が初期値) zoom: 15, //拡大率(1〜21まで設定可能) }); }else{ //'map'というidが無かった場合 map = new google.maps.Map(document.getElementById('show_map'), { //'show_map'というidを取得してマップを表示 center: {lat: gon.lat, lng: gon.lng}, //controllerで定義した変数を緯度・経度の値とする(値はDBに入っている) zoom: 15, //拡大率(1〜21まで設定可能) }); marker = new google.maps.Marker({ //GoogleMapにマーカーを落とす position: {lat: gon.lat, lng: gon.lng}, //マーカーを落とす位置を決める(値はDBに入っている) map: map //マーカーを落とすマップを指定 }); } } function codeAddress(){ //コールバック関数 let inputAddress = document.getElementById('address').value; //'address'というidの値(value)を取得 geocoder.geocode( { 'address': inputAddress}, function(results, status) { //ジオコードしたい住所を引数として渡す if (status == 'OK') { let lat = results[0].geometry.location.lat(); //ジオコードした結果の緯度 let lng = results[0].geometry.location.lng(); //ジオコードした結果の経度 let mark = { lat: lat, //緯度 lng: lng //経度 }; map.setCenter(results[0].geometry.location); //最も近い、判読可能な住所を取得したい場所の緯度・経度 let marker = new google.maps.Marker({ map: map, //マーカーを落とすマップを指定 position: results[0].geometry.location //マーカーを落とす位置を決める }); } else { alert('該当する結果がありませんでした'); } }); }今回は、地図を表示させる場所が2か所あり、それぞれ別の場所をマップの中心にしたかったので、initMap関数内で「指定の
id
の有無」による条件分岐を設けることで対応しています。JavaScriptについては知識が浅く、コメントアウトで解説した部分で解釈の違いがあるかもしれません。その際にはご指摘頂けると有り難いです。
おわりに
GoogleMapは多くのサイトで利用されているので、どのように実装すればいいかを多少なり知ることができて良かったです!GoogleMapAPIにはまだまだ使ってない機能が沢山あるので、時間があれば色々チャレンジしていきたいと思います!!
参考記事
gonを使ったRailsとJavascriptの連携について
ジオコーディング備忘録
RailsでGoogleMapを表示させる(gem 'gmaps4rails'の使い方)
Rails Google Mapを表示させる方法
- 投稿日:2020-02-28T00:17:45+09:00
読書ログ『メタプログラミング Ruby 第2版』4章
4章 水曜日: ブロック
はじめに
- ブロックとは、スコープを制御するのに強力なツール
- ブロックは「呼び出し可能オブジェクト」大家族の一員
- ブロックの家族はオブジェクト指向とは異なる、「関数型プログラミング 言語」の流れ
ブロックの基本
- ブロックを定義できるのは、メソッドを呼び出す時だけ。
def a_method(a,b) a + yield(a,b) end a_method(1,2){ |x, y|(x + y) *3 } #=> 10
- メソッド内部では、Kernel#block_given?メソッドを使ってブロックの有無を確認できる
def a_method return yield if block_given 'ブロックがありません' end a_method #=> "ブロックがありません a_method { "ブロックがあるよ!" } #=> "ブロックがあるよ!"ブロックはクロージャ
- ブロックのコードを実行するには、環境(ローカル変数 / インスタンス変数 / self)が必要
- これらオブジェクトに紐づけられた名前のことで、束縛と呼ばれる
- ブロックには、コードと束縛の集まりの両方が含まれる
- ブロックを定義すると、その時点でその場所にある束縛を取得する。
- ブロックをメソッドに渡したときは、その束縛も一緒に連れて行く
def my_method x = "Goodbye" yield("cruel") end x = "Hello" my_method{ |y| "#{x}, #{y} world" } #=> "Hello, cruel world"
- ブロックの中で新しい束縛を定義することもできる
- ブロックが終了した時点で消えてしまう
def just_yield yield end top_level_variable = 1 just_yield do top_level_variable += 1 local_to_block = 1 end top_level_variable #=> 2 local_to_block #=> Error !
- ブロックがローカル束縛を包み込んで一緒に連れて行くので、ブロックはクロージャ
スコープ
- プログラムがスコープを変えると、新しい束縛と置き換えられる
- 一般的にはスコープが変わると、束縛はスコープを抜ける
スコープゲート
- プログラムがスコープを切り替えて、新しいスコープをオープンする場所
- クラス定義
- モジュール定義
- メソッド
- プログラムがクラスやモジュールの定義、あるいはメソッドに出入りすると、スコープが変化する
- 境界線は、class, module, defといったキーワードで印が付けられている
v1 = 1 class MyClass #スコープゲート: classの入り口 v2 = 2 local_variables #=> ["v2"] def my_method # スコープゲート: defの入り口 v3 = 3 local_variables end # スコープゲート: defの出口 local_variables #=> ["v2"] end obj = MyClass.new obj.my_method #=> [:v3] local_variables #=> [:v1, :obj]
- クラスやモジュールの定義のコードはすぐに実行されるが、メソッド定義のコードはメソッドを呼び出した時に実行される
スコープのフラット化
- スコープゲートを通り抜けて、ローカル変数を持ち運ぶ
classの場合
- Class.newを使う
my_var = "成功" MyClass = Class.new do puts "クラス定義の中は#{my_var} !" def my_method ... end enddefの場合
- define_methodを使う
my_var = "成功" MyClass = Class.new do puts "クラス定義の中は #{my_var}!" define_method :my_method do "メソッド定義の中も #{my_var}!" end endフラットスコープ
- 2つのスコープを一緒の場所に押し込めて、変数を共有する魔術
- 「スコープのフラット化」 / 「入れ子構造のレキシカル(静的)スコープ」とも
スコープの共有
- 複数のメソッドで変数を共有したいが、その他からは見えないようにする
- 全てのメソッドを同じフラットスコープに定義すればいい
- ただ、実際には共有スコープはあまり使われない
def define_methods shared = 0 Kernel.send :define_method, :counter do shared end Kernel.send :define_method, :inc do |x| shared += x end end define_methods counter inc(4) counter
- スコープゲート、フラットスコープ、共有スコープを組み合わせれば、スコープを自由にねじ曲げて、好きな場所から必要な変数を見に行けるようになる
クロージャのまとめ
スコープゲート
- class / module / def
クロージャ
- ブロックのこと
- ブロックを定義すると、現在の環境にある束縛を包み込んで、持ち運ぶことができる
フラットスコープ
- classは
Class.new
/ moduleはModule.new
/ defはModule.define_method
と置き換えることができる共有スコープ
- 同じフラットスコープに複数のメソッドを定義してスコープゲートで守る
instance_eval
- instance_evalに渡したブロックは、レシーバをselfにしてから評価されるので、レシーバのprivateメソッドや@vなどのインスタンス変数にもアクセスできる
- Instance_evalに渡したブロックのことを コンテキスト探査機と呼ぶ
class MyClass def initialize @v = 1 end end obj = MyClass.new obj.instance_eval do self #=> #<MyClass:0x3340dc @v=1> @v #=> 1 end
- コンテキスト探査機を使うと、カプセル化を破壊できる
- irbからオブジェクトの中身を見たい時、instance_evalを使ってオブジェクトに入るのが近道
- ブロックを評価するためだけにオブジェクトを生成するオブジェクトを、クリーンルームと呼ぶ
- クリーンルームとして使うには、BasicObjectのインスタンスが最適
呼び出し可能オブジェクト
- ブロックの使用は2つのプロセスに分けられる
- コードを保管する
- (yieldを使って)あとで呼び出す
- Rubyでコードを保管できる場所
- Procの中。これはブロックがオブジェクトになった物
- Lambdaの中。これはProcの変形
- メソッドの中。
Prcoオブジェクト
- ブロックはオブジェクトではない
- Procはブロックをオブジェクトにしたもの
- Procを生成するには、
Proc.new
にブロックを渡す- オブジェクトになったブロックをあとで評価するには、Proc#callを呼び出す
inc = Proc.new{ |x| x + 1 } inc.call(2) #=> 3
- Procを生成する方法
- lambda / proc
dec = lamda{|x| x-1 } dec.class #=> Proc dec.call(2) #=> 1「Proc」対「lambda」
- lambdaで作ったProcは、他のProcとは違い、lambdaと呼ばれる
- Procとlambdaの違い
- returunキーワードに関すること
- 引数のチェックに関すること
- 結局どっちを使えばいいの?
- Procの機能が必要でない限り、最初にlambdaを選ぶ
Methodオブジェクト
- Object#methodを呼び出すと、メソッドそのものをMethodオブジェクトとして取得できる
class MyClass def initialize(value) @x = value end def my_method @x end end object = MyClass.new(1) m = object.method :my_method m.call #=> 1
- Methodオブジェクトはブロックやlambdaに似ている
- lambdaは定義されたスコープで評価されるが、Methodは所属するオブジェクトのスコープで評価される
呼び出し可能オブジェクトのまとめ
- ブロック
- 定義されたスコープで評価される
- Proc
- Procクラスのオブジェクト。ブロックのように、定義されたスコープで評価される
- lambda
- Procクラスのオブジェクトだが、通常のProcとは微妙に異なる。ブロックやProcと同じくクロージャであり、定義されたスコープで評価される
- メソッド
- オブジェクトに束縛され、オブジェクトのスコープで評価される
- オブジェクトのスコープから引き離し、他のオブジェクトに束縛することもできる。
感想
な、なんとかまとめてみた。。。
ちょっと、この章の理解が全章までと比べて弱い...
ブロックは、わかった気になってるけど、なんだろう。慣れてないTODO: 時間を空けてもう一度読み直す