20200228のRubyに関する記事は14件です。

Ruby の Enumerable を再実装するときに正解が見たくなったら TruffleRuby の実装を見るとわかりやすい

Enumerable の再実装やってると正解が見たくなる

仲間で集まって Ruby の Enumerable のメソッドを再実装する遊びをよくやっているんだけど、書いたら答え合わせがしたくなる。そういうときは CRuby の実装 を見るのが一番なんだけど、Cが読めないとかなり辛い。

例: Enumerable#collect の CRuby の実装(?)

enum.c
static 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;
}

ruby/ruby enum.c より

なにか別の関数(collect_i?)を呼び出してるっぽいことくらいしかわからない…

TruffleRuby はわりと Ruby でかかれてる

いろいろ有名な処理系を見たら、 Oracle の TruflleRubyEnumerable 実装が Ruby で書かれているのでとっつきやすかった。

src/main/ruby/truffleruby/core/enumerable.rb
  def collect
    if block_given?
      ary = []
      each do |*o|
        ary << yield(*o)
      end
      ary
    else
      to_enum(:collect) { enumerator_size }
    end
  end

oracle/truffleruby src/main/ruby/truffleruby/core/enumerable.rb より

読める!読めるぞ!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Rails6系とMySQL8系の組み合わせの開発環境をDockerだけでつくる

概要

  • :point_right: この記事は弊社ァ内で行った準備作業の備忘録メモでございますわっ
  • 次の新規案件に備え、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              local

docker 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-handshake

APコンテナ(一時)を建てる

$ # 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のバージョンを決めている
Gemfile
source '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.yml
config/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

スクリーンショット 2020-02-21 18.37.09.png

  • うまく、いった、みたい、

ソースコードのコピー

  • ディレクトリ内をコピーするときは . をつけるらしい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 -a

DBコンテナ(一時)の削除

  • DBコンテナ(一時)も、消す。
$ # host OS (local PC)
$ 
$ docker stop tmp_db_mysql
$ docker rm tmp_db_mysql
$ 
$ # 消えていることを確認
$ docker ps -a

docker network(一時)の削除

  • docker network(一時)も、消す。
$ # host OS (local PC)
$ 
$ docker network rm ${APP_NETWORK}
$ 
$ # 消えていることを確認
$ docker network ls

docker volume(一時)の削除

  • docker volume(一時)も、消す。
$ # host OS (local PC)
$ 
$ docker volume rm ${DB_VOLUME}
$ docker volume rm ${BUNDLE_VOLUME}
$ 
$ # 消えていることを確認
$ docker volume ls

Railsプロジェクトの新規作成まとめ

  • 長文になってしまって申し訳ナソス
  • ローカル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/Dockerfile
FROM 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.conf
containers/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/Dockerfile
FROM 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.yml
version: '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 にアクセス(初回は時間がかかるのでログを見て落ち着いたら)

スクリーンショット 2020-02-28 20.23.46.png

  • うまく、できた、みたい、

READMEに説明を書く

  • やる。
  • 僕だけ使えても意味がない。
  • もしかしたらこの作業が一番骨折れるかもしれない。

最後に

  • 長文失礼。
  • ポート開放不親切失礼。必要に応じて書き換え願う
  • ruby2.7対応していないgemがいくつかあるみたいで、ログが汚い。warningの大量発生や。。。(2020/02ころ現在)
  • WEBコンテナ追加してnginxとソケット通信する作業でに労力増えてる感ある。rails severのありがたみを感じる。。。
  • redisの設定は開発時にやるで良いよね。。。

  1. https://hub.docker.com/_/ruby ←←2020/02くらいにここを閲覧した、そして全部取得して比べてみた。 

  2. https://docs.docker.com/storage/volumes/ ←Docker領域内に名前つけて保存されるから、万が一ソースコードごと紛失してもワンチャンある() 

  3. https://medium.com/veltra-engineering/how-to-copy-a-directory-using-docker-cp-command-f2c73f9ccf75 ←←こちらのサイトがとても参考になりました :pray::pray::pray: 

  4. https://docs.docker.com/develop/develop-images/multistage-build/#use-a-previous-stage-as-a-new-stage 

  5. https://docs.docker.com/compose/compose-file/compose-versioning/ 

  6. https://github.com/puma/puma#binding-tcp--sockets unixソケットにするためにbindのオプションをconfig/puma.rbに書く必要があった。こことかこれとかこのへん読んでも書き方迷いそう。。。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyでもいい感じにパスワードを出して欲しい

イメージ

確かにLinuxだったらさ、コマンドでランダムなパスワードとか出せると思うけどRubyでもやりたいじゃんと思って考え中

やり方

securerandomというライブラリを使えばランダムな値を簡単に生成できるので便利かも

実行方法

ruby randompw.rb [パスワードの桁数]

randompw.rb
require 'securerandom'

i = ARGV[0]
#puts i
puts SecureRandom.alphanumeric(i.to_i)

感想

こんな感じにSecurerandomのライブラリを使って何桁のパスワードを出力して欲しいかを入れるだけ

こうすると、とりあえずランダムなパスワードがいい感じができるのでちょっと満足した。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ハッシュからキーを取り出して配列に格納する

問題

book = {title: "多動力", price: 1280, impression: モチベが上がる}

上記のようなハッシュからキーだけを取り出して配列に格納するにはどうしたら良いでしょうか

方法

ハッシュに対してkeysメソッドを使用して配列に格納します。

array = book.keys

出力すると

puts array

キーだけが取り出されて出力されます

ターミナル
title
price
impression

この方法でハッシュからキーだけを取り出して配列に格納することができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【slice と slice! の使い方】(ruby)

sliceをあまり使っておらず、使い方を忘れていたので書き残しておきます。

次の条件を満たすメソッドを作成しなさい(ruby)

  • 任意の文字列の最初の2文字のみを出力する
  • 文字列が2文字以下の場合文字列をそのまま返す
  • 例えば"a"は"a"を、空文字""は""を返す

回答例

head_two.rb
def 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.rb
  example2 = 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.rb
  example3 = word.slice(0, 2)
  puts  example3 #=> ha
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スクリプト言語 KINX/基本編(2) - 制御構造

はじめに

前回の続き。過去の解説は以下を参照。概要は以下の初回記事「スクリプト言語 KINX(ご紹介)」を参照してください。

見た目は JavaScript頭脳(中身)は Ruby、(安定感は AC/DC)」なスクリプト言語 KINX。オブジェクト指向と C 系シンタックスで C 系プログラマになじむ触感 をお届け。

Kinx 基本編(2)- 制御構造

文・制御構文

文(ステートメント)として、宣言、代入、continuebreakreturnthrowyield、および制御構文として if-elsewhiledo-whileforswitch-casetry-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 ... 実数。intdbldblint へのキャストはサポートできていない。
  • native<type> ... type は復帰値のタイプ(int or dbl)。native< native<type> > とかは未サポート。

代入文

代入文は普通の式文。代入は右辺から評価される。

a = b = 10;

上記では b = 10 が先に評価され、その結果が a に代入される。

continue

ループ先頭に戻る。正確にはループ条件式の直前に戻る。ただし、for 文の場合は第三フィールド(カウンタ更新の部分、何て言うんだ?)の直前に戻る。

continue はラベル指定が可能。continue LABELLABEL の示すブロックの先頭(ブロックがループの場合は上記の場所)に制御が戻る。また、continue は if 修飾が可能。continue if (expression) の形で条件を指定することができる。

continue;
continue LABEL;

break

ループを抜ける。正確にはループ・ブロックの直後に進む。

break はラベル指定が可能。break LABELLABEL の示すブロックの末尾に制御が進む。また、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() で確認する感じ。今のところ、SystemExceptionFiberExceptionRuntimeException というのがあるが、ユーザーが一般に投げられる例外は 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 とかがスローされる可能性があるのでソレ用)のと、スタックトレースが保持されないという制約がある。これらは何とかなりそうな気もするので、今後の検討課題。

おわりに

誰かが期待してくれるのかはさっぱり不明だが、ご意見・ご要望は随時募集中。色々考えよう。

今回も地道に★が増えるといいな、と宣伝。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】入力された値を元にGETリクエストを送ってjsonを取得し、DBに保存する

すること

今回はGET statuses/oembed — Twitter Developersを用いてツイッターカードを表示します。
image.png
流れは以下の通りです。

  1. 入力されたURLからIDを取得
  2. リクエストを送信
  3. データを取得
  4. URL, jsonをデータベースに保存
  5. 保存したデータを表示

開発環境

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.rb
class 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

createupdateの違いは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)
  end

Net::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)
  end

Ruby の 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&#39;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>&mdash; 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' %>

リンク

例外処理など細かく行う場合

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rubyXLでexcelデータを出力する際に悩んだこと。

備忘録な意味合いで残します。

1. テンプレートとするExcelで完全にブランクなセルに値をセットしようとするとエラーになる。
・罫線などの情報を一切もっていないセルには、
  file[sheet_name][row][col].change_contents(value)
 こういう指定ができない。
 もしこれで、nil:NilClassのエラーがでるようなら、該当のセルにスペースを一度いれておく。とかで取得できるみたい。

2. insert_rowで値を挿入すると、その下のセル結合などが壊れる。
・仕様のようなので仕方がないが、UIで操作する単純な行の挿入のイメージとは処理が違う。挿入する行の前後は、セル結合などは極力してない方がベスト。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Devise のviewを編集可能にする

はじめに

導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。

Deviseを導入

実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sass

Devise 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:views

1.に書かれている通りにconfig/environments/development.rbにURLオプションを記載する。

config/environments/development.rb
config.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:migrate

viewの作成

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.rb
config.scoped_views = true

終わりに

deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Devise viewのデザインを編集可能にする

はじめに

導入当初にBootstrapを使ってデザインや配置が変更できなかったので配置できるようにしてみました。
*導入初心者向けです。

Deviseを導入

実行環境
ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem bootstrap-sass

Devise 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:views

1.に書かれている通りにconfig/environments/development.rbにURLオプションを記載する。

config/environments/development.rb
config.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:migrate

viewの作成

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.rb
config.scoped_views = true

終わりに

deviseの導入からviewの編集可能方法まで書きました。
devise:controllers usersなどもありますがコントローラに関しては使う人それぞれなので今回は省きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
end

in句に関して

https://www.sejuku.net/blog/72497

僕の記事より圧倒的にわかりやすい記事を書いている中で発見しました

https://nyoken.com/rails-includes

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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は後々使用します。

Google Maps Platform

今回、利用するAPIは次の2つです。

  • Maps JavaScript API
  • Geocoding API

この2つを有効に設定しておいてください。

gemのインストール

必要なgemをインストールします。

Gemfile
gem "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を入れています。

.env
GOOGLE_MAP_KEY = "あなたが取得したkeyを記述してください"

underscore.jsを作成

その後、app/assets/javascripts下にunderscore.jsを作成してください。作成したファイルに次のリンク先のコードをコピペして貼り付けるらしい。

underscore.js

application.jsで読み込み設定

application.jsに次の記述を追記します。

application.js
//= require underscore
//= require gmaps/google

modelを編集

modelを次のように編集していきます。

review.rb
class Review < ApplicationRecord
  has_one :spot, dependent: :destroy
  accepts_nested_attributes_for :spot
end
spot.rb
class Spot < ApplicationRecord
  belongs_to :review

  geocoded_by :address
  after_validation :geocode
end

viewを編集

投稿ページを作成します。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.rb
def 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]
  )
end

showアクションで記述している

@lat = @review.spot.latitude
@lng = @review.spot.longitude
gon.lat = @lat
gon.lng = @lng

では、controllerで定義した@lat@lngの変数をJavaScriptでも扱えるように、それぞれgon.latgon.lngに代入しています。

JavaScriptの編集

いよいよJavaScriptでGoogleMapを表示させていきます。

googlemap.js
let 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を表示させる方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

読書ログ『メタプログラミング 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
end

defの場合

  • 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と同じくクロージャであり、定義されたスコープで評価される
  • メソッド
    • オブジェクトに束縛され、オブジェクトのスコープで評価される
    • オブジェクトのスコープから引き離し、他のオブジェクトに束縛することもできる。

感想

な、なんとかまとめてみた。。。

ちょっと、この章の理解が全章までと比べて弱い...
ブロックは、わかった気になってるけど、なんだろう。慣れてない:sweat:

TODO: 時間を空けてもう一度読み直す

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む