20190708のRailsに関する記事は28件です。

カスタムバリデーションで他のモデルのカラムを使用する

About

カスタムバリデーションで他のモデルのカラムを使いたい場合の記載方法について記載しています。

Environment

この記事ではmacbook(unix)にインストールしたruby 2.5.1p57, Rails 5.2.3を使用しています。

Validationとは

「バリデーション」とは、「検証、実証、認可、妥当性」を意味する英単語
「質的な良し悪し」を判断するのではなく、「システム的な適合不適合」を判断するための言葉

小難しく書きましたが、意図しないデータをDBに登録できない様、バリデーションをかけることが一般的であるそうです。

(参考文献)
https://career-picks.com/business-yougo/validation/

カスタムバリデーション

通常、バリデーションは以下の様な形式で設定しますが、複雑なバリデーションを設定したい場合、自作のバリデーション(= カスタムバリデーション)を作成します。

通常
class User < ApplicationRecord
  validates :user_id, presence: true
  validates :email, presence: true
end

基本的には、以下の様に定義したメソッド名をvalidate に続けて記載することで、設定可能です。

カスタムバリデーション
class User < ApplicationRecord
  validates :user_id, presence: true
  validates :email, presence: true
  validate :if_user_does_not_have_nickname

  def if_user_does_not_have_nickname
    return if nickname.present?
    errors.add(:nickname, "Nickname is absent")
  end
end

他のモデルのカラムも組み合わせたい場合

validationを考えていると、他のモデルのカラムを条件として加えたい時があるかと思います。
その様な場合、モデル間でアソシエーションを組むことで他のモデルのカラムを使用することができます。

(例)睡眠時間を記録するアプリ
・日付がデータとして渡されている
・もしユーザの睡眠記録が一つもなければ、睡眠記録の新規作成が可能
・睡眠記録がすでにある場合は、渡されたデータが既存の睡眠記録の最終日付の翌日にマッチしているかどうか調べる
・ミスマッチの場合、渡されたデータを新規登録しない

sleep.ruby
class Sleep < ApplicationRecord
  # アソシエーションの設定。読み込み順の関係で、カスタムバリデーションより必ず上に記載してください。
  belongs_to :user
  validates :slept_time, presence: true
  validates :wakeup_time, presence: true
  # カスタムバリデーションの呼び出し(コントローラでnew, createメソッドの場合のみ)
  validate :dates_cannnot_be_registered_if_there_is_no_yesterdays_date, on: [:new, :create]

  # カスタムバリデーションの作成
  def dates_cannnot_be_registered_if_there_is_no_yesterdays_date
    # もし日付があり、ユーザの睡眠記録がない場合は処理を抜ける(= そのまま新規登録する。)
    return if date.present? && user.sleeps.blank?
    # もし日付がユーザの最新の睡眠記録の日付の翌日でない場合は、データを新規登録しない。
    if date != user.sleeps.last.date.tomorrow
      errors.add(:date, "You can't register that there is no data about before days.")
    end
  end
end

最後に

いかがでしょうか。アソシエーションを組めていれば、意外と簡単に実装できてしまいます。
私はbelongs toの記述位置がカスタムバリデーションの下になっていたため、エラーと何時間も格闘する羽目になりましたが...。
参考になれば幸いです!

筆者について

TECH::EXPERTにて4月よりruby, railsを学習している未経験エンジニアです。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!

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

deviseでユーザー認証をしよう

deviseでカラムを追加して、ログイン機能を実装するには・・・

userモデルを作成するところまではこちら

https://qiita.com/mokku/private/aa4191817369edb269fb
最低限のログインページ実装は上で完了しています。

deviseのビューを生成する

$ rails g devise:views
Running via Spring preloader in process 5927
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

ビューファイルの編集を行う

今回は、追加でニックネームを入力してもらいたいのでニックネームの入力欄を追加します。
※一旦、仮で反映させておきます。

new.html.erb
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :nickname %><br />
    <%= f.text_field :nickname, autofocus: true, autocomplete: "email" %>
  </div>

image.png

ニックネーム入力欄ができました。

コントローラを作成しよう

ターミナルで下記コマンドを入力してコントローラを作成する

$ rails g controller [コントローラ名]
$ rails g controller users

image.png

参考

deviseのビューファイル
https://qiita.com/yasuno0327/items/ff17ddb6a4167fc6b471

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

amcharts 4 Demos を使ってグラフを作成(piechart編)

About

amcharts関連の日本語文献のあまりの少なさから、誰かのお役に立てればと思い記載しています!

最終的には、このようなグラフを作成することができます。7d0de671ad5679b2edef0a20c7ab9b06.gif

Environment

この記事ではmacbook(unix)にインストールしたruby 2.5.1p57, Rails 5.2.3を使っています。

amchartsとは

前回の記事をご覧ください!

Piechartとは

円グラフです。今回はこちらを作成します。
2e57a92ca3d39c6dbd16f293e967fc7c.gif

導入方法

こちらのサイトのページ内を下にスクロールすると、Demo sourceが置いてありますので、コピペで使用可能です。
基本的にはそのままコードを触らずとも、実装可能です。

Demo_source
<!-- Styles -->
<style>
#chartdiv {
  width: 100%;
  height: 500px;
}

</style>

<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>

<!-- Chart code -->
<script>
am4core.ready(function() {

// Themes begin
am4core.useTheme(am4themes_animated);
// Themes end

// Create chart instance
var chart = am4core.create("chartdiv", am4charts.PieChart);

// Add data
chart.data = [ {
  "country": "Lithuania",
  "litres": 501.9
}, {
  "country": "Czech Republic",
  "litres": 301.9
}, {
  "country": "Ireland",
  "litres": 201.1
}, {
  "country": "Germany",
  "litres": 165.8
}, {
  "country": "Australia",
  "litres": 139.9
}, {
  "country": "Austria",
  "litres": 128.3
}, {
  "country": "UK",
  "litres": 99
}, {
  "country": "Belgium",
  "litres": 60
}, {
  "country": "The Netherlands",
  "litres": 50
} ];

// Add and configure Series
var pieSeries = chart.series.push(new am4charts.PieSeries());
pieSeries.dataFields.value = "litres";
pieSeries.dataFields.category = "country";
pieSeries.slices.template.stroke = am4core.color("#fff");
pieSeries.slices.template.strokeWidth = 2;
pieSeries.slices.template.strokeOpacity = 1;

// This creates initial animation
pieSeries.hiddenState.properties.opacity = 1;
pieSeries.hiddenState.properties.endAngle = -90;
pieSeries.hiddenState.properties.startAngle = -90;

}); // end am4core.ready()
</script>

<!-- HTML -->
<div id="chartdiv"></div>

編集方法

style, htmlについては割愛し、scriptについて筆者がわかる範囲で記載していきます。

index.html.erb
<!-- Chart code -->
<script>
am4core.ready(function() {

// Themes begin
// テーマです。
am4core.useTheme(am4themes_animated);
// Themes end

// Create chart instance
// 始めにインスタンスを作成します。Piechart形式であることをここで指定しています。
var chart = am4core.create("chartdiv", am4charts.PieChart);

// Add data
// ここにデータを入力していきます。
chart.data = [ {
  "country": "Lithuania",
  "litres": 501.9
}, {
  "country": "Czech Republic",
  "litres": 301.9
}, {
  "country": "Ireland",
  "litres": 201.1
}, {
  "country": "Germany",
  "litres": 165.8
}, {
  "country": "Australia",
  "litres": 139.9
}, {
  "country": "Austria",
  "litres": 128.3
}, {
  "country": "UK",
  "litres": 99
}, {
  "country": "Belgium",
  "litres": 60
}, {
  "country": "The Netherlands",
  "litres": 50
} ];

// Add and configure Series
// 作成したインスタンスに設定を追加していきます。
var pieSeries = chart.series.push(new am4charts.PieSeries());
// データはlitres(リットル)
pieSeries.dataFields.value = "litres";
// 単位はcountryです
pieSeries.dataFields.category = "country";
pieSeries.slices.template.stroke = am4core.color("#fff");
pieSeries.slices.template.strokeWidth = 2;
pieSeries.slices.template.strokeOpacity = 1;

// This creates initial animation
pieSeries.hiddenState.properties.opacity = 1;
pieSeries.hiddenState.properties.endAngle = -90;
pieSeries.hiddenState.properties.startAngle = -90;

}); // end am4core.ready()
</script>

実装例

最後に、私が作成した"sommeil"というアプリケーションのコードの内、一部のamcharts部分を参考までに記載します。

_piechart.html.erb
<!-- Styles -->
<style>
  #chart2div {
    width: 100%;
    height: 300px;
  }
</style>
<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
<!-- Chart code -->
<script>
  am4core.ready(function () {
    // Themes begin
    am4core.useTheme(am4themes_animated);
    // Themes end
    // Create chart instance
    var chart2 = am4core.create("chart2div", am4charts.PieChart);
    // Add data
    var sleeping_time = '<%= @sleeping_time %>';
    chart2.data = [{
      "time": "Sleep",
      "amount": sleeping_time
    }, {
      "time": "Awake",
      "amount": (24 - sleeping_time)
    }];
    // Add and configure Series
    var pieSeries = chart2.series.push(new am4charts.PieSeries());
    pieSeries.labels.template.disabled = true;
    pieSeries.dataFields.value = "amount";
    pieSeries.dataFields.category = "time";
    pieSeries.slices.template.stroke = am4core.color("#fff");
    pieSeries.slices.template.strokeWidth = 2;
    pieSeries.slices.template.strokeOpacity = 1;
    // This creates initial animation
    pieSeries.hiddenState.properties.opacity = 1;
    pieSeries.hiddenState.properties.endAngle = -90;
    pieSeries.hiddenState.properties.startAngle = -90;
  }); // end am4core.ready()
</script>
<!-- HTML -->
<div id="chart2div"></div>

最後に

注釈や解説できない部分がまだまだありますので、今後検証して追記していきます。
ご覧いただき、ありがとうございました。

筆者について

TECH::EXPERTにて4月よりruby, railsを学習している未経験エンジニアです。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!

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

railsの基礎知識 ※自分メモ用

Rails学習コース

modelとApplication Record
Postモデル

rails g modelによってPostモデルが定義された「post.rb」app/modelsに作成

◎ターミナル◎

Post=モデル名
content=カラム名
:text=データ名

 rails g model Post content:text

そして、このコマンドによって、以下の2つのファイルが作成される。
・app/modelsフォルダにモデルが定義されたファイル
・db/migrateフォルダにマイグレーションファイル

◎ターミナル◎

rails cosole

ex)text = "Hello"

テーブルに投稿データの保存

posts テーブルにデータを追加するには下の図のように、
① new メソッドで Post モデルのインスタンスを作成
② posts テーブルに保存
これら2つの手順を行っていく。

スクリーンショット 2019-07-08 21.30.20.png

◎具体的なコマンド↓↓↓

post1 = Post.new(content: "ライオン") 新しいの生成
post1.save セーブする

1、テーブルからデータを取り出す方法

①テーブルからデータを1つ取り出す
②contentカラムの値を取り出す
→「Post.first」で取得したデータから投稿内容を取得することができる

①post = Post.first
②post.content

2、テーブルからデータを取り出す方法

↓↓テーブルからすべてのデータを取り出す方法↓↓

posts = Post.all

※「Post.all」では、テーブルにある全てのデータが配列で取得できる

↓↓投稿の配列から1つのデータを取り出す方法↓↓

Post.all [0] 

※Post.all [0] のように、インデックス番号で1つの要素を取得できる

↓↓配列のデータから投稿内容を取り出す方法↓↓

Post.all[0].content

rails consoleまとめ

データの作成(new,save)
データ取得(Post.all,post.content..etc)

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

終了~ヘルスケアwebサービスを自分で作る医者の日記~

12章
12.18テストの途中まで終了
テストコードの仕組み
get してからassertなど、順次上から処理していっているのか?
まだわからん

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

TECH ~Day14~

学習を始めてから早くも2週間が経ち、中間試験までの日にちが近づいてきました。今までの学習はほとんど知識を入れる学習ばかりだったのでいざ問題を解くという場面になった時にどれだけ今までの学習が活かせるのか、とても不安です。
最近は特に忙しくなってきたので、学習を効率よく行うにはどうすればいいのか常に考えています。

学習内容

・部分テンプレート
・render
・render partial オプション
・git, git clone, git hub
・bundlerによるgem管理
・bundle exec コマンド

・部分テンプレート
ツイッターなどでツイート画面などがHTML構造的に同じもの。
同じHTML構造の部分を共通化していくことによって、無駄なくビューファイルを作ることができる。この共通化された部分を「部分テンプレート」という。

・render
部分テンプレートを呼び出す際に利用するメソッド。

・render partial オプション
renderに「:partial」というオプションをつけることで、明示的に部分テンプレート名を指定し、部分テンプレートを表示することができます。

!部分テンプレートのファイル名はアンダーバー「_」から始まる。

・git, git clone, git hub
git
ソースコードの変更履歴、バージョン等を記録するツールで、開発する際には、ひっすと言えるツールです。facebookやgoogleも利用している。

git hub
チーム開発やソースコードの共有にとても便利なサービス。
自分のソースコードを保管できるため、エンジニアの履歴書のような役割も果たす。

git clone リポジトリURL ディレクトリ名
git cloneコマンドは、外部のサーバーにあるgitで管理されたソースコードを自分のパソコン(ローカル環境)にダウンロードするコマンド。
実際に使用する際には「git clone リポジトリURL ディレクトリ名」という形でターミナルで実行して行う。

・bundlerによるgem管理
bundlerを利用してインストールしたgemは、railsの各プロジェクトではなく、rubyのバージョンごとにある保存場所にインストールされています。

「bundle show[gemの名前]」というコマンドを入力するとbundlerでインストールしたgemがどこに保存されているか確認できる。

・bundle exec コマンド
bundler execは二つ以上のrailsアプリケーションを同じバージョンのrubyを利用して作成している際に必要となるコマンド。

「bundle exec rake db:create」
 ↑データベースの作成コマンド

「bundle exec rails s」
 ↑サーバーの起動

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

Strong Parameters~ヘルスケアwebサービスを自分で作る医者の日記~

12章で

def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end

登場
7章を見返すと
Strong Parametersか!
読み直してやっと理解できた、、、

リスト 7.18のコメントと、上の再録コメントでも重ねて指摘しているように、この実装は最終形ではありません。その理由は、paramsハッシュ全体を初期化するという行為はセキュリティ上、極めて危険だからです。これは、ユーザーが送信したデータをまるごとUser.newに渡していることになります。ここで、Userモデルにadmin属性というものがあるとしましょう。この属性は、Webサイトの管理者であるかどうかを示します (この属性を実装するのは10.4.1になってからです)。admin=’1’という値をparams[:user]の一部に紛れ込ませて渡してしまえば、この属性をtrueにすることができます。これはcurlなどのコマンドを使えば簡単に実現できます。paramsハッシュがまるごとUser.newに渡されてしまうと、どのユーザーでもadmin=’1’をWebリクエストに紛れ込ませるだけでWebサイトの管理者権限を奪い取ることができてしまいます。

以前のバージョンのRailsでは、モデル層でattr_accessibleメソッドを使うことで上のような危険を防止していましたが、Rails 4.0ではコントローラ層でStrong Parametersというテクニックを使うことが推奨されています。Strong Parametersを使うことで、必須のパラメータと許可されたパラメータを指定することができます。さらに、上のようにparamsハッシュをまるごと渡すとエラーが発生するので、Railsはデフォルトでマスアサインメントの脆弱性から守られるようになりました。

この場合、paramsハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可し、それ以外を許可しないようにしたいと考えています。これを行うには、次のように記述します。

params.require(:user).permit(:name, :email, :password, :password_confirmation)
このコードの戻り値は、許可された属性のみが含まれたparamsのハッシュです (:user属性がない場合はエラーになります)。

これらのパラメータを使いやすくするために、user_paramsという外部メソッドを使うのが慣習になっています。このメソッドは適切に初期化したハッシュを返し、params[:user]の代わりとして使われます

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

埋め込み~ヘルスケアwebサービスを自分で作る医者の日記~

edit アクションでページ内に埋め込み
updateで更新するとな、、、

<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= hidden_field_tag :email, @user.email %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Update password", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MaterializecssのCarouselを使用して、3秒ごとに画像が自動で切り替わるページを作る

概要

TECH::EXPERTのカリキュラムでオリジナルのミニアプリを作成する機会があり、
その一部のページでMaterializecssのCarouselを使用し、3秒ごとに画像が切り替わるページを作成したので紹介します。

MaterializecssのCarouselとは

画像をくるくると回せる機能です。
Image from Gyazo
https://materializecss.com/carousel.html

自分が作成したページ紹介

Image from Gyazo

作成する前提

MaterializecssがCDNで読み込めている

編集するファイル

・ビューファイル
・CSSファイル
・jsファイル

ビューファイル

about.html.erb
<section class="about-main" >
  <div class="carousel carousel-slider" data-indicators="true" id="big3" >
    <div class="carousel-fixed-item">
      <div class="container">
        <h1 class="white-text">Work Hard See Result</h1>
      <% if user_signed_in? %>
        <a class="btn waves-effect white black-text darken-text-2" href="/" target="_blank">HOME</a>
      <%else%>
        <a class="btn waves-effect white black-text darken-text-2" href="/users/sign_in" target="_blank">Log in</a>
      <%end%>
      </div>
    </div>
    <div class="carousel-item big3pic" id="benchpress">
      <div class="container">
        <h3 class="white-text">Bench Press</h3>
        <p class="white-text">chest</p>
      </div>
    </div>
    <div class="carousel-item big3pic" id="deadlift">
      <div class="container">
        <h3 class="white-text">Dead Lift</h3>
        <p class="white-text">back</p>
      </div>
    </div>
    <div class="carousel-item big3pic"  id="squat">
      <div class="container">
        <h3 class="white-text">Squat</h3>
        <p class="white-text">legs</p>
      </div>
    </div>
  </div>
</section>

"carousel carousel-slider" を用いました。
"carousel-fixed-item"は固定して表示したいものを書きます。
"carousel carousel-item"の部分がそれぞれの画像のクラスです。

CSSファイル

style.css
#big3{
  height: 100vh;
}
#benchpress{
  background-image: url('benchpress.jpg');
  background-size: 100%;
}
#deadlift{
  background-image: url('deadlift.jpg');
  background-size: 100%;
}
#squat{
  background-image: url('squat.jpg');
  background-size: 100%;
}

それぞれidを付与してるので、idによって画像を変更してください。

jsファイル

about.js
$(document).ready(function(){
  $('#big3').carousel(
  {
    dist: 0,
    padding: 0,
    fullWidth: true,
    indicators: true,
    duration: 100,
  }
  );
  autoplay()
  function autoplay() {
      $('#big3').carousel('next');
      autoplay: true,
      setTimeout(autoplay, 3000);
  }
});

autoplayの関数を定義してます。
id=big3に対して3000ミリ秒で次のcarousel-itemを
表示するように設定してます。

オプションは下記を参照しました。

  • [1] dist: 0, => 遠近ズーム0
  • [2] padding: 0, => 中央以外の項目の余白0
  • [3] fullWidth: true, => carouselを全幅のスライダーへ
  • [4] indicators: true, => インジケータを表示
  • [5] duration: 100, => 次のスライドへ移動しきるまでの時間100ミリ秒

Image from Gyazo

最後に

この記事を書いた目的

・自分なりに工夫した点をアウトプットして、理解を深める。
・あわよくば有識者にフィードバックをもらいたい。
・私と同じ初学者からも奇譚のない意見をもらいたい。(自分だったらどうこうする的な)

筆者について

TECH::EXPERTにて4月27日より52期夜間・休日コースでruby/railsを学習している未経験エンジニアです。
ご不備等ありましたら、ご指摘ください。ちなみに本記事が初投稿になります。
言わずもがなかもしれませんが、趣味はボディメイク・筋トレでございます。
余談ですが、120kg⇨66kgまで減量して大会出場した経験があり
ダイエットについての質問はなんでも答えられるかと思います:muscle:

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

letter_opener でメール内容がブラウザで表示されない

letter_opener でメール内容がブラウザで表示されない

表題のとおりなのですが、
letter_openerの設定を正しく行っても、うまくブラウザに表示されない場合....

Railsであれば、ApplicationMailerfromを確認しましょう。
fromが設定されていないと、ブラウザで表示されません・・・・

これ調べるので1日使ってしまった・・・・orz

自分用メモではあるのですが、
他の同じハマリをしている方達への教訓となれば幸いです。

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

TECH ~Day13~

学習内容

・アソシエーション
・n + 1問題
・includesメソッド
・後置if
・updateメソッド

・アソシエーション
モデル間の関連付けを管理する機能のこと。
定義しておくことでモデルをまたいだデータの呼び出しをより簡単に行うことができる。

・n + 1問題
データを呼び出す際に大量のSQLが発行されてしまう問題。

・includesメソッド
これはn+1問題を解消することができる。
指定された関連モデルをまとめて取得することで、SQLの発行回数を減らしてくれる。
書き方
 includes(:モデル名)

・後置if
最後のendを省略してif文を処理の後方に配置する書き方。
elsif、elseに当たる条件分岐がなく、かつ処理が一行で完結する場合に用いる。

・updateメソッド
Active Recordのうちの一つ。
updateメソッドはモデルのインスタンスに対して使用することで、引数内の情報にレコードを更新することができる。

コメントを投稿するためのルーティングの記述方法

・resourcesメソッド
・ルーティングのネスト

・resourcesメソッド
Railsの基本となるコントローラの7つのアクションで記載した7つのアクション名に対してのルーティングを自動で生成するメソッド。
7つのアクションを簡単に定義するメソッド。

・ルーティングのネスト
ネストとは入れ子構造とも呼ばれ、ある記述の中に入れ子構造で別の記述をする方法である。
ルーティングでいうと、あるコントーラへのルーティングの記述の中に、別のコントローラへのルーティングを記述するということを示す。

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

rails スラッシュを複数挟んでもnot foundにならない

同僚から

Rails のルーティングは連続するスラッシュは単一扱いされるらしい。
localhost:3000/utilities///////hoge/39
の様にスラッシュを複数挟んでもnot foundにならないのはなんでなん?

という質問を受けたので、備忘録として書いておきます。

僕が調べた感じ、railsの名前解決の流れは
https://qiita.com/kkyouhei/items/1203f5aa521c065a7097
らしい。

そこで通る
Journey::Router::Utils.normalize_path(path)
にて

 def self.normalize_path(path) 
     path = "/#{path}"
     path.squeeze!('/')
     path.sub!(%r{/+\Z}, '')
     path = '/' if path == ''
     path
 end

https://github.com/rails/journey/blob/master/lib/journey/router/utils.rb#L14

引数に含まれる文字が複数並んでいたら 1 文字にまとめる

.squeeze!('/')をしているから

https://docs.ruby-lang.org/ja/latest/method/String/i/squeeze.html

履歴にも残してはいけないものを書いていたので上げ直しです。
ごめんなさい

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

[Rails]refile + fields_for の書き方

やりたいこと

・refileを使用して画像アップロードの実装
・複数モデルに一度で保存したいため、fields_forを使用

この2つの組み合わせの書き方が調べてもほとんど出てこなかったので書いておく。

環境

Ruby 2.5.1
Rails 5.2.3

導入、基本的な使い方参考

refileの基本と複数画像のアップロード
導入方法や基本的な使い方は上記サイトを参考にしてください。

ただ一点、Gemfileのインストールは上記サイトの通りだとエラーが出たので、
下記コードで実行できました。

Gemfile
gem "refile", require: "refile/rails", github: 'manfe/refile'
gem "refile-mini_magick", github: 'refile/refile-mini_magick'

参考サイト様だとgithub: 'refile/refile'となってる部分、
bundle install時にエラーが出たので、エラーメッセージに従ってgithub: 'manfe/refile'としたら成功しました。

fields_forを使用する場合の書き方

前提:
・親モデルと子モデルは1対1のアソシエーション
・1画像のみアップロードするものとする

入力フォーム

ビューファイル.html.haml
= form_with model: @user do |f|    # 親モデルをuserと仮定
   # 省略

  = f.fields_for :image do |i|     # 子モデルをimageと仮定
    = i.attachment_field :image    # 「:image」 はimage_idカラムの_idを抜いたもの

attachment_fieldを使用する。
これでfile_fieldと同じファイル選択フォームが作成される。

モデルのアソシエーション

モデル.rb
  # userモデル
  has_one :image, dependent: :destroy    # 「dependent: :destroy」でuserを削除すると関連するimageも削除される
  accepts_nested_attributes_for :image   # fields_forに必要な記述
  accepts_attachments_for :image, attachment: :image  # refileに必要な記述

  # imageモデル
  attachment :image # 「:image」 はimage_idカラムのidを抜いたもの
  belongs_to :event

attachment :image:image はモデル名ではなく、カラム名なので間違えないように。
カラムは「image_id」で登録していても、ここでは「_id」は抜かして書く。

コントローラーの記述

users_controller.rb
  def new
    @user = User.new
    @user.build_image  # fields_forのために親子モデルを関連付け
  end

  def create
    User.create(user_params)
  end

  def index
    @users = User.all.includes(:image)  # この呼び出し方が必須というわけではないです
  end

  def user_params
    params.require(:user).permit(
      :name,
      :email,
      image_attributes: [
      :id,
      :image,           # 「image_id」カラムでも「_id」は入れなくて良い
      :image_cache_id   # これを記述しておくとキャッシュファイルも保存される(らしい)
      ]
    )
  end

最低限の記述のみ書いてます。

ちなみに、.includes(:image)で子モデルが2つ以上ある場合は、
.includes([:image, :address])
のように、引数内で[]で囲む必要があるので注意。

ビューファイルでの保存した画像の表示

ビューファイル.html.haml
= attachment_image_tag @user.image, :image, :fill, 200, 120, format: "jpg"

# 「@user.image」 は子モデル名。今回はfields_forでuserが親モデルとして紐づいているためこのように指定
# 「:image」 はimage_idカラムのidを抜いたもの
# 「200, 120,」 は表示する画像のサイズ。 「横, 縦,」

attachment_image_tagを使用して保存したデータを呼び出す。

今回、1画像のみのアップロード前提で書いたが、複数画像アップロードだとかなり変わってくる。

複数画像アップロードの場合

入力フォーム

ビューファイル.html.haml
= form_with model: @user do |f| 
   # 省略

  = f.fields_for :images do |i|     # :imagesと複数形にする
    = i.attachment_field :image, multiple: true, name: "images_attributes[0][image][]"    
# 「multiple: true」 で複数ファイル選択を可能にする
# 「name: "images_attributes[0][image][]"」 でname属性をparamsに合わせる

= f.fields_for :images do |i|のモデル指定を:imagesと複数形にする。
その下の= i.attachment_field :imageはimage_idカラムのidを抜いたものなので単数形のままでOK。
multiple: trueを指定することにより、fields_forもあるためparamsの形がかなり変わり、そのままではparamsに値が入らない。
値がちゃんとparamsに入るよう、name: "images_attributes[0][image][]"によりname属性を変わった後のparamsと合わせた形に指定し、値がparamsに入るようにする。

モデルのアソシエーション

モデル.rb
  # userモデル
  has_many :images, dependent: :destroy    # has_manyに変更、複数形に変更
  accepts_nested_attributes_for :images   # 複数形に変更
  accepts_attachments_for :images, attachment: :images  # 複数形に変更

  # imageモデル
  attachment :image # カラムなので単数形のままでOK
  belongs_to :event

attachment :imageも複数形に変えないよう注意

コントローラーの記述

users_controller.rb
  def new
    @user = User.new
    @user.images.build  # 複数形になり記述が少し変更
  end

  def create
    # 先にuserインスタンスにparamsを入れる
    User.new(user_params)
    # 複数画像は配列で持っているため、1つ1つインスタンスに入れる
    params[:images_attributes][:"0"][:image].each do |image|
      @user.images.build(image)
    end
    User.save
  end

  def index
    @users = User.all.includes(:images)  # 複数形に変更。この呼び出し方が必須というわけではないです
  end

  # image_attributes:をストロングパラメーターから削除
  def user_params
    params.require(:user).permit(
      :name,
      :email
    )
  end

コントローラーのcreateアクションとストロングパラメーターについては、もっと良い書き方があるはず。
正直、params[:images_attributes][:"0"][:image]のストロングパラメーターでの取得の方法がわからなかったため、苦肉の策でストロングパラメーターからimageを消し、createアクションで直接paramsを渡している。

いろいろな書き方を試したが、ストロングパラメーターでは取得できなかった。
できる方法があれば教えていただけると助かります。

ビューファイルでの保存した画像の表示

ビューファイル.html.haml
# 複数画像は配列で保存されているため、1つ1つ取り出して表示
@user.images.each do |image|  # 「@user.images」と複数形に変更
  = attachment_image_tag image, :image, :fill, 200, 120, format: "jpg"

# 「attachment_image_tag image」 の「image」はeach文で指定した変数

複数画像の場合については以上。
これで複数画像のDB保存と表示ができるはず。

複数保存の問題点

上記のコードで一応複数画像についても、DB保存、表示が可能になった。
しかし問題がある。

元々fields_for.attachment_fieldは1つだけ書いてファイル選択フォームは1つしか無かったものが、例えば3画像保存し、編集しようとするとあら不思議、フォームが3つに増えています!

編集機能について現状わかっていること

| fields_forは紐づくcaptured_images全てに対して展開されるため、 |
| editアクションのviewにてfields_forが呼ばれるたびに、 |
| 選択された画像それぞれに対していちいち展開されてしまう点でした。 |

上記のリンク先でも記述されているが、複数保存したものをeditアクションなどで編集しようとすると、保存した画像の数だけファイル選択フォームが増えるという現象が起きる。
上記リンク先によるとどうもfields_forの仕様で、紐づくレコードが複数あるとその分だけフォームが呼び出されるらしい。

editアクションで表示するビューをnewアクションのビューと同じにしていると確実にフォームが増えてしまうので、編集の仕方をどうにか工夫しなければならない。
何か良い方法があったら教えていただけると助かります。

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

RailsでStripeを使って決済機能を実装する(仮払い機能付き)

サービスを開発していると決済機能をサービス内に実装したいことがよくあるかと思います。
今までちょくちょくstripeを利用してきたので、その経験を元に仮払い(オーソリ)込みの決済の実装方法を記述していこうと思います。

Stripeとは

Stripeとはインターネットビジネス内に組み込むことができる決済サービスの1つです。Stripeを使えば、自分が開発しているサービスにクレジットカードを使った決済機能を組み込むことができます。決済方法が銀行振込のみのサービスにはぜひ導入を検討してみてください。

stripeの手数料

stripeの手数料は決済毎に3.6%かかるのみです。導入費用や月ごとの利用料等は全て無料です。つまり、使った分だけ手数料がかかるという仕組みです。固定で発生する費用がないため、個人開発で売り上げがなかなか見込めない小さなサービスにもぴったりだと思います。

Railsプロジェクトにstripeを導入する

それではstripeをRailsに導入してみましょう。
stripeはgemを用意してくれているので、導入はただgemをbundle installするだけです。

Gemfile
  gem 'stripe'

stripeのgemを入れると、Rails内でstripeの様々なモデルを利用することができます。
単発の決済に利用するのは以下の2つのモデルです。

Stripe::Customer

決済の際にまずstripeを利用する顧客情報を登録します。
emailやsourceなどのパラメータを渡します。

Stripe::Charge

単発決済を行う際の決済情報を登録します。
決済金額(amount)や決済通貨(currency)などのパラメータを渡します。

Stripe::Refund

決済のキャンセルを登録します。
仮払いの状態であれば全額返金可能。本決済完了後であればstripeの手数料(3.6%)が引かれた96.4%の金額が返金されます。

Stripe Checkoutを実装する

それでは今回はAirbnbのような自分の部屋を貸すサービスを例にしてコードを書いていきたいと思います。
部屋の管理はRoomモデル、支払いの管理はPaymentモデルで行なっているとします。(1対1の関係)

まずはStripeのAPIキーをstripe.rbに入れておきます。

stripe.rb
#開発環境とテスト環境にはstripeのテストモードのAPIキーを使用する
if Rails.env.development? || Rails.env.test?
  Rails.configuration.stripe = {
    :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY'],
    :secret_key      => ENV['STRIPE_SECRET_KEY']
  }
end

#本番環境にはstripeの本番環境用APIキーを使用する
if Rails.env.production?
  Rails.configuration.stripe = {
    :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY_PRODUCTON'],
    :secret_key      => ENV['STRIPE_SECRET_KEY_PRODUCTION']
  }
end

#それぞれの環境に適したstripeAPIキーをセットしておく。
Stripe.api_key = Rails.configuration.stripe[:secret_key]

次にroom_controllerというコントローラーにstripeを実装していきます。

room_controller.rb
def create
    @room = Room.find(params[:room_id])

    begin

      ActiveRecord::Base.transaction  do
        @room.reserve = "仮予約"
        @room.save!
        #二重決済を防ぐためにすでにpaymentテーブルがユニークか調べる      
        raise "Already paid" if @room.payment.present? 
        #後でPaymentをnewする際に、万が一エラーが起こらないように、transactionの中でpayment_paramsの値を確認しておく
        raise "prameter error" unless params[:amount].present? && params[:stripeEmail].present? && params[:stripeToken].present?
      end

      ##############Stripe(決済)#########################################
        customer = Stripe::Customer.create(
          email: params[:stripeEmail],
          source: params[:stripeToken]
        )

        charge = Stripe::Charge.create(
          customer:       customer.id,
          amount:         params[:amount],
          description:    "「#{@room.name}」の決済",
          currency:       'jpy',
          receipt_email:  params[:stripeEmail],
          metadata: {'仮払い' => "1回目"},
          capture: false #capture:falseにすると仮払いで処理してくれる。
        )
      #####################################################################

      ###############決済記録を作成###################################################
        # stripeのcheckoutフォームから送られてきたパラメーターでpaymentのインスタンスを作成
        payment = Payment.new(payment_params)

        payment.email = customer.email # 支払った人がstripeのcheckoutフォームに入力したemail(支払い完了後、stripeからこのメールアドレスに支払い完了メールが送られる)
        payment.description = charge.description #決済の概要
        payment.currency = charge.currency  #通貨
        payment.customer_id = customer.id   # stripeのcustomerインスタンスのID
        payment.payment_date = Time.current # payment_date(支払いを行った時間)は現在時間を入れる
        payment.payment_status = "仮払い" # payment_status(この支払い)は仮払い状態(stripeのcaptureをfalseにしている)
        payment.uuid = SecureRandom.uuid  # 請求書の番号としてuuidを用意する
        payment.charge_id = charge.id  # 返金(refund)の時に使うchargeのIDをpaymentに保存しておく
        payment.stripe_commission = (charge.amount * 0.036).round  # stripeの手数料(3.6%)分の金額
        payment.amount_after_subtract_commission = charge.amount - payment.stripe_commission  # stripeの手数料(3.6%)分を引いた金額(依頼者が払った96.4%の金額)
        payment.receipt_url = charge.receipt_url  # この決済に対するstripeが用意してくれる領収書のURL
        payment.save!
      #############################################################################

      redirect_to temporary_complete_path #仮払い完了画面へ

    # stripe関連でエラーが起こった場合
    rescue Stripe::CardError => e
    flash[:error] = "#決済(stripe)でエラーが発生しました。{e.message}"
    render :new

    # Invalid parameters were supplied to Stripe's API
    rescue Stripe::InvalidRequestError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(InvalidRequestError)#{e.message}"
      render :new

    # Authentication with Stripe's API failed(maybe you changed API keys recently)
    rescue Stripe::AuthenticationError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(AuthenticationError)#{e.message}"
      render :new

    # Network communication with Stripe failed
    rescue Stripe::APIConnectionError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(APIConnectionError)#{e.message}"
      render :new

    # Display a very generic error to the user, and maybe send yourself an email
    rescue Stripe::StripeError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(StripeError)#{e.message}"
      render :new

    # stripe関連以外でエラーが起こった場合
    rescue => e
      flash.now[:error] = "エラーが発生しました#{e.message}"
      render :new
    end

  end

Paymentは領収書の役割を果たすクラスです。
gemのprownやwicked_pdfを使えば、簡単に領収書を作成できるかと思います。
ただ、stripeでは自動的に領収書が作成され、receipt_urlを使えば、作成された領収書のURLを引っ張ってくることもできます。

次にStripe Checkoutのフォームを作ります。

new.html.erb
<div>
    <%= form_tag room_path, id:"payForm" do %>

        <script src="https://checkout.stripe.com/checkout.js"></script>
        <%= hidden_field_tag 'stripeToken' %>
        <%= hidden_field_tag 'amount', @room.price %>
        <%= hidden_field_tag 'stripeEmail' %>
        <!-- 仮払いしたユーザーのID -->
        <%= hidden_field_tag 'user_id', current_user.id %>
        <!-- この仮払いのroomインスタンスを特定 -->
        <%= hidden_field_tag "room_id", @room.id %>

        <!-- 支払い済みであれば決済させない -->
        <% if @room.payment.present? %>
          <p class="btn btn-error">支払い済み</p>
        <% else %>
          <button id="btn-pay" type="button" class="btn btn-primary">仮払いする</button>
        <% end %>

        <script>
          // #ボタンを押した際のcheckoutのフォームはStripeCheckout.configureで設定する
            var handler = StripeCheckout.configure({
                //StripeのAPIキーを引っ張ってくる
                key: '<%= Rails.configuration.stripe[:publishable_key] %>',
                locale: 'auto', //言語の設定(autoの場合、ユーザのブラウザ規定言語が呼び出される)
                currency: 'jpy',
                // image: "image/directory", もしstripe checkoutのフォーム上部に画像を入れたい場合はここで指定する
                panelLabel: "{{amount}}のお支払い", //checkoutボタンの表示文字、{{amount}}の中に金額が入る
                allowRememberMe: false, //RememberMeを使いたい場合はここをtrueにする
                token: function(token,arg) { //ここでstripeTokenとstripeEmailに値を入れてsubmitする
                    document.getElementById('stripeToken').value = token.id;
                    document.getElementById('stripeEmail').value = token.email;
                    document.getElementById('payForm').submit();
                }
            });
            //Stripe Checkoutのフォームに表示される情報をここで指定する
            document.getElementById('btn-pay').addEventListener('click', function(e){
                handler.open({
                    name: '<%= @room.name %>',
                    description: '<%= @room.price %>円',
                    amount: document.getElementById("amount").value
                });
                e.preventDefault();
            })
        </script>
    <% end %>
</div>

これで仮払い機能の実装はできました。
あとはお部屋の貸し出しを大家さんがOKしてくれたら、本決済してあげるだけです。
つまり、capturedパラメータをtrueに変更してあげるだけです。(captureメソッドを使う)

room_controller.rb
def pay_complete
  room = current_user.rooms.find(params[:room_id])

  begin

    ActiveRecord::Base.transaction  do

      room.reserve = "予約完了" #予約状況
      room.save!
      room.payment.payment_status = "完了" #支払い状況
      room.payment.save!

      #仮払いをcaputureして決済を完了させる
      charge_id = room.payment.charge_id
      charge = Stripe::Charge.retrieve(charge_id) #retrieveメソッドで仮払いしたchargeインスタンスを引っ張ってくることができる
      charge.capture #captureメソッドを使うと、本決済となり、実際にクレジットカードから決済が行われる。
    end

    redirect_to  complete_path #本決済完了画面へ

  #stripe関連でエラーが起こった場合
  rescue Stripe::CardError => e
  flash.now[:error] = "決済でエラーが発生しました。#{e.message}"
  render :new

  # Invalid parameters were supplied to Stripe's API
  rescue Stripe::InvalidRequestError => e
    flash.now[:error] = "決済(stripe)でエラーが発生しました(InvalidRequestError)#{e.message}"
    render :new

  # Authentication with Stripe's API failed(maybe you changed API keys recently)
  rescue Stripe::AuthenticationError => e
    flash.now[:error] = "決済(stripe)でエラーが発生しました(AuthenticationError)#{e.message}"
    render :new

  # Network communication with Stripe failed
  rescue Stripe::APIConnectionError => e
    flash.now[:error] = "決済(stripe)でエラーが発生しました(APIConnectionError)#{e.message}"
    render :new

  # Display a very generic error to the user, and maybe send yourself an email
  rescue Stripe::StripeError => e
    flash.now[:error] = "決済(stripe)でエラーが発生しました(StripeError)#{e.message}"
    render :new
  ########################################################################################
  # 下のレスキューはstripe関係以外でのエラーが起こった時に発動する
  rescue => e
    flash.now[:error] = "エラーが発生しました#{e.message}"
    render :new
  end

stripeの場合、仮払いの状態が7日間続いたものは自動的にキャンセルされるようになっています。
7日間以上仮払いの状態を保ちたい場合は、rakeタスクにて一旦Stripe::Refundクラスを使って既存の仮払いをキャンセルし、再度Stripe::Chargeから仮払いを行うと良いかと思います。

さいごに

Stripeを使えば、自分のサービスに決済機能がつけられるので、サービス開発がさらに楽しくなりますね!

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

Railsからスプレッドシートを定時で書き換える

はじめに

サービスのKPIなどをスプレッドシートで管理することが多いとは思いますが、毎回データ取ってスプレッドシートに書き込んで、、をやってるとまあまあ時間の無駄なのでRailsアプリから直接スプレッドシートシートに書き込むようにしました。
加えてKPIの計測なので毎朝データを更新できるようにバッチも組むようにしました。

今回には以下を使っています。

  • Rails 5.2
  • gem 'google_drive'

実装

作るもの

Railsのデータベースから当月に会員登録したユーザーの数を日別で取得するようにします。
ユーザーは User モデルとします。

認証情報の取得

API経由でスプレッドシートを編集するために認証情報が必要なので Google Developers Console で情報を取得します。

プロジェクトの作成

ますはプロジェクトを作成します。
スクリーンショット 2019-07-07 11.54.03.png
スクリーンショット 2019-07-07 11.56.49.png

APIの有効化

続いてスプレッドシートのAPIの利用を有効化するためにライブラリからスプレッドシートを検索して有効化します。
スクリーンショット 2019-07-07 11.57.16.png
スクリーンショット 2019-07-07 12.01.27.png

認証情報の作成

まずはOAuth同意画面で操作します。アプリケーション等を埋めて保存します。
スクリーンショット 2019-07-07 12.07.03.png

つづいてOAuthクライアントを作成していきます。
スクリーンショット 2019-07-07 12.08.39.png
スクリーンショット 2019-07-07 12.09.17.png

作成したクライアントのリストから名前をクリックするとクライアントIDとクライアントシークレットを確認できます。
スクリーンショット 2019-07-07 12.10.00.png

リフレッシュトークンの取得

つづいて先程作成したクライアントの情報をもとにリフレッシュトークンを取得します。
書くの面倒くさくなってしまったのでこちらのやり方はこの記事を参考にしてみてください。

Rails側の実装

今回はもろもろの処理をジョブの中に書いていきます。

Google API を今回の処理以外でも使う可能性を加味して ApplicationJob に API の認証の処理を書き、他のジョブでも呼び出せるようにしました。(使う可能性を加味してというか、私の場合は複数使っているため)

app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base

  def google_drive
    credentials = Google::Auth::UserRefreshCredentials.new(
      client_id: ENV['KPI_SHEET_CLIENT_ID'],
      client_secret: ENV['KPI_SHEET_CLIENT_SECRET'],
      scope: %w(https://www.googleapis.com/auth/drive https://spreadsheets.google.com/feeds/),
      redirect_uri: 'http://storys.jp'
    )
    credentials.refresh_token = ENV['KPI_SHEET_REFRESH_TOKEN']
    credentials.fetch_access_token!
    GoogleDrive::Session.from_credentials(credentials)
  end
end

実際にスプレッドシートに書き込む処理は以下です。
ws[1, 1] はシートの1行目の1列目、ws[1, 2]は1行目の2列目です。
同じようにws[2, 1]は2行目の1列目になります。

app/jobs/export_kpi_job.rb
class ExportKpiJob < ApplicationJob

  def perform
    spreadsheet = google_drive.spreadsheet_by_key('スプレッドシートのキー')
    ws = spreadsheet.worksheet_by_title('該当のシートの名前')

    ws[1, 1] = ''
    ws[1, 2] = '新規登録ユーザー数'

    current_date = Date.current
    beginning_day_of_month = current_date.beginning_of_month
    end_day_of_month = current_date.end_of_month

    (beginning_day_of_month..end_day_of_month).each.with_index(2) do |date|
      ws[index, 1] = date
      ws[index, 2] = Users.where(created_at: date.beginning_of_day..date.end_of_month)
    end

    ws.save
  end
end

定時処理の設定

定時処理には gem 'whenever' を使います。
whenever自体の使い方はこちらを参考にしてください。

まずはタスクの作成

lib/tasks/rails_sample.rake
namespace :rails_sample do
  desc 'KPIをスプレッドシートに吐き出す'
  task :export_kpi => :environment do
    ExportKpiJob.perform_now
  end
end

今回は毎朝4時にタスクが走るようにします。

config/schedule.rb
every 1.day, at: '4:00 am', role: [:app] do
  rake 'rails_sample:export_kpi'
end

まとめ

以上でRailsからスプレッドシートを定時で書き換えるようにできました。
こういう小さな自動化を含めて、自分はもちろん他の人の仕事の効率を上げてあげることがエンジニアのできる貢献の1つかと思うので今後も続けたい所存。

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

[ruby]バッチ処理について

バッチ処理とは?

バッチ処理とは“一定量の(あるいは一定期間の)データを集め、一括処理するための処理方法”

私は、大量のデータを処理すること全般をイメージしてる。

引用元
https://www.imkk.jp/blog/what-is-batch-processing.html

バッチ処理のフロー

私の業務では、DBに一括登録するデータをスプレッドシートから取り組むバッチ処理でした。
以下では、データベースに商品を一括にスプレッドシートから取り込んで追加するという場合。

フローは以下の通り
まずrakeファイルにそのバッチ処理がどういったものなのかをdescにかく。
そのあと走らせる。
以下がそのコード

import_new_products_information.rake
namespace :batch do
    desc '新規商品の追加'
    task import_new_products_information: :environment do
        require 'batches/import_new_products_information'
        Batches::ImportNewProductsInformation.run
    end
end

走らせるファイル。
一応シート内に商品名と商品コードというカラムがあることが前提。

import_new_products_informations.rb
require 'batch/base'
require 'google_spreadsheet'

module Batches
  class ImportrProductsInformations < Batch::Base
    SPREADSHEET_KEY = '追加する商品のスプレッドシートキー'.freeze
    SHEET_NAME = '追加する商品一覧があるシートの名前 例)取り込み用など'.freeze

    def execute(_args)
      gs = GoogleSpreadsheet.new(SPREADSHEET_KEY)

      gs.spreadsheet.worksheet_by_title(SHEET_NAME).tap do |sheet|
        sheet.list.each do |row|
          products_informations.create!(
            product_name: row['商品名'],
            product_number: row['商品コード']
          )
        end
      end
    end
  end
end
google_spredsheet.rb
def worksheet_by_title(sheet_name)
    spreadsheet.worksheet_by_title(sheet_name)
 end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Thorのコマンドの中でrailsのActiveRecordを使う

以前railsのタスクの書式という記事を書きましたが、railsのタスクが引数がややこしくて、オプションもスマートに使えず余り好きではありません。

Thorでコマンドを書く方が好きなのですが、Thorの中でActiveRecordを使ってDBの操作が出来たのでメモしておきます。

以下はrailsアプリケーションの直下にthorというディレクトリを作りthor/foobarにコマンドがある想定のコードです。

railsのautoloadも効くのでthorをGemfileに追加します。

gem 'thor', '~> 0.20'

@scivola さんにコメントいただきましたがrailtiesがthorに依存しているのでGemfileには書かなくても読み込めます。ただし私の環境で試したところrequire 'thor'を書かないとuninitialized constant Thorで動きませんでした。

#!/usr/bin/env ruby

require_relative '../config/application'
Rails.application.initialize!

class Main < Thor
  desc "exec", "foobar"
  def exec
    binding.pry
  end
end

Main.start(ARGV)

これで

thor/foobar exec

で実行可能です。なんてことは無くてconfig/environment.rbのコードを丸っと持ってきただけです。なので

RAILS_ENV=production thor/foobar exec

で実行するとproduction環境で実行可能です。

spring経由で起動してないので起動が重いのが玉に傷です。やり方をちょっと調べてみて、ちなみにspring経由で起動するのにこんなタイムリーな記事があったのですが、残念ながら記事内のリンク先のgithubのリンクが死んでいてコードを見ることが出来ませんでした。spring-commands-rspecも参考になると思ってみてみましたが、私がspringの仕組み自体全く理解してないのでイマイチでよく分かりませんでした。その内調べて分かったらまた共有します。何かご存じの方いらっしゃいまたらヒントでも結構ですのでコメントお願いします!

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

複数のherokuアカウント上のRailsアプリを管理・デプロイするための情報まとめ

一台のデバイスで複数のherokuアカウントを管理・デプロイするための情報

余裕があるときに随時加筆修正します。

heroku で複数アカウントを管理するCLIのプラグインを追加する

$ heroku plugins:install heroku-accounts
$ heroku accounts:add any_account_name
$ heroku accounts:set added_account_name

heroku のssh鍵を追加する

heroku keys:add
# 自動で公開健がherokuにupされる

heroku にRailsアプリケーションをデプロイする

herokuにアプリケーションを作成する

heroku create app_name

app_nameは省略可能
アプリ名を省略した場合、heroku側で自動に生成される

この時自動でremoteのリポジトリ名がherokuに設定される。
もし、違う名前にしたい場合は、

heroku git:remote -a app_name -r <any_repository_name>

以降、pushするときは

git push <any_repository_name> master

Rails アプリケーションの設定

Railsアプリケーションのデータベースをsqlite3からpostgreSQLに変更する。

TIPS

herokuアプリケーションを削除する

herokuのデプロイに失敗したとき、作成したアプリケーションをいったん削除したい場合がある。

その場合は

heroku apps:destroy --app app_name

を実行すればいい。

herokuにRailsアプリケーションをデプロイしたとき、rake taskに失敗するときの対処法

考慮した可能性
- assets precompile関係
- bundler関係 〇
- gem関係
- その他

bundler 2.0.1を利用したrailsアプリケーションがherokuにデプロイできなくなったので原因を探ってみたらbundler 2.0.2対応が原因らしい。

つい最近まで、bundler のバージョンが2.0.1でも問題なく動作したが、おそらく2019年6月13日にリリースされたbundler 2.0.2に対応した影響のためか、2.0.1だと

could not find bundler(2.0.1) required by your /tmp/build_xxxxxx....xxxxx/Gemfile.lock

とエラーが出る。

この場合、まずローカル環境にあるGemfile.lockを一度削除したのちに、以下のコマンドを打ち

gem install bundler -v 2.0.2

bundler(2.0.2)をインストールし、bundle install (--path vendor/bundle)、bundle updateする。

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

Rails6 のちょい足しな新機能を試す48(Duration編)

はじめに

Rails 6 に追加されそうな新機能を試す第48段。 今回は、 Duration 編です。
Rails 6 では、 Duration の計算で小数点以下が丸められることが無くなりました。

Ruby 2.6.3, Rails 6.0.0.rc1, Rails 5.2.3 で確認しました。Rails 6.0.0.rc1 は gem install rails --prerelease でインストールできます。

$ rails --version
Rails 6.0.0.rc1

簡単なスクリプトを作る

今回は、動作確認用の簡単なスクリプトを書いて確認します。

bin/duration.rb
duration = 0.4.seconds

d = DateTime.parse('2019-01-01')

d += duration
d += duration
d += duration
d += duration
d += duration

p d > DateTime.parse('2019-01-01')
p d == DateTime.parse('2019-01-01')

Rails 5 では

0.4.seconds を足すとき 整数に丸められて 0.seconds として加算されてしまいます。

結果、 0.4.seconds を5回足す前も後も変化がありません。

$ bin/rails runner bin/duration.rb
false
true

Rails 6 では

整数に丸められることが無くなったので、より正確な値が得られるようになります。

$ bin/rails runner bin/duration.rb
true
false

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try048_duration

参考情報

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

ブロックとスコープを使いこなしたい【メタプログラミングRuby】

はじめに

この記事ではブロックって何?からinstance_evalってやつすげー!までやります。
ブロックとスコープの知識をスッキリさせたい方にはぴったりだと思っています。

ブロックとは

Rubyでは、do ~ end、または{ ~ }の処理の塊のことをブロックと言います。

一行で記述されるブロックは{ ~ }を使い、複数行のブロックにはdo ~ endを使う慣習があります。

ブロックを定義できるのは、メソッドを呼び出す時だけです。
ブロックはメソッドに渡され、メソッド内でyieldキーワードを使ってブロックをコールバックできます。

block.rb

def my_method(a, b)
  a + yield(a, b)
end

p my_method(1, 2){ |x, y| x + y } # => 4

ブロックの基本的な説明はここまでにして、早速内容に入っていきましょう。

内容

ブロックと束縛

ブロックを呼び出すためには、ローカル変数、インスタンス変数、selfといった環境が必要になります。
このオブジェクトに紐づけられている環境の事を束縛とも言います。

つまり、ブロックとはコードと束縛両方の集まりという事になります。

ブロックにおいて、コードはブロック内にあるコードを使用し、束縛はブロックを定義した場所にある束縛を使用します。

そして、ブロックをメソッドに渡した時は、その束縛も一緒に運ばれていきます。
メソッドにある束縛はブロックからは見る事が出来ません。

blocks.rb
def my_method
  my_var = "my_methodの変数"
  yield
end

my_var = "トップレベルの変数"
p my_method { my_var } # => "トップレベルの変数"

ブロックと束縛を理解するためには、スコープに関する知識を深める必要があります。

スコープゲート

スコープゲートとは、スコープが変化する境界線の事です。(スコープに関しての基本的な説明は省きます)
Rubyでは、下記の3つがスコープゲートになります。

  • クラス定義
  • モジュール定義
  • メソッド

実際に下記のコードを見ながら確認していきましょう。

scope_gate.rb
top_level_var = "トップレベルの変数"
local_variables # => [:top_level_var, :obj]

class MyClass #クラス定義
  my_class_var = "マイクラスの変数"
  local_variables # => [:my_class_var]

  def my_method # メソッド定義
    my_method_var = "マイメソッドの変数"
    local_variables # => [:my_method_var]
  end

  local_variables # => [:my_class_var]
end

local_variables # => [:top_level_var, :obj]

このような感じですね。

それぞれのスコープ内で束縛が変わっている事が確認できると思います。
思わぬ所で、予期せぬ変数が参照されるエラーが発生し難くなり便利ですね。

しかし、Rubyを使っていると、スコープゲートを超えて束縛を渡したい局面に遭遇する事があります。

そういう時はフラットスコープを使っていきましょう。

スコープゲートを超えて束縛を渡す(フラットスコープ)

クラス定義ではなく、メソッド呼び出しを使用する事でスコープゲートを超えることが可能になります。
(正確には、スコープゲートを超えているわけではなく、スコープゲートを使用していないだけです)

先程のscope_gate.rbを書き換えていきましょう。
もしMyClass内でtop_level_varを使いたい時はこのように書きます。

scope_gate.rb
top_level_var = "トップレベルの変数"
local_variables # => [:top_level_var, :obj]

MyClass = Class.new do #クラス定義
  my_class_var = "マイクラスの変数"
  local_variables # => [:my_class_var, :top_level_var, :obj]

  def my_method # メソッド定義
    my_method_var = "マイメソッドの変数"
    local_variables # => [:my_method_var]

  end
  local_variables # => [:my_class_var, :top_level_var, :obj]
end

local_variables # => [:top_level_var, :obj]

my_method内でmy_class_varを使いたい場合も、先程と同じように、メソッド呼び出しに書き換えます。

scope_gate.rb
top_level_var = "トップレベルの変数"

class MyClass #クラス定義
  my_class_var = "マイクラスの変数"
  local_variables # => [:my_class_var]

  define_method :my_method do # メソッド定義
    my_method_var = "マイメソッドの変数"
    local_variables # => [:my_method_var, :my_class_var]

  end
  local_variables # => [:my_class_var]
end

local_variables # => [:top_level_var, :obj]

技術的には、この方法の事を入れ子構造のレキシカルスコープと呼びます。
しかしRubyに限ってはフラットスコープと呼ばれる事が多いようです。

フラットスコープを応用すれば、自由自在にスコープを組み合わせる事ができます。
共有スコープという方法もありますが、上記の2つ(スコープゲート、フラットスコープ)を理解していれば自然と使えるようになる方法なので省きます。(気になる方は調べてみてください)

コードと束縛を好きなように組み合わせる(instance_eval)

instance_evalというメソッドがあります。

instance_evalに渡したブロックは、オブジェクトのコンテキストで評価されます。
つまり、レシーバがselfにしてから評価されるので、レシーバのprivateメソッドやインスタンス変数などにもアクセスが可能になるという事です。

また、他のブロックと同じように、instance_evalを定義した時の束縛も見ることが出来ます。

それでは、コードを見ていきましょう。

instance_eval.rb
top_level_var = "トップレベルの変数"

class MyClass
  def initialize
    @my_instance_var = "マイクラスのインスタンス変数"
  end
end

my_class = MyClass.new

my_class.instance_eval do
  self # => #<MyClass:0x00007fc4b08bdcc0 @my_instance_var="マイクラスのインスタンス変数">
  top_level_var # => "トップレベルの変数"
  @my_instance_var # => "マイクラスのインスタンス変数"
end

このような感じです。

instance_evalに渡したブロックのことは、インスタンス探査機と呼ばれます。
オブジェクトの内部を探査して、そこで実行するコードだからです。

まとめ

ブロックを上手く扱うためには、束縛を理解する必要がありました。
そして、束縛の住処であるスコープも学習しました。
instance_evalはおまけのような感じです。

気になる所や、間違っている所等ありましたらコメントください!!

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

ActiveRelationでdefault値を設定する方法

前提知識
RailsではActiveRecordがORマピイングシステムでDBとやりとりをしている。

ORマッピング
 railsで決められている方法で記述するだけで、データベースの種類に関係なく、データを扱える仕組み。
利点
・DBの種類毎に変わる記述方法を気にしなくて済む

migration file
DBに反映させたい情報を記載したfile

rails db:migrateを実行すると
timeスタンプの順番に沿って各fileの内容が実行される。

schema
DBへ反映させるための情報が記載されたfile
migration fileの情報を反映させたのがschema
schemaがDBと直接やりとりしている。

使い方。
ターミナルで以下のコマンドを実行する。
rails generate model モデル名 カラム名:その型

モデル名は単数形で先頭は大文字
カラム名:その型、は複数指定できる(書かなくても良い)

カラム名:その型を1つも指定しない場合
ex. ) rails generate model User

作成されるmigration file
スクリーンショット 2019-07-07 13.39.57.png

スクリーンショット 2019-07-07 21.46.05.png

次に
rails db:migrate
を実行する。schemaに以下の様に記述される。

スクリーンショット 2019-07-07 13.40.03.png

カラム名:その型1以上指定した場合
ex. ) rails generate model Player name:string age:integer height:integer

作成されるmigrationfile

次に
rails db:migrate
を実行する 
スクリーンショット 2019-07-07 21.52.07.png

shemaが更新されます。
名前がplayersのテーブルが作られ、カラムには以下の三つが存在する。
名前にstring型、年齢にinteger型、高さにinteger型を持つカラム

スクリーンショット 2019-07-07 13.45.31.png

さて、本題に入りましょう
大前提として、ターミナルで打つコマンドからはdefault値を指定できません。じゃあ、どうするの?
migration fileをいじくり、それをmigarateすることでdefault値を設定します。
default値を設定したいタイミングは以下の通りが考えられるのでそれに沿って説明します。今回はadminにdefaul値を設定します。
・テーブルまたはカラムをまだ作成していない時
・既存のdefault値を変更する
・既に存在するカラムにdefault値を設定する

  1. テーブルまたはカラムをまだ作成していない時 rails generate migration User name:string age:integer height:integer admin:string を実行すると以下のmigration fileが作成されます。 スクリーンショット 2019-07-07 18.24.50.png

adminの後にdefault: faulsを設定してあげます。
スクリーンショット 2019-07-07 18.25.53.png

後はmigrateを実行すれば完成です。
rails db:migrate
schemaにdefaultが作られていることを確認する。
スクリーンショット 2019-07-07 18.30.07.png

rollbackが可能(この場合はtableを作る作業の逆、つまり削除が実行される。)
2,・既存のdefault値を変更する
現状は以下の様になっている。
scheme
スクリーンショット 2019-07-07 18.30.07.png

ここから、trueをfalseに変更したい。
rails generate migration change_column_cats
を実行する。そうすると、以下のmigration fileが作成される。

migration file
スクリーンショット 2019-07-07 18.35.20.png

ここにこう書き足す。
スクリーンショット 2019-07-07 21.33.26.png

rails db:migrateを実行する。
スクリーンショット 2019-07-07 21.33.40.png
adminのdefault値がfalseからtrueになってるのが確認できる。

ただし、この書き方だとrollbackすることができません。
試しにしてみると、、、
スクリーンショット 2019-07-07 21.34.43.png

この様に怒られます。
rollbackは元の状態がわかる必要があります。そのため、以下の様に記述を変えてあげるとかぎゃくせいを持たせる事ができます。
スクリーンショット 2019-07-07 21.58.33.png

以下は時間がある時に追加します。
3.既に存在するカラムにdefault値を設定する

1、2の組み合わせで解決できます。
rollbackを気にしない時

rollbackを気にする時
SQLでのカラムのdefault値はnullになっています。なので、何も指定しない状態だとfrom:〜〜 to:falseの〜〜部分はnilに設定してあげればOKです。

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

railsのcredentials.yml.encとRAILS_MASTER_KEY

RAILS_MASTER_KEYってなんだ?

credentials.yml.encをつかう際に、デコードするためのキー

credentials.yml.encってなんだ?

rails5.2より前のバージョンでは、secrets.ymlにて平文で情報を扱っていた。(secretsなのに。)
環境変数を暗号化して入れられるということですね。

これまでsecrets.ymlは環境ごとに分割して使われていましたが
credentials.yml.encは本番環境のみで使われる想定のものだそうです。

使い方

Rails.application.credentials.hogehoge

で情報を取得する事ができます。

参照
Rails5.2からsecrets.yml*が廃止されcredentials.yml.encに統合されるよ

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

amcharts 4 Demos を使ってグラフを作成

About

amcharts関連の日本語文献のあまりの少なさから、誰かのお役に立てればと思い記載しています!

最終的には、このようなグラフを作成することができます。
7d0de671ad5679b2edef0a20c7ab9b06.gif

Environment

この記事ではmacbook(unix)にインストールしたruby 2.5.1p57, Rails 5.2.3を使っています。

amchartsとは

amcharts
javascriptでチャートを描画するためのフレームワークです。 棒、エリア、列、バー、パイ、XY、散布、ローソク足のようなチャートを描画することが出来ます。

(参考文献)
http://mikawatan.hatenablog.com/entry/2016/10/09/171130

Demosとは

amchartsが提供しているサンプルの一覧です。
実際にご覧いただいた方が早いと思います。下記よりご確認ください。
https://www.amcharts.com/demos/

使用方法

HTML内でResources(CDN)を記載することで利用できます。
商用利用の場合も、チャート内のamchartsアイコン(amchartsへのリンク)を削除しなければそのまま使用可能です。

(参考文献)
https://www.amcharts.com/online-store/
Free license
"Use anywhere you want as long as you don't mind a small amCharts attribution on charts"

Rails環境ですが、Demoをわかりやすく、そのまま使用するために、
今回は各viewファイル内に直接記載していきます。

index.html.erb
<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>

これで準備完了です。

導入方法

今回は、こちらのチャートを扱います。
ページ内で下にスクロールすると、Demo sourceが置いてありますので、コピーします。

Demo_source
<!-- Styles -->
<style>
#chartdiv {
  width: 100%;
  height: 500px;
}
</style>

<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>

<!-- Chart code -->
<script>
am4core.ready(function() {

// Themes begin
am4core.useTheme(am4themes_animated);
// Themes end

// Create chart instance
var chart = am4core.create("chartdiv", am4charts.XYChart);
chart.scrollbarX = new am4core.Scrollbar();

// Add data
chart.data = [{
  "country": "USA",
  "visits": 3025
}, {
  "country": "China",
  "visits": 1882
}, {
  "country": "Japan",
  "visits": 1809
}, {
  "country": "Germany",
  "visits": 1322
}, {
  "country": "UK",
  "visits": 1122
}, {
  "country": "France",
  "visits": 1114
}, {
  "country": "India",
  "visits": 984
}, {
  "country": "Spain",
  "visits": 711
}, {
  "country": "Netherlands",
  "visits": 665
}, {
  "country": "Russia",
  "visits": 580
}, {
  "country": "South Korea",
  "visits": 443
}, {
  "country": "Canada",
  "visits": 441
}];

// Create axes
var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
categoryAxis.dataFields.category = "country";
categoryAxis.renderer.grid.template.location = 0;
categoryAxis.renderer.minGridDistance = 30;
categoryAxis.renderer.labels.template.horizontalCenter = "right";
categoryAxis.renderer.labels.template.verticalCenter = "middle";
categoryAxis.renderer.labels.template.rotation = 270;
categoryAxis.tooltip.disabled = true;
categoryAxis.renderer.minHeight = 110;

var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.renderer.minWidth = 50;

// Create series
var series = chart.series.push(new am4charts.ColumnSeries());
series.sequencedInterpolation = true;
series.dataFields.valueY = "visits";
series.dataFields.categoryX = "country";
series.tooltipText = "[{categoryX}: bold]{valueY}[/]";
series.columns.template.strokeWidth = 0;

series.tooltip.pointerOrientation = "vertical";

series.columns.template.column.cornerRadiusTopLeft = 10;
series.columns.template.column.cornerRadiusTopRight = 10;
series.columns.template.column.fillOpacity = 0.8;

// on hover, make corner radiuses bigger
var hoverState = series.columns.template.column.states.create("hover");
hoverState.properties.cornerRadiusTopLeft = 0;
hoverState.properties.cornerRadiusTopRight = 0;
hoverState.properties.fillOpacity = 1;

series.columns.template.adapter.add("fill", function(fill, target) {
  return chart.colors.getIndex(target.dataItem.index);
});

// Cursor
chart.cursor = new am4charts.XYCursor();

}); // end am4core.ready()
</script>

<!-- HTML -->
<div id="chartdiv"></div>

お気づきでしょうか?既に使用方法欄で記載した内容は、こちらのDemo sourceに必ず記載されているものです。なので、コピーをそのまま利用される方は、CDNを記載する必要はありません。

それでは、コピーした内容をそのままhtmlにペーストします。

index.html.erb
<!--コピー内容をそのままペースト-->

以上でチャートの描画ができました。
b9e9abae191c2a130c75d49593f3db76.gif

編集方法

このままでは当然ですが、サンプルデータそのままになってしまいます。
各機能について(理解している部分)を記載していきます。

Styles

cssです。フォントのサイズ等を指定できますが、指定できる項目には限りがあります。

index.html.erb
<!-- Styles -->
<style>
  #chartdiv {
    width: 100%;
    height: 500px;
    font-size: 20px;
   }
</style>

chart code

本体です。以下、コメントアウトにて詳細を記載します。

index.html.erb
<!-- Chart code -->
<script>
am4core.ready(function() {

// Themes begin
// テーマです。
am4core.useTheme(am4themes_animated);
// Themes end

// Create chart instance
//始めにインスタンスを作成しています。
var chart = am4core.create("chartdiv", am4charts.XYChart);
//X軸方向のスクロールバーです。私はいらなかったのでコメントアウトしました。
chart.scrollbarX = new am4core.Scrollbar();

// Add data
// ここにデータを入力していきます。
chart.data = [{
  "country": "USA",
  "visits": 3025
}, {
  "country": "China",
  "visits": 1882
}, {
  "country": "Japan",
  "visits": 1809
}, {
  "country": "Germany",
  "visits": 1322
}, {
  "country": "UK",
  "visits": 1122
}, {
  "country": "France",
  "visits": 1114
}, {
  "country": "India",
  "visits": 984
}, {
  "country": "Spain",
  "visits": 711
}, {
  "country": "Netherlands",
  "visits": 665
}, {
  "country": "Russia",
  "visits": 580
}, {
  "country": "South Korea",
  "visits": 443
}, {
  "country": "Canada",
  "visits": 441
}];

// Create axes
//ここからはX軸方向について
var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
//データフィールドのカテゴリーです。今回はcountryですね。
categoryAxis.dataFields.category = "country";
//この辺りはあまり手を加える必要はないかと思います。
categoryAxis.renderer.grid.template.location = 0;
categoryAxis.renderer.minGridDistance = 30;
categoryAxis.renderer.labels.template.horizontalCenter = "right";
categoryAxis.renderer.labels.template.verticalCenter = "middle";
//文字の向きです。このままだと270度回転して縦方向の文字列になりますので、
//横向き希望の方は0に設定すると良いです。
categoryAxis.renderer.labels.template.rotation = 270;
categoryAxis.tooltip.disabled = true;
categoryAxis.renderer.minHeight = 110;

//ここからはY軸方向について
var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.renderer.minWidth = 50;

// Create series
// ようやくチャートの設定です。上記のデータフィールドの項目を使用します。
var series = chart.series.push(new am4charts.ColumnSeries());
series.sequencedInterpolation = true;
// Y軸方向はvisits
series.dataFields.valueY = "visits";
// X軸方向はcountry
series.dataFields.categoryX = "country";
// tooltipはグラフにカーソルを当てると上部に表示される内容です。
series.tooltipText = "[{categoryX}: bold]{valueY}[/]";
series.columns.template.strokeWidth = 0;

series.tooltip.pointerOrientation = "vertical";

series.columns.template.column.cornerRadiusTopLeft = 10;
series.columns.template.column.cornerRadiusTopRight = 10;
series.columns.template.column.fillOpacity = 0.8;

// on hover, make corner radiuses bigger
// ホバーした際の設定です。
var hoverState = series.columns.template.column.states.create("hover");
hoverState.properties.cornerRadiusTopLeft = 0;
hoverState.properties.cornerRadiusTopRight = 0;
hoverState.properties.fillOpacity = 1;

series.columns.template.adapter.add("fill", function(fill, target) {
  return chart.colors.getIndex(target.dataItem.index);
});

// Cursor
// カーソル設定です。非表示にしたい場合は、以下のコメントアウトのように記載します。
chart.cursor = new am4charts.XYCursor();
// chart.cursor.lineX.disabled = true;
// chart.cursor.lineY.disabled = true;

}); // end am4core.ready()
</script>

HTML

最後に、HTMLを記載します。

index.html.erb
<!-- HTML -->
<div id="chartdiv"></div>

リンクの貼り方

chartにリンクを貼りたい!という方、必見です。
下記の様な記載で実装できます。

index.html.erb
<script>
//  データの項目にidを追加
chart.data = [{
  "country": "USA",
  "visits": 3025,
  "id": 0
}, {
  "country": "China",
  "visits": 1882,
  "id": 1
}, {
  "country": "Japan",
  "visits": 1809,
  "id": 2
}, {
  "country": "Germany",
  "visits": 1322,
  "id": 3
}, {
  "country": "UK",
  "visits": 1122,
  "id": 4
}];

// script内で下記記載。最後の{}内の記載方法でid毎のページを呼び出せます。
series.columns.template.url =
      `http://hoge/users/huga/sleeps/{id.urlEncode()}`;

実装例

最後に、私が作成した"sommeil"というアプリケーションのコードの内、一部のamcharts部分を参考までに記載します。

こちらのコードでは、オリジナル要素としてチャートのグラフ内から詳細ページに飛べる仕組みを導入しています。
この部分に関して、いくらGoogle先生に聞いても出てこなかったので、記事を執筆しました...。

全編はgithubをご覧下さい。
実装イメージについては、グローバルIPで恐縮ですが公開中です。(sommeil)

_chart2.html.erb
</style>
<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
<!-- Chart code -->
<script>
  am4core.ready(function () {
    // Themes begin
    am4core.useTheme(am4themes_animated);
    // Themes end
    // Create chart instance
    var chart = am4core.create("chartdiv", am4charts.XYChart);
    //chart.scrollbarX = new am4core.Scrollbar();
    // Add data
    var sleeping_times = '<%= @sleeping_times %>'.replace(/\[/g, "").replace(/\]/g, "").split(',');
    var dates = '<%= @dates %>'.replace(/\[/g, "").replace(/\]/g, "").replace(/\s/g, "").split(',');
    var sleep_ids = '<%= @sleep_ids %>'.replace(/\[/g, "").replace(/\]/g, "").replace(/\s/g, "").split(
      ',');
    console.log(sleep_ids);
    chart.data = [{
      "day": dates[0],
      "time": sleeping_times[0],
      "id": sleep_ids[0]
    }, {
      "day": dates[1],
      "time": sleeping_times[1],
      "id": sleep_ids[1]
    }, {
      "day": dates[2],
      "time": sleeping_times[2],
      "id": sleep_ids[2]
    }, {
      "day": dates[3],
      "time": sleeping_times[3],
      "id": sleep_ids[3]
    }, {
      "day": dates[4],
      "time": sleeping_times[4],
      "id": sleep_ids[4]
    }, {
      "day": dates[5],
      "time": sleeping_times[5],
      "id": sleep_ids[5]
    }, {
      "day": dates[6],
      "time": sleeping_times[6],
      "id": sleep_ids[6]
    }, {
      "day": dates[7],
      "time": sleeping_times[7],
      "id": sleep_ids[7]
    }, {
      "day": dates[8],
      "time": sleeping_times[8],
      "id": sleep_ids[8]
    }, {
      "day": dates[9],
      "time": sleeping_times[9],
      "id": sleep_ids[9]
    }, {
      "day": dates[10],
      "time": sleeping_times[10],
      "id": sleep_ids[10]
    }, {
      "day": dates[11],
      "time": sleeping_times[11],
      "id": sleep_ids[11]
    }, {
      "day": dates[12],
      "time": sleeping_times[12],
      "id": sleep_ids[12]
    }, {
      "day": dates[13],
      "time": sleeping_times[13],
      "id": sleep_ids[13]
    }];
    // Create axes
    var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
    categoryAxis.dataFields.category = "day";
    categoryAxis.renderer.grid.template.location = 0;
    categoryAxis.renderer.minGridDistance = 30;
    categoryAxis.renderer.labels.template.horizontalCenter = "middle";
    categoryAxis.renderer.labels.template.verticalCenter = "middle";
    categoryAxis.renderer.labels.template.rotation = 0;
    categoryAxis.renderer.labels.template.fill = am4core.color("#fff");
    categoryAxis.tooltip.disabled = true;
    categoryAxis.renderer.minHeight = 110;
    var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
    valueAxis.renderer.minWidth = 50;
    valueAxis.renderer.labels.template.fill = am4core.color("#fff");
    // Create series
    var series = chart.series.push(new am4charts.ColumnSeries());
    series.sequencedInterpolation = true;
    series.dataFields.valueY = "time";
    series.dataFields.categoryX = "day";
    series.tooltipText = "[{categoryX}: bold]{valueY}[/]";
    series.columns.template.strokeWidth = 0;
    series.tooltip.pointerOrientation = "vertical";
    //Go to detail page
    var current_user_id = '<%= @user.id %>'
    series.columns.template.url =
      `http://52.194.48.235/users/${current_user_id}/sleeps/{id.urlEncode()}`;
    series.columns.template.column.cornerRadiusTopLeft = 10;
    series.columns.template.column.cornerRadiusTopRight = 10;
    series.columns.template.column.fillOpacity = 0.8;
    // on hover, make corner radiuses bigger
    var hoverState = series.columns.template.column.states.create("hover");
    //hoverState.properties.cornerRadiusTopLeft = 0;
    //hoverState.properties.cornerRadiusTopRight = 0;
    hoverState.properties.fillOpacity = 1;
    series.columns.template.adapter.add("fill", function (fill, target) {
      return chart.colors.getIndex(target.dataItem.index);
    });
    // Cursor
    chart.cursor = new am4charts.XYCursor();
    chart.cursor.lineX.disabled = true;
  }); // end am4core.ready()
</script>
<!-- HTML -->
<div id="chartdiv"></div>

最後に

合わせてpiechartの作成方法なども近日公開予定です。
ご覧いただき、ありがとうございました。

筆者について

TECH::EXPERTにて4月よりruby, railsを学習している未経験エンジニアです。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!

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

capistrano3でpumaの運用ー設定ファイルの管理

railsユーザの多くがつかってるのではないかと思われるcapistranoとpuma。この組み合わせで、どうやってpuma起動時に任意の設定を指定するか、意外とわけわからんのでまとめた。

pumaのデフォルト仕様

なにも指定せずにpuma起動すると、 config/puma/{env}.rb, config/puma.rb, の優先順位で設定ファイルが読まれる。
puma/configuration.rb at 0cd28f373a245eca4e717bff1415b97bbc2356d2 · puma/puma · GitHub

ちなみに、ローカル環境などでrails server したときはどうも、puma, thin, webrickの順にrackハンドラを探して見つかったやつで起動するっぽい。最近のrailsではrails newするとpumaが入るので、特にいじらないかぎりrails serverしたときにはpumaが起動すると思っていい。
rack/handler.rb at 2.0.7 · rack/rack · GitHub

% rails server
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...

つまり、ローカルで起動するpumaの挙動を変更したい場合は、config/puma/development.rbをつくるか、config/puma.rbをつくってrails server起動すればいい。

capistranoをつかう場合

しかし、capistrano3-pumaをデフォルト設定でつかう場合、config/puma/{env}.rb, config/puma.rbは完全無視される。1
代わりに、capistrano3-pumaが持つ設定ファイル生成機能(後述)をもとに新規設定ファイルを作成し、デプロイサーバにアップロードし、それをpuma起動時に読むようになる。

ここでcapistrano & pumaの設定ファイルの運用方針が、大きく2つの道にわかれる。

pumaオリジナルconfig/puma/{env}.rb, config/puma.rbで運用する

capistrano3-pumaが提供する設定ファイル機能をつかわずに、従来のpumaの運用方法でやる方法。設定を緻密に詳細に指定したいときはこっちのほうがたぶんやりやすい。

必要な設定を逐一記述して、リポジトリにpushしておく。2
あとは、deploy.rbまたはdeploy/{env}.rbに、設定ファイルのパスpuma_confを設定すればいい。
Why it builds a new puma.config instead of using config/puma/production.rb? (question) · Issue #244 · seuros/capistrano-puma · GitHub

具体的にはこういう設定。

set :deploy_to, '/path/to/app'
set :puma_conf, "#{current_path}/config/puma/staging.rb"

# `current_path`の値は`deploy_to`によって決まるので、
# `puma_conf`は`deploy_to`よりあとに指定する点に注意!

こうすると、capistrano経由でpumaを起動するときに、-C /path/to/app/current/config/puma/staging.rbが指定されてくれる。

% bundle exec cap staging puma:start
00:00 puma:start
      using conf file /path/to/app/current/config/puma/staging.rb
      01 bundle exec puma -C /path/to/app/current/config/puma/staging.rb --daemon
...

capistrano3-pumaの設定ファイル生成機能をつかって運用する

capistrano3-pumaには設定ファイル生成機能puma:configがある。pumaの設定をがっちりかけなくてもそれなりの設定で動いてくれる。

cap {env} deploy:checkが実行されるときに、存在しなければ/path/to/app/shared/puma.rb(またはpuma_confに指定されたパス)を自動生成して各サーバにアップロードする。3具体的にはこのとき、cap {env} puma:configというタスクが実行されている。
cap {env} puma:configを単体で実行すると、既存ファイルが存在するしないに関係なくcapistrano管理上の設定で上書きされる

ふだんcapistrano運用者がつかうコマンドはcap {env} deploy(内部的にdeploy:checkを実行する)のはずなので、設定ファイルははじめの一回だけ自動生成され、その後都度更新されることはない。
だから、サーバ上の設定を手動で直接かきかえる運用も可能。ただし、そういう運用をおこなう場合は、cap {env} puma:configの実行に注意。手動で加えた設定は消されてしまうので。

% bundle exec cap staging puma:config
00:00 puma:config
      Uploading /path/to/app/shared/puma.rb 100.0%

# /path/to/app/shared/puma.rb
# を確認すると、設定ファイルが追加されてたり、上書きされてるのがわかる

# /path/to/app/shared/puma.rb
# ちなみに、このパスを変えたい場合は、前述した要領で`puma_conf`を指定すればいい

capistrano管理上のpuma設定を上書きする

cap {env} puma:configが実行されたときに生成されるファイルは、デフォルトでは以下に定義された値がコピーされる。

capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub

これらの設定値はもちろんcapistrano設定で全部上書きできる。
config/deploy.rbconfig/deploy/{env}.rbに以下のように記述する。(すでに紹介したpuma_confの上書きと同じ要領)

set :puma_env, 'staging'
set :puma_threads, [4, 4]
set :puma_workers, 0
set :puma_preload_app, false

設定可能な設定値はこちらのリストも参考
GitHub - seuros/capistrano-puma: Puma integration for Capistrano

念のためもう一度言うと、ここで設定した設定値はpuma起動時に直接読まれるわけではない。puma:configが実行されて、shared/puma.rb(またはpuma_confで指定されたパス)ファイルにその値がハードコードされ、それが起動時(cap {env} puma:start)に読み込まれることによってpumaに渡る。
つまり、puma_threads, puma_workersなどのcapistrano管理上のpuma設定は、cap {env} puma:configが実行されてはじめてリリースされる。

capistrano管理上のpuma設定をプログラムする

ここまで見ると、設定ファイル生成機能はライトにつかえる。しかしpumaのチューニングに余念がない人々にとっては、これだけでは足りない点がある。

puma設定には、設定の「値」だけではなく、「プログラム」がかけるのである。たとえば、pumaプロセスがフォークしたときのコールバックなどをカスタム設定できる仕組みになっている。
GitHub - puma/puma: A Ruby/Rack web server built for concurrency

こういうかんじで。

on_worker_boot do
  # configuration here
end

before_fork do
  # configuration here
end

こういうことをやりたくなったら4、はじめのpumaオリジナル設定ファイルの運用に切り替えるのをおすすめするが、一応設定ファイル生成機能をつかってもできる。

cap {env} puma:configが実行されるときに、生成される設定ファイルのテンプレートがこれ。
capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHub

つまり、このテンプレートを差し替えることができれば、設定だけじゃなくてプログラムも自由にかける。
上記のテンプレートを参考に、config/deploy/templatesまたはlib/capistrano/templates以下にカスタムテンプレートをおけばいい。これらのディレクトリがどこで決まるのかは以下でわかる。
capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub

config/deploy/templates/puma.rbに以下のようなテンプレートを用意して(このテンプレートはbundle exec capが実行されるローカルになければいけない点に注意)

#!/usr/bin/env puma

puts "Hello!! This is custom puma template!!"

directory '<%= current_path %>'
rackup "<%=fetch(:puma_rackup)%>"
environment '<%= fetch(:puma_env) %>'
pidfile "<%=fetch(:puma_pid)%>"
state_path "<%=fetch(:puma_state)%>"

puma:configを実行

% be cap staging puma:config
00:00 puma:config
      Uploading /path/to/app/current/config/puma/staging.rb 100.0%

shared/puma.rbにリリースされてる。

#!/usr/bin/env puma

puts "Hello!! This is custom puma template!!"

directory '/path/to/app/current'
rackup "/path/to/app/current/config.ru"
environment 'staging'
pidfile "/path/to/app/shared/tmp/pids/puma.pid"
state_path "/path/to/app/shared/tmp/pids/puma.state"

  1. pumaのふるまいにはぱっと見変更が反映されたのかされてないのかわからない類のものも多いので、これではまる人は多い気がする。そして設定ファイルのパスに環境差分がおきてしまうのも歯がゆい! 

  2. もしサーバごとに異なるpuma設定で運用したい場合、git pushするのはよくない。 

  3. deploy:check時に自動生成する機能は、たぶんCapfileinstall_plugin Capistrano::Pumaと書いていれば有効になっている。 

  4. よくおこなわれるカスタム設定はすでにデフォルトのテンプレートに組み込まれてるので、ここを参照。capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHub 

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

capistrano3でpumaの運用ー設定ファイルの管理どうする?

railsユーザの多くがつかってるのではないかと思われるcapistranoとpuma。この組み合わせで、どうやってpuma起動時に任意の設定を指定するか、意外とわけわからんのでまとめた。

pumaのデフォルト仕様

なにも指定せずにpuma起動すると、 config/puma/{env}.rb, config/puma.rb, の優先順位で設定ファイルが読まれる。
puma/configuration.rb at 0cd28f373a245eca4e717bff1415b97bbc2356d2 · puma/puma · GitHub

ちなみに、ローカル環境などでrails server したときはどうも、puma, thin, webrickの順にrackハンドラを探して見つかったやつで起動するっぽい。最近のrailsではrails newするとpumaが入るので、特にいじらないかぎりrails serverしたときにはpumaが起動すると思っていい。
rack/handler.rb at 2.0.7 · rack/rack · GitHub

% rails server
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...

つまり、ローカルで起動するpumaの挙動を変更したい場合は、config/puma/development.rbをつくるか、config/puma.rbをつくってrails server起動すればいい。

capistranoをつかう場合

しかし、capistrano3-pumaをデフォルト設定でつかう場合、config/puma/{env}.rb, config/puma.rbは完全無視される。1
代わりに、capistrano3-pumaが持つ設定ファイル生成機能(後述)をもとに新規設定ファイルを作成し、デプロイサーバにアップロードし、それをpuma起動時に読むようになる。

ここでcapistrano & pumaの設定ファイルの運用方針が、大きく2つの道にわかれる。

pumaオリジナルconfig/puma/{env}.rb, config/puma.rbで運用する

capistrano3-pumaが提供する設定ファイル機能をつかわずに、従来のpumaの運用方法でやる方法。設定を緻密に詳細に指定したいときはこっちのほうがたぶんやりやすい。

必要な設定を逐一記述して、リポジトリにpushしておく。2
あとは、deploy.rbまたはdeploy/{env}.rbに、設定ファイルのパスpuma_confを設定すればいい。
Why it builds a new puma.config instead of using config/puma/production.rb? (question) · Issue #244 · seuros/capistrano-puma · GitHub

具体的にはこういう設定。

set :deploy_to, '/path/to/app'
set :puma_conf, "#{current_path}/config/puma/staging.rb"

# `current_path`の値は`deploy_to`によって決まるので、
# `puma_conf`は`deploy_to`よりあとに指定する点に注意!

こうすると、capistrano経由でpumaを起動するときに、-C /path/to/app/current/config/puma/staging.rbが指定されてくれる。

% bundle exec cap staging puma:start
00:00 puma:start
      using conf file /path/to/app/current/config/puma/staging.rb
      01 bundle exec puma -C /path/to/app/current/config/puma/staging.rb --daemon
...

capistrano3-pumaの設定ファイル生成機能をつかって運用する

capistrano3-pumaには設定ファイル生成機能puma:configがある。pumaの設定をがっちりかけなくてもそれなりの設定で動いてくれる。

cap {env} deploy:checkが実行されるときに、存在しなければ/path/to/app/shared/puma.rb(またはpuma_confに指定されたパス)を自動生成して各サーバにアップロードする。3具体的にはこのとき、cap {env} puma:configというタスクが実行されている。
cap {env} puma:configを単体で実行すると、既存ファイルが存在するしないに関係なくcapistrano管理上の設定で上書きされる

ふだんcapistrano運用者がつかうコマンドはcap {env} deploy(内部的にdeploy:checkを実行する)のはずなので、設定ファイルははじめの一回だけ自動生成され、その後都度更新されることはない。
だから、サーバ上の設定を手動で直接かきかえる運用も可能。ただし、そういう運用をおこなう場合は、cap {env} puma:configの実行に注意。手動で加えた設定は消されてしまうので。

% bundle exec cap staging puma:config
00:00 puma:config
      Uploading /path/to/app/shared/puma.rb 100.0%

# /path/to/app/shared/puma.rb
# を確認すると、設定ファイルが追加されてたり、上書きされてるのがわかる

# /path/to/app/shared/puma.rb
# ちなみに、このパスを変えたい場合は、前述した要領で`puma_conf`を指定すればいい

capistrano管理上のpuma設定を上書きする

cap {env} puma:configが実行されたときに生成されるファイルは、デフォルトでは以下に定義された値がコピーされる。

capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub

これらの設定値はもちろんcapistrano設定で全部上書きできる。
config/deploy.rbconfig/deploy/{env}.rbに以下のように記述する。(すでに紹介したpuma_confの上書きと同じ要領)

set :puma_env, 'staging'
set :puma_threads, [4, 4]
set :puma_workers, 0
set :puma_preload_app, false

設定可能な設定値はこちらのリストも参考
GitHub - seuros/capistrano-puma: Puma integration for Capistrano

念のためもう一度言うと、ここで設定した設定値はpuma起動時に直接読まれるわけではない。puma:configが実行されて、shared/puma.rb(またはpuma_confで指定されたパス)ファイルにその値がハードコードされ、それが起動時(cap {env} puma:start)に読み込まれることによってpumaに渡る。
つまり、puma_threads, puma_workersなどのcapistrano管理上のpuma設定は、cap {env} puma:configが実行されてはじめてリリースされる。

capistrano管理上のpuma設定をプログラムする

ここまで見ると、設定ファイル生成機能はライトにつかえる。しかしpumaのチューニングに余念がない人々にとっては、これだけでは足りない点がある。

puma設定には、設定の「値」だけではなく、「プログラム」がかけるのである。たとえば、pumaプロセスがフォークしたときのコールバックなどをカスタム設定できる仕組みになっている。
GitHub - puma/puma: A Ruby/Rack web server built for concurrency

こういうかんじで。

on_worker_boot do
  # configuration here
end

before_fork do
  # configuration here
end

こういうことをやりたくなったら4、はじめのpumaオリジナル設定ファイルの運用に切り替えるのをおすすめするが、一応設定ファイル生成機能をつかってもできる。

cap {env} puma:configが実行されるときに、生成される設定ファイルのテンプレートがこれ。
capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHub

つまり、このテンプレートを差し替えることができれば、設定だけじゃなくてプログラムも自由にかける。
上記のテンプレートを参考に、config/deploy/templatesまたはlib/capistrano/templates以下にカスタムテンプレートをおけばいい。これらのディレクトリがどこで決まるのかは以下でわかる。
capistrano-puma/puma.rb at v3.1.1 · seuros/capistrano-puma · GitHub

config/deploy/templates/puma.rbに以下のようなテンプレートを用意して(このテンプレートはbundle exec capが実行されるローカルになければいけない点に注意)

#!/usr/bin/env puma

puts "Hello!! This is custom puma template!!"

directory '<%= current_path %>'
rackup "<%=fetch(:puma_rackup)%>"
environment '<%= fetch(:puma_env) %>'
pidfile "<%=fetch(:puma_pid)%>"
state_path "<%=fetch(:puma_state)%>"

puma:configを実行

% be cap staging puma:config
00:00 puma:config
      Uploading /path/to/app/current/config/puma/staging.rb 100.0%

shared/puma.rbにリリースされてる。

#!/usr/bin/env puma

puts "Hello!! This is custom puma template!!"

directory '/path/to/app/current'
rackup "/path/to/app/current/config.ru"
environment 'staging'
pidfile "/path/to/app/shared/tmp/pids/puma.pid"
state_path "/path/to/app/shared/tmp/pids/puma.state"

  1. pumaのふるまいにはぱっと見変更が反映されたのかされてないのかわからない類のものも多いので、これではまる人は多い気がする。そして設定ファイルのパスに環境差分がおきてしまうのも歯がゆい! 

  2. もしサーバごとに異なるpuma設定で運用したい場合、git pushするのはよくない。 

  3. deploy:check時に自動生成する機能は、たぶんCapfileinstall_plugin Capistrano::Pumaと書いていれば有効になっている。 

  4. よくおこなわれるカスタム設定はすでにデフォルトのテンプレートに組み込まれてるので、ここを参照。capistrano-puma/puma.rb.erb at v3.1.1 · seuros/capistrano-puma · GitHub 

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

[WIP]RedisとSidekiqの説明

SidekiqとRedisを知るために、情報をまとめた記事です。

sidekiqとは

sidekiqとは、非同期処理を行いたい時に使うライブラリである。
Railsが必須というわけではないようですが、基本的にはRailsで使います。

sidekiqの使い方

1.まず、sidekiqを追加します。

gem 'sidekiq'

2.app/workersにワーカーファイルを作成します。

rails g sidekiq:worker Hard #このコマンドがapp/workers/hard_worker.rbファイルを作成します
class HardWorker
  include Sidekiq::Worker
  def perfome(name, count)
    # do something
  end
end

3.非同期処理が行われるように、ジョブを作成します

HardWorker.perform_async('bob', 5)

"perform"はインスタンスメソッドです。
他にも下記のようなインスタンスメソッドがあります。

HardWorker.perfome_in(5.minutes, 'bob', 5)
HardWorker.perfome_at(5.minutes.from_now, 'bob', 5)

メモ:もしsidekiqと使うときにrails consoleで使う場合は、springを止める事で新しいワーカーを使う事ができます。ワーカーの初期化かな?

4.sidekiqを動かすには下記コマンドを打ちましょう

bundle exec sidekiq

sidekiqにはpro版とかがある

Pro版とEnterprise版とで機能などが変わってくるそうです。
https://sidekiq.org/

Redisとは

Key-Valueストアデータベースです。
オープンソースのインメモリ型です。

データ保存方法の特徴

コンピュータの記憶領域として、
- CPUから直接アクセスできるメインメモリ
- ハードディスクなどのディスクストレージ
の2つがあります。
インメモリ・データベースとは、メインメモリ上だけでデータを管理する仕組みです。
メモリは「揮発性」という性質により、コンピューターが停止すると、メモリに載っている情報も全て消えてしまいます。しかしRedisではデータ永続化機能のオプションがあり、メインメモリ上のデータをディスクストレージに記憶させる事ができます。

Redisで保存できるデータ

Redisでは
- リスト型
- セット型
- ハッシュ型
のデータも扱う事ができます。

Redisの用途

キャッシュ

  • DBクエリ結果のキャッシュ
  • 永続的なセッションのキャッシュ
  • Webページのキャッシュ
    • 画像
    • ファイル
    • メタデータ

チャット

リアルタイムでのコメントストリーム
ソーシャルメディアフィード

ゲームのリーダーボード

リアルタイムでのランキングリストを作成する

セッションストア

セッション状態のなどを管理する時

リッチメディアストリーミング

(とにかく認証系を素早く大量に処理できる、という事でしょうか・・・)

地理空間

(地理空間データを簡単・高速に処理する事ができるそうです。位置情報などをリアルタイムに更新する際などには良さそうですね。)

Machine Learning

作成されたデータに対して、並行して機械学習・データ処理を行う訳ですが、その処理を行う際に、迅速に処理する事ができるという点ではRedisは有用なようです。

リアルタイム分析

断続的に更新されるデータを素早く処理するという意味で、Redisは有用そうです。

こうして見ると、Redisは、メモリの特徴を踏まえればわかるように、リアルタイム処理などに有効に働くようです。

競合ライブラリ

MemcachedもインメモリのOSSデータストアです。

参照
https://github.com/mperham/sidekiq/wiki/Getting-Started
https://aws.amazon.com/jp/redis/

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

今日学んだ事

7/7(日) 21:30〜0:20

●タグ

html.erb
<span>〜</apan>

・span囲った部分をインライン要素としてグループ化することができるタグ。
グループ化する事で、指定した範囲にスタイルシートを適用できたりする。

html.erb
<i>〜</i>

・フォントを「イタリック体」にする

●averageメソッド

・ActiveRecordクラスのメソッド
使用例
カラム=score
テーブル=students
クラス=Student
scoreカラムのaverageを求める

students = students.all
students.average.(:score)
#scoreの平均を小数点ありの状態で返す

小数点を四捨五入する場合

students = students.all
students.average.(:score) round
#roundメソッドを使用すると小数点を四捨五入する

●present?メソッド

・配列の中身がなければfalseを返す
・レビューの評価の平均など出すときに、レビューがない作品にfalseを返したい時などに使う

index.html.erb
<% if product.reviews.present? %>

このコードでレビューがない場合でもエラーが回避できる

●レシーバ

・インスタンスメソッドを利用するインスタンス自身の事

products = Product.all
#以下の式のレシーバはproducts
products.limit(5)

1.Productクラスからproductsインスタンスを生成
2.productsインスタンス自身がlimitメソッドを利用している
3.この時productsがレシーバとなる
ち・な・み・に
メソッドの呼び出しを「オブジェクト(インスタンス)にメッセージ(メソッド)を送る」と表現する。
今回の場合
「products(オブジェクト)にlimit(メッセージ)を送る」となる

●selfメソッド

・インスタンスメソッド内にselfを記述すると、そのメソッドを利用したインスタンス(レシーバ)が代入された変数のように扱う事ができる

def review.average
 self.review.average(:rate).round

うーん、よく分からない。。。。

●コントローラ内に記述されているlayout 'レイアウトファイル名'について

1.class MovieController < ApplicationController
2.  layout 'movie'
3.  
4.  def index
5.    @movies = Movie.all
6.  end
7.end

・MovieControllerのindexアクションが呼ばれたときに表示されるレイアウトはmovie.html.erbとなる。
・なにも指定しないとレイアウトファイルはapplication.html.erbとなる

以上

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

has_manyのidsを使って、簡単に中間テーブルの関連付けを行う

概要

チェックボックスを複数選択し、その情報を配列で受け取って、中間テーブルの関連付けを行います。

E-R図

スクリーンショット 2019-07-07 22.57.39.png
※今回説明で使用するアプリとカラム数があっていませんが、中間テーブルの使い方として掲載します。

事前準備

ToDo系アプリを想定して進めます。
画像のようなタスクを新規作成する画面を用意しました。
今回の話で使用するのは、Labelの複数のチェックボックスから値を取ってくる方法です。
スクリーンショット 2019-07-07 23.13.00.png

view

app/views/_form.html.erb
<tr>
  <th>Label</th>
  <td>
    <% Label.all.each do |label| %>
      <%= form.check_box :label_ids, { multiple: true, checked: @task.labels.find_by(id: label.id).present?, include_hidden: false }, label[:id] %>
      <label class='badge badge-secondary'><%= label.name %></label>
    <% end %>
  </td>
</tr>

form.check_box :label_idsと記述すると、paramsの値として、"label_ids"=>["7", "8", "10"]このような形でハッシュのキーを持つ値を送れます。
multipleオプションを使用することで、複数のチェックボックスのパラメータを配列形式で送れます。
checked: @task.labels.find_by(id: label.id).present?これはeditで使用することを想定しており、編集するタスクに紐付いているラベルにチェックを付けるということをしています。
include_hidden: falseチェックしていない項目については、パラメータを送らないオプションです。

model

app/models/task.rb
class Task < ApplicationRecord
  has_many :labelings, dependent: :destroy
  has_many :labels, through: :labelings
end
app/models/labeling.rb
class Labeling < ApplicationRecord
  belongs_to :task
  belongs_to :label
end
app/models/label.rb
class Label < ApplicationRecord
  has_many :labelings, dependent: :destroy
end

controller

app/controllers/tasks_controller.rb
  def create
    @task = Task.new(task_params)
    if @task.save
      redirect_to root_path, notice: '新しいタスクを作成しました'
    else
      render :new
    end
  end

 #省略

  private

  def task_params
    params.require(:task).permit(:subject, :content, :expired_at, :state, :priority, :user_id, label_ids: [])
  end

label_ids: []複数チェックボックスの値を配列で渡すことを許可しています。

タスク新規作成時に、ラベルを複数つける処理の動き

もう一度タスク新規投稿の画面を貼ります。
スクリーンショット 2019-07-07 23.13.00.png
Labelの開発DB操作次回MTGまでにチェックを入れた状態で登録ボタンを押します。
この3つのチェックボックスの値を取得します。

まずparamsには次のような値が送られます

Parameters: { "utf8"=>"✓",
              "authenticity_token"=>"WoRUP+q21xCR760x9L3Y9lyLeQ+THkS9YOGISfGRyDRg0xaI1D2BNnRQLTuaHANpw+NCohg06QtB5U4RxruO5g==", 
              "task"=>{ "subject"=>"タスク新規作成", 
                        "content"=>"ラベルを複数登録", 
                        "expired_at(1i)"=>"2019", 
                        "expired_at(2i)"=>"7", 
                        "expired_at(3i)"=>"8", 
                        "expired_at(4i)"=>"10", 
                        "expired_at(5i)"=>"00", 
                        "user_id"=>"87", 
                        "state"=>"着手中", 
                        "priority"=>"high", 
                        "label_ids"=>["7", "8", "10"]}, 
              "commit"=>"登録する" }

次に、controllerのcreateアクション時にbinding.pryで値を確認します。

  def create
    @task = Task.new(task_params)
    binding.pry
    if @task.save
      redirect_to root_path, notice: '新しいタスクを作成しました'
    else
      render :new
    end
  end
# idsだけ確認
pry(#<TasksController>)> @task.label_ids
=> [7, 8, 10]

# newメソッドで作られたlabelsを確認
pry(#<TasksController>)> @task.labels
=> [#<Label:0x00007f92cfe32628 id: 7, name: "開発", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:10:17 JST +09:00>,
 #<Label:0x00007f92cfe32470 id: 8, name: "DB操作", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:10:24 JST +09:00>,
 #<Label:0x00007f92cfe322e0 id: 10, name: "次回MTGまで", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:11:06 JST +09:00>]

pry(#<TasksController>)> @task.save
   (0.9ms)  BEGIN
   (pry):5
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 87], ["LIMIT", 1]]
   (pry):5
  Task Create (33.3ms)  INSERT INTO "tasks" ("subject", "content", "created_at", "updated_at", "expired_at", "state", "priority", "user_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id"  [["subject", "タスク新規作成"], ["content", "ラベルを複数登録"], ["created_at", "2019-07-07 23:59:15.509944"], ["updated_at", "2019-07-07 23:59:15.509944"], ["expired_at", "2019-07-08 10:00:00"], ["state", "着手中"], ["priority", 0], ["user_id", 87]]
   (pry):5
  Labeling Create (0.9ms)  INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["task_id", 212449], ["label_id", 7], ["created_at", "2019-07-07 23:59:15.553820"], ["updated_at", "2019-07-07 23:59:15.553820"]]
   (pry):5
  Labeling Create (0.3ms)  INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["task_id", 212449], ["label_id", 8], ["created_at", "2019-07-07 23:59:15.556142"], ["updated_at", "2019-07-07 23:59:15.556142"]]
   (pry):5
  Labeling Create (0.2ms)  INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["task_id", 212449], ["label_id", 10], ["created_at", "2019-07-07 23:59:15.557177"], ["updated_at", "2019-07-07 23:59:15.557177"]]
   (pry):5
   (0.8ms)  COMMIT
   (pry):5
=> true

pry(#<TasksController>)> @task.id
=> 212449
pry(#<TasksController>)> Labeling.where(task_id: 212449)
  Labeling Load (0.2ms)  SELECT "labelings".* FROM "labelings" WHERE "labelings"."task_id" = $1  [["task_id", 212449]]
   app/controllers/tasks_controller.rb:38
=> [#<Labeling:0x00007f92d142e9e0
  id: 11,
  task_id: 212449,
  label_id: 7,
  created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00,
  updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>,
 #<Labeling:0x00007f92d142e8a0
  id: 12,
  task_id: 212449,
  label_id: 8,
  created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00,
  updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>,
 #<Labeling:0x00007f92d142e760
  id: 13,
  task_id: 212449,
  label_id: 10,
  created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00,
  updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>]

無事1度のcreateアクションで、中間テーブルに3個保存されました!

タスク編集時に、複数のラベルを変更した時の、中間テーブルの動き

ここまで新規作成時の中間テーブルの関連付けの説明をしました。
では、更新する時はどうやるの?
外すラベルのid調べてテーブルからdeleteしてinsertするの?といった疑問が浮かぶと思います。
実はdeleteとかはいい感じに勝手にやってくれます!
実際の動きを確認してみました

スクリーンショット 2019-07-08 0.07.20.png
新規作成時に開発DB操作次回MTGまでにチェックを入れてます。
viewにchecked: @task.labels.find_by(id: label.id).present?を記載しているので、デフォルトでチェックが入っています

スクリーンショット 2019-07-08 0.07.35.png
DB操作次回MTGまでのチェックボックスを外し、
新たに検討中にチェックしました。
更新するボタンを押します。

   (0.2ms)  BEGIN
   app/controllers/tasks_controller.rb:52
  Label Load (0.3ms)  SELECT "labels".* FROM "labels" WHERE "labels"."id" IN ($1, $2)  [["id", 7], ["id", 9]]
   app/controllers/tasks_controller.rb:52
  Labeling Destroy (0.3ms)  DELETE FROM "labelings" WHERE "labelings"."task_id" = $1 AND "labelings"."label_id" IN ($2, $3)  [["task_id", 212449], ["label_id", 8], ["label_id", 10]]
   app/controllers/tasks_controller.rb:52
  Labeling Create (0.4ms)  INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["task_id", 212449], ["label_id", 9], ["created_at", "2019-07-08 00:08:31.021375"], ["updated_at", "2019-07-08 00:08:31.021375"]]
   app/controllers/tasks_controller.rb:52
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 87], ["LIMIT", 1]]
   app/controllers/tasks_controller.rb:52
   (2.1ms)  COMMIT
   app/controllers/tasks_controller.rb:52

すると、中間テーブルからチェックボックスを外したlabel_idがDestroyされた後にCreateされています。

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