20191221のRubyに関する記事は30件です。

1年業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~

はじめに

2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)

対象読者

今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。

今回使用した技術

フロントエンド

  • Javascript (業務ではTypeScriptを使っています)
  • React
  • Redux
  • axious

バックエンド

  • Ruby 2.5.7
  • Ruby on Rails 5.2.4
  • rubocop
  • guard
  • rspec

CI/CD

  • Circle CI

フロントエンド編

React とは

A JavaScript library for building user interfaces

https://reactjs.org/

Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。

Reactの考え方

Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.

https://reactjs.org/

Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。

axios とは

Promise based HTTP client for the browser and node.js

https://github.com/axios/axios

Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、

  • ChromeやFirefox, Safariなどのブラウザに対応

  • ブラウザからXMLHttpsRequestsを作成

  • リクエストとレスポンスデータを変換

  • Promise APIをサポート

と言った特徴があります。

今回は、このライブラリを使ってAPIとの通信を行なっていきます。

実装

以下がコードになります。

import React from 'react';
import axios from 'axios';

const Title = ({todoCount}) => {
  return (
    <div>
      {
          todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> :
          <h1>タスクがありません</h1>
      }
    </div>
  );
}

const TodoForm = ({addTodo}) => {
  let input;
  return (
    <form onSubmit={(e) => {
        e.preventDefault();
        addTodo(input.value);
        input.value = '';
      }}>
      <input className="form-control col-md-12" ref={node => {
        input = node;
      }} />
      <br />
    </form>
  );
};

const Todo = ({todo, remove}) => {
  return (
    <li>
      <a href="#" className="list-group-item">{todo.contents}</a>
      <button onClick={() => {remove(todo.id)}}>削除</button>
    </li>
  );
}

const TodoList = ({todos, remove}) => {
  const todoNode = todos.map((todo) => {
    return (<Todo todo={todo} key={todo.id} remove={remove} />)
  });
  return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>);
}

window.id = 0;
class TodoApp extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      data: []
    }
    this.apiUrl = 'http://localhost:3000/api/todos/'
  }

  componentDidMount(){
    axios.get(this.apiUrl)
      .then((res) => {
        this.setState({ data: res.data });
      });
  }

  addTodo(val){
    const todo = {contents: val}
    axios.post(this.apiUrl, todo)
       .then((res) => {
          this.state.data.push(res.data);
          this.setState({data: this.state.data});
       });
  }

  handleRemove(id){
    const remainder = this.state.data.filter((todo) => {
      if(todo.id !== id) return todo;
    });

    axios.delete(this.apiUrl+id)
      .then((res) => {
        this.setState({data: remainder});
      })
  }

  render(){

    return (
      <div>
        <Title todoCount={this.state.data.length}/>
        <TodoForm addTodo={this.addTodo.bind(this)}/>
        <TodoList
          todos={this.state.data}
          remove={this.handleRemove.bind(this)}
        />
      </div>
    );
  }
}

export default TodoApp

上記のコードを見てもらうと分かりやすいと思うのですが、TodoAppコンポーネントにconstructorメソッド、componentDidMountメソッドを定義しています。componentDidMountメソッドは、Reactのライフサイクルメソッドの一種で、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なっています。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。

バックエンド編

Ruby on Rails とは

Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.

https://github.com/rails/rails

大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。

今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。

APIモードは、下記のようにコマンドライン引数に --apiとつけるだけで、できてしまいます(さすがRails!)

rails new my_api --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active Record とは

Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.

https://guides.rubyonrails.org/active_record_basics.html

MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。

ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、完結なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。

例えば、ターミナル上で、

rails console

を叩き

Todo.all

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。

他にも

  • モデル同士のアソシエーションを表現する
  • 関連付けられているモデル間の継承階層を表現する
  • データをデータベースで永続化する前にバリデーションを行う

などの特徴を持っています。

RuboCop とは

RuboCop is a Ruby static code analyzer and code formatter.

https://github.com/rubocop-hq/rubocop

rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。

bundle exec rubocop -a

参考
https://rubocop.readthedocs.io/en/stable/

実装

実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。

bundle exec rails generate scaffold Todo contents:string

実際のコード

todo_controller.rb

class TodosController < ApplicationController
  before_action :set_todo, only: [:show, :edit, :update, :destroy]

  def index
    @todos = Todo.all
    render json: @todos
  end

  def show
    render json: @todo
  end

  def new
    @todo = Todo.new
  end

  def edit
  end

  def create
    @todo = Todo.new(create_params)
    if @todo.save
      render json: @todo
    else
      render :new
    end
  end

  def update
    if @todo.update!(update_params)
      render json: @todo
    else
      render :edit
    end
  end

  def destroy
    @todo.destroy
    render json: @todos
  end

  private
    def set_todo
      @todo = Todo.find(params[:id])
    end

    def create_params
      params.require(:todo).permit(:contents)
    end

    def update_params
      params.require(:todo).permit(%i[id contents])
    end

    def destroy_params
      params.require(:todo).permit(:id)
    end
end

routes.rb

Rails.application.routes.draw do
  scope :api, defaults: { format: :json } do
    resources :todos, only: %i[show index create update destroy]
  end
end

models/todo.rb

class Todo < ApplicationRecord
  def as_json(options = {})
    super(options.reverse_merge(except: %i[created_at updated_at]))
  end
end

今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/

CI編

後日書きます

まとめ

半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。

その他の参考資料

https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react

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

半年間業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~

Opt Technologies Advent Calendar 2019の21日目の記事です。

はじめに

2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)

ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。

https://github.com/sh1okoh/adventcalendar

対象読者

今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。

今回使用した技術

フロントエンド

  • JavaScript (業務ではTypeScriptを使っています)
  • React
  • Redux
  • axios
  • eslint
  • prettier

バックエンド

  • Ruby 2.5.7
  • Ruby on Rails 5.2.4
  • rubocop
  • guard
  • rspec
  • rack-cors

CI/CD

  • Circle CI

フロントエンド編

React とは

A JavaScript library for building user interfaces

https://reactjs.org/

Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。

Reactの考え方

Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.

https://reactjs.org/

Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。

axios とは

Promise based HTTP client for the browser and node.js

https://github.com/axios/axios

Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、

  • ChromeやFirefox, Safariなどのブラウザに対応

  • ブラウザからXMLHttpsRequestsを作成

  • リクエストとレスポンスデータを変換

  • Promise APIをサポート

と言った特徴があります。

今回は、このライブラリを使ってAPIとの通信を行なっていきます。

実装

以下がコードになります。

import React from 'react';
import axios from 'axios';

const Title = ({todoCount}) => {
  return (
    <div>
      {
          todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> :
          <h1>タスクがありません</h1>
      }
    </div>
  );
}

const TodoForm = ({addTodo}) => {
  let input;
  return (
    <form onSubmit={(e) => {
        e.preventDefault();
        addTodo(input.value);
        input.value = '';
      }}>
      <input className="form-control col-md-12" ref={node => {
        input = node;
      }} />
      <br />
    </form>
  );
};

const Todo = ({todo, remove}) => {
  return (
    <li>
      <a href="#" className="list-group-item">{todo.contents}</a>
      <button onClick={() => {remove(todo.id)}}>削除</button>
    </li>
  );
}

const TodoList = ({todos, remove}) => {
  const todoNode = todos.map((todo) => {
    return (<Todo todo={todo} key={todo.id} remove={remove} />)
  });
  return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>);
}

window.id = 0;
class TodoApp extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      data: []
    }
    this.apiUrl = 'http://localhost:3000/api/todos/'
  }

  componentDidMount(){
    axios.get(this.apiUrl)
      .then((res) => {
        this.setState({ data: res.data });
      });
  }

  addTodo(val){
    const todo = {contents: val}
    axios.post(this.apiUrl, todo)
       .then((res) => {
          this.state.data.push(res.data);
          this.setState({data: this.state.data});
       });
  }

  handleRemove(id){
    const remainder = this.state.data.filter((todo) => {
      if(todo.id !== id) return todo;
    });

    axios.delete(this.apiUrl+id)
      .then((res) => {
        this.setState({data: remainder});
      })
  }

  render(){

    return (
      <div>
        <Title todoCount={this.state.data.length}/>
        <TodoForm addTodo={this.addTodo.bind(this)}/>
        <TodoList
          todos={this.state.data}
          remove={this.handleRemove.bind(this)}
        />
      </div>
    );
  }
}

export default TodoApp

上記のコードを見てもらうと分かりやすいと思うのですが、TodoAppコンポーネントにconstructorメソッド、componentDidMountメソッド, renderメソッドを定義しています。componentDidMountメソッド, renderメソッドは、Reactのライフサイクルメソッドの一種で、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。

バックエンド編

Ruby on Rails とは

Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.

https://github.com/rails/rails

大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。

今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。

APIモードは、下記のようにコマンドライン引数に --apiとつけるだけで、できてしまいます(さすがRails!)

rails new my_api --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active Record とは

Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.

https://guides.rubyonrails.org/active_record_basics.html

MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。

ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。

例えば、ターミナル上で、

rails console

を叩き

Todo.all

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。

他にも

  • モデル同士のアソシエーションを表現する
  • 関連付けられているモデル間の継承階層を表現する
  • データをデータベースで永続化する前にバリデーションを行う

などの特徴を持っています。

RuboCop とは

RuboCop is a Ruby static code analyzer and code formatter.

https://github.com/rubocop-hq/rubocop

rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。

bundle exec rubocop -a

参考
https://rubocop.readthedocs.io/en/stable/

実装

実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。

bundle exec rails generate scaffold Todo contents:string

実際のコード

todo_controller.rb

class TodosController < ApplicationController
  before_action :set_todo, only: [:show, :edit, :update, :destroy]

  def index
    @todos = Todo.all
    render json: @todos
  end

  def show
    render json: @todo
  end

  def new
    @todo = Todo.new
  end

  def edit
  end

  def create
    @todo = Todo.new(create_params)
    if @todo.save
      render json: @todo
    else
      render :new
    end
  end

  def update
    if @todo.update!(update_params)
      render json: @todo
    else
      render :edit
    end
  end

  def destroy
    @todo.destroy
    render json: @todos
  end

  private
    def set_todo
      @todo = Todo.find(params[:id])
    end

    def create_params
      params.require(:todo).permit(:contents)
    end

    def update_params
      params.require(:todo).permit(%i[id contents])
    end

    def destroy_params
      params.require(:todo).permit(:id)
    end
end

routes.rb

Rails.application.routes.draw do
  scope :api, defaults: { format: :json } do
    resources :todos, only: %i[show index create update destroy]
  end
end

models/todo.rb

class Todo < ApplicationRecord
  def as_json(options = {})
    super(options.reverse_merge(except: %i[created_at updated_at]))
  end
end

db/schema.rb

ActiveRecord::Schema.define(version: 2019_12_15_104719) do

  create_table "todos", force: :cascade do |t|
    t.string "contents"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/

CI編

後日書きます

まとめ

半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。

その他の参考資料

https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react

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

業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~

Opt Technologies Advent Calendar 2019の21日目の記事です。

はじめに

2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)

ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。

https://github.com/sh1okoh/adventcalendar

対象読者

今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。

今回使用した技術

フロントエンド

  • JavaScript (業務ではTypeScriptを使っています)
  • React
  • Redux
  • axios
  • eslint
  • prettier

バックエンド

  • Ruby 2.5.7
  • Ruby on Rails 5.2.4
  • rubocop
  • guard
  • rspec
  • rack-cors

CI/CD

  • Circle CI

フロントエンド編

React とは

A JavaScript library for building user interfaces

https://reactjs.org/

Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。

Reactの考え方

Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.

https://reactjs.org/

Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。

axios とは

Promise based HTTP client for the browser and node.js

https://github.com/axios/axios

Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、

  • ChromeやFirefox, Safariなどのブラウザに対応
  • リクエストとレスポンスデータを変換
  • Promise APIをサポート

と言った特徴があります。

今回は、このライブラリを使ってAPIとの通信を行なっていきます。

実装

以下がコードになります。

import React from 'react';
import axios from 'axios';

const Title = ({todoCount}) => {
  return (
    <div>
      {
          todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> :
          <h1>タスクがありません</h1>
      }
    </div>
  );
}

const TodoForm = ({addTodo}) => {
  let input;
  return (
    <form onSubmit={(e) => {
        e.preventDefault();
        addTodo(input.value);
        input.value = '';
      }}>
      <input className="form-control col-md-12" ref={node => {
        input = node;
      }} />
      <br />
    </form>
  );
};

const Todo = ({todo, remove}) => {
  return (
    <li>
      <a href="#" className="list-group-item">{todo.contents}</a>
      <button onClick={() => {remove(todo.id)}}>削除</button>
    </li>
  );
}

const TodoList = ({todos, remove}) => {
  const todoNode = todos.map((todo) => {
    return (<Todo todo={todo} key={todo.id} remove={remove} />)
  });
  return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>);
}

window.id = 0;
class TodoApp extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      data: []
    }
    this.apiUrl = 'http://localhost:3000/api/todos/'
  }

  componentDidMount(){
    axios.get(this.apiUrl)
      .then((res) => {
        this.setState({ data: res.data });
      });
  }

  addTodo(val){
    const todo = {contents: val}
    axios.post(this.apiUrl, todo)
       .then((res) => {
          this.state.data.push(res.data);
          this.setState({data: this.state.data});
       });
  }

  handleRemove(id){
    const remainder = this.state.data.filter((todo) => {
      if(todo.id !== id) return todo;
    });

    axios.delete(this.apiUrl+id)
      .then((res) => {
        this.setState({data: remainder});
      })
  }

  render(){

    return (
      <div>
        <Title todoCount={this.state.data.length}/>
        <TodoForm addTodo={this.addTodo.bind(this)}/>
        <TodoList
          todos={this.state.data}
          remove={this.handleRemove.bind(this)}
        />
      </div>
    );
  }
}

export default TodoApp

上記のコードを見てもらうと分かりやすいと思うのですが、TodoAppコンポーネントにconstructorメソッド、componentDidMountメソッド, renderメソッドを定義しています。componentDidMountメソッド, renderメソッドは、Reactのライフサイクルメソッドの一種です。componentDidMountメソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。

バックエンド編

Ruby on Rails とは

Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.

https://github.com/rails/rails

大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。

今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。

APIモードは、下記のようにコマンドライン引数に --apiとつけるだけで、できてしまいます(さすがRails!)

rails new my_api --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active Record とは

Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.

https://guides.rubyonrails.org/active_record_basics.html

MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。

ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。

例えば、ターミナル上で、

rails console

を叩き

Todo.all

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。

他にも

  • モデル同士のアソシエーションを表現する
  • 関連付けられているモデル間の継承階層を表現する
  • データをデータベースで永続化する前にバリデーションを行う

などの特徴を持っています。

RuboCop とは

RuboCop is a Ruby static code analyzer and code formatter.

https://github.com/rubocop-hq/rubocop

rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。

bundle exec rubocop -a

参考
https://rubocop.readthedocs.io/en/stable/

実装

実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。

bundle exec rails generate scaffold Todo contents:string

実際のコード

todo_controller.rb

class TodosController < ApplicationController
  before_action :set_todo, only: [:show, :edit, :update, :destroy]

  def index
    @todos = Todo.all
    render json: @todos
  end

  def show
    render json: @todo
  end

  def new
    @todo = Todo.new
  end

  def edit
  end

  def create
    @todo = Todo.new(create_params)
    if @todo.save
      render json: @todo
    else
      render :new
    end
  end

  def update
    if @todo.update!(update_params)
      render json: @todo
    else
      render :edit
    end
  end

  def destroy
    @todo.destroy
    render json: @todos
  end

  private
    def set_todo
      @todo = Todo.find(params[:id])
    end

    def create_params
      params.require(:todo).permit(:contents)
    end

    def update_params
      params.require(:todo).permit(%i[id contents])
    end

    def destroy_params
      params.require(:todo).permit(:id)
    end
end

routes.rb

Rails.application.routes.draw do
  scope :api, defaults: { format: :json } do
    resources :todos, only: %i[show index create update destroy]
  end
end

models/todo.rb

class Todo < ApplicationRecord
  def as_json(options = {})
    super(options.reverse_merge(except: %i[created_at updated_at]))
  end
end

db/schema.rb

ActiveRecord::Schema.define(version: 2019_12_15_104719) do

  create_table "todos", force: :cascade do |t|
    t.string "contents"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/

CI編

後日書きます

まとめ

半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。

その他の参考資料

https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react

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

React+Railsでモダン()なTODOアプリを作ってみた ~ 備忘録も兼ねて ~

Opt Technologies Advent Calendar 2019の21日目の記事です。

はじめに

2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)

ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。

https://github.com/sh1okoh/adventcalendar

対象読者

今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。

今回使用した技術

フロントエンド

  • JavaScript (業務ではTypeScriptを使っています)
  • React
  • Redux
  • axios
  • eslint
  • prettier

バックエンド

  • Ruby 2.5.7
  • Ruby on Rails 5.2.4
  • rubocop
  • guard
  • rspec
  • rack-cors

CI/CD

  • Circle CI

フロントエンド編

React とは

A JavaScript library for building user interfaces

https://reactjs.org/

Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。

Reactの考え方

Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.

https://reactjs.org/

Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。

axios とは

Promise based HTTP client for the browser and node.js

https://github.com/axios/axios

Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、

  • ChromeやFirefox, Safariなどのブラウザに対応
  • リクエストとレスポンスデータを変換
  • Promise APIをサポート

と言った特徴があります。

今回は、このライブラリを使ってAPIとの通信を行なっていきます。

実装

以下がコードになります。

import React from 'react';
import axios from 'axios';

const Title = ({todoCount}) => {
  return (
    <div>
      {
          todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> :
          <h1>タスクがありません</h1>
      }
    </div>
  );
}

const TodoForm = ({addTodo}) => {
  let input;
  return (
    <form onSubmit={(e) => {
        e.preventDefault();
        addTodo(input.value);
        input.value = '';
      }}>
      <input className="form-control col-md-12" ref={node => {
        input = node;
      }} />
      <br />
    </form>
  );
};

const Todo = ({todo, remove}) => {
  return (
    <li>
      <a href="#" className="list-group-item">{todo.contents}</a>
      <button onClick={() => {remove(todo.id)}}>削除</button>
    </li>
  );
}

const TodoList = ({todos, remove}) => {
  const todoNode = todos.map((todo) => {
    return (<Todo todo={todo} key={todo.id} remove={remove} />)
  });
  return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>);
}

window.id = 0;
class TodoApp extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      data: []
    }
    this.apiUrl = 'http://localhost:3000/api/todos/'
  }

  componentDidMount(){
    axios.get(this.apiUrl)
      .then((res) => {
        this.setState({ data: res.data });
      });
  }

  addTodo(val){
    const todo = {contents: val}
    axios.post(this.apiUrl, todo)
       .then((res) => {
          this.state.data.push(res.data);
          this.setState({data: this.state.data});
       });
  }

  handleRemove(id){
    const remainder = this.state.data.filter((todo) => {
      if(todo.id !== id) return todo;
    });

    axios.delete(this.apiUrl+id)
      .then((res) => {
        this.setState({data: remainder});
      })
  }

  render(){

    return (
      <div>
        <Title todoCount={this.state.data.length}/>
        <TodoForm addTodo={this.addTodo.bind(this)}/>
        <TodoList
          todos={this.state.data}
          remove={this.handleRemove.bind(this)}
        />
      </div>
    );
  }
}

export default TodoApp

上記のコードを見てもらうと分かりやすいと思うのですが、TodoAppコンポーネントにconstructorメソッド、componentDidMountメソッド, renderメソッドを定義しています。componentDidMountメソッド, renderメソッドは、Reactのライフサイクルメソッドの一種です。componentDidMountメソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。

バックエンド編

Ruby on Rails とは

Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.

https://github.com/rails/rails

Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。

今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。

APIモードは、下記のようにコマンドライン引数に --apiとつけるだけで、できてしまいます(さすがRails!)

rails new my_api --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active Record とは

Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.

https://guides.rubyonrails.org/active_record_basics.html

MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。

ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルのレコードのデータを取得できたり、更新できたりしてとても便利です。

例えば、ターミナル上で、

rails console

を叩き

Todo.all

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。

他にも

  • モデル同士のアソシエーションを表現する
  • 関連付けられているモデル間の継承階層を表現する
  • データをデータベースで永続化する前にバリデーションを行う

などの特徴を持っています。

RuboCop とは

RuboCop is a Ruby static code analyzer and code formatter.

https://github.com/rubocop-hq/rubocop

rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。

bundle exec rubocop -a

参考
https://rubocop.readthedocs.io/en/stable/

実装

実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。

bundle exec rails generate scaffold Todo contents:string

実際のコード

controllers/todos_controller.rb

class TodosController < ApplicationController
  before_action :set_todo, only: [:show, :edit, :update, :destroy]

  def index
    @todos = Todo.all
    render json: @todos
  end

  def show
    render json: @todo
  end

  def new
    @todo = Todo.new
  end

  def edit
  end

  def create
    @todo = Todo.new(create_params)
    if @todo.save
      render json: @todo
    else
      render :new
    end
  end

  def update
    if @todo.update!(update_params)
      render json: @todo
    else
      render :edit
    end
  end

  def destroy
    @todo.destroy
    render json: @todos
  end

  private
    def set_todo
      @todo = Todo.find(params[:id])
    end

    def create_params
      params.require(:todo).permit(:contents)
    end

    def update_params
      params.require(:todo).permit(%i[id contents])
    end

    def destroy_params
      params.require(:todo).permit(:id)
    end
end

config/routes.rb

Rails.application.routes.draw do
  scope :api, defaults: { format: :json } do
    resources :todos, only: %i[show index create update destroy]
  end
end

models/todo.rb

class Todo < ApplicationRecord
  def as_json(options = {})
    super(options.reverse_merge(except: %i[created_at updated_at]))
  end
end

db/schema.rb

ActiveRecord::Schema.define(version: 2019_12_15_104719) do

  create_table "todos", force: :cascade do |t|
    t.string "contents"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/

CI編

後日書きます

まとめ

半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。

その他の参考資料

https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react

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

React+Railsでモダン()なTODOアプリを作ってみた 〜備忘録も兼ねて〜

Opt Technologies Advent Calendar 2019の21日目の記事です。

はじめに

2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)

ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。

https://github.com/sh1okoh/adventcalendar

対象読者

今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったSPAの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。

今回使用した技術

フロントエンド

  • JavaScript (業務ではTypeScriptを使っています)
  • React
  • Redux
  • axios
  • eslint
  • prettier

バックエンド

  • Ruby 2.5.7
  • Ruby on Rails 5.2.4
  • rubocop
  • guard
  • rspec
  • rack-cors

CI/CD

  • Circle CI

フロントエンド編

React とは

A JavaScript library for building user interfaces

https://reactjs.org/

Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。

Reactの考え方

Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.

https://reactjs.org/

Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。

axios とは

Promise based HTTP client for the browser and node.js

https://github.com/axios/axios

Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、

  • ChromeやFirefox, Safariなどのブラウザに対応
  • リクエストとレスポンスデータを変換
  • Promise APIをサポート

と言った特徴があります。

今回は、このライブラリを使ってAPIとの通信を行なっていきます。

実装

以下がコードになります。

import React from 'react';
import axios from 'axios';

const Title = ({todoCount}) => {
  return (
    <div>
      {
          todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> :
          <h1>タスクがありません</h1>
      }
    </div>
  );
}

const TodoForm = ({addTodo}) => {
  let input;
  return (
    <form onSubmit={(e) => {
        e.preventDefault();
        addTodo(input.value);
        input.value = '';
      }}>
      <input className="form-control col-md-12" ref={node => {
        input = node;
      }} />
      <br />
    </form>
  );
};

const Todo = ({todo, remove}) => {
  return (
    <li>
      <a href="#" className="list-group-item">{todo.contents}</a>
      <button onClick={() => {remove(todo.id)}}>削除</button>
    </li>
  );
}

const TodoList = ({todos, remove}) => {
  const todoNode = todos.map((todo) => {
    return (<Todo todo={todo} key={todo.id} remove={remove} />)
  });
  return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>);
}

window.id = 0;
class TodoApp extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      data: []
    }
    this.apiUrl = 'http://localhost:3000/api/todos/'
  }

  componentDidMount(){
    axios.get(this.apiUrl)
      .then((res) => {
        this.setState({ data: res.data });
      });
  }

  addTodo(val){
    const todo = {contents: val}
    axios.post(this.apiUrl, todo)
       .then((res) => {
          this.state.data.push(res.data);
          this.setState({data: this.state.data});
       });
  }

  handleRemove(id){
    const remainder = this.state.data.filter((todo) => {
      if(todo.id !== id) return todo;
    });

    axios.delete(this.apiUrl+id)
      .then((res) => {
        this.setState({data: remainder});
      })
  }

  render(){

    return (
      <div>
        <Title todoCount={this.state.data.length}/>
        <TodoForm addTodo={this.addTodo.bind(this)}/>
        <TodoList
          todos={this.state.data}
          remove={this.handleRemove.bind(this)}
        />
      </div>
    );
  }
}

export default TodoApp

上記のコードを見てもらうと分かりやすいと思うのですが、TodoAppコンポーネントにconstructorメソッド、componentDidMountメソッド, renderメソッドを定義しています。componentDidMountメソッド, renderメソッドは、Reactのライフサイクルメソッドの一種です。componentDidMountメソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。

で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。

APIの疎通に関しては、

axios.get

axios.post

で行なって、APIからのレスポンスデータを取得したりしています。

で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
スクリーンショット 2019-12-21 22.14.48.png

で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
スクリーンショット 2019-12-21 22.17.35.png

本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。

バックエンド編

Ruby on Rails とは

Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.

https://github.com/rails/rails

Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。

今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。

APIモードは、下記のようにコマンドライン引数に --apiとつけるだけで、できてしまいます(さすがRails!)

rails new my_api --api

APIモード
https://guides.rubyonrails.org/api_app.html

Active Record とは

Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.

https://guides.rubyonrails.org/active_record_basics.html

MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。

ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルのレコードのデータを取得できたり、更新できたりしてとても便利です。

例えば、ターミナル上で、

rails console

を叩き

Todo.all

などとすると、下記のような出力結果を得ることができます。

スクリーンショット 2019-12-21 23.16.15.png

またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。

他にも

  • モデル同士のアソシエーションを表現する
  • 関連付けられているモデル間の継承階層を表現する
  • データをデータベースで永続化する前にバリデーションを行う

などの特徴を持っています。

RuboCop とは

RuboCop is a Ruby static code analyzer and code formatter.

https://github.com/rubocop-hq/rubocop

rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。

bundle exec rubocop -a

参考
https://rubocop.readthedocs.io/en/stable/

実装

実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。

bundle exec rails generate scaffold Todo contents:string

実際のコード

controllers/todos_controller.rb

class TodosController < ApplicationController
  before_action :set_todo, only: [:show, :edit, :update, :destroy]

  def index
    @todos = Todo.all
    render json: @todos
  end

  def show
    render json: @todo
  end

  def new
    @todo = Todo.new
  end

  def edit
  end

  def create
    @todo = Todo.new(create_params)
    if @todo.save
      render json: @todo
    else
      render :new
    end
  end

  def update
    if @todo.update!(update_params)
      render json: @todo
    else
      render :edit
    end
  end

  def destroy
    @todo.destroy
    render json: @todos
  end

  private
    def set_todo
      @todo = Todo.find(params[:id])
    end

    def create_params
      params.require(:todo).permit(:contents)
    end

    def update_params
      params.require(:todo).permit(%i[id contents])
    end

    def destroy_params
      params.require(:todo).permit(:id)
    end
end

config/routes.rb

Rails.application.routes.draw do
  scope :api, defaults: { format: :json } do
    resources :todos, only: %i[show index create update destroy]
  end
end

models/todo.rb

class Todo < ApplicationRecord
  def as_json(options = {})
    super(options.reverse_merge(except: %i[created_at updated_at]))
  end
end

db/schema.rb

ActiveRecord::Schema.define(version: 2019_12_15_104719) do

  create_table "todos", force: :cascade do |t|
    t.string "contents"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/

CI編

後日書きます

まとめ

半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。

その他の参考資料

https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react

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

[Rails]特定のインスタンスに対して前後のレコードを取得したりランダムで取得したりするあれを試してみた

はじめに

こんにちはどうも、pirikaraです。
髪の毛を切りました。

今回はこんなやつを実装しました。
スクリーンショット 2019-12-21 12.36.00.png

特定の投稿(今回はitemの出品)に関して、DBから前後のレコードを取得して表示させたりリンクを飛ばす感じのあれです。
あとDBからランダムにレコードを取得して表示させたりリンク飛ばしたりする感じのあれです。

まずは特定の投稿に関してDBから前後レコードを取得する奴から実装していきます。
Rails標準のAPIでは見つからなかったので、今回はmodelに対してメソッドを書き込んでいきます。

いざ、実装

今回はItemクラスのインスタンスに関して、その前後レコードを取得するメソッドをmodels/item.rbに記述していきます。
前のレコードを取得するメソッドを『previous』
後のレコードを取得するメソッドを『next』
としてmodelに定義していきます。

item.rb
class Item < ApplicationRecord
  #......省略


  def previous
    Item.where("id < ?", self.id).order("id DESC").first
  end

  def next
    Item.where("id > ?", self.id).order("id ASC").first
  end
end

現在のインスタンス(self)のidより大きいか小さいかで前後レコードを判定・取得します。
作成したメソッドをview側で呼び出します。

items/show.haml
#......省略

- if @item.previous.present?
  =link_to item_path(@item.previous.id) do
    = @item.previous.name
- else
- else
  .none
- if @item.next.present?
  =link_to item_path(@item.next.id) do
    = @item.next.name
- else
  .none
#......省略

controller側で@item = Item.find(params[:id])などとし、Itemのshow画面に遷移。
@itemに対して『previous』 『next』メソッドを使用することで、@itemの前後レコードを取得できます。

また、前後レコードがない場合(最新のレコード、もしくは最古のレコードの場合)にリンクを表示しないよう『present?』メソッドによって真偽判定、条件分けしています。

Image from Gyazo

左右感がちょっと気持ち悪いですが、実装できました。(あとで直します。)

変化球

前後レコードではなく、DB内のデータをランダムで取得するあれを実装を試みてみます。
ネットサーフィンしながらいくつか方法を見つけたので試してみます。

1. RAND()関数

MySQLのネイティブ関数RAND()を使用してみます。

items_controller.rb
def show
  #......省略

  @random1 = Item.order("RAND()").limit(1)
  @random2 = Item.order("RAND()").limit(1)

 #......省略
end

左右のリンクがあるので、それぞれ@random1@random2として変数をインスタンス変数を定義してみました。
viewはこちら

items/show.haml
- if @random1.present?
  =link_to item_path(@random1.ids) do
    - @random1.each do |random1|
      = random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.ids) do
    - @random2.each do |random2|
      = random2.name
- else
  .none

RAND()関数で取得したデータにlimitつけてましたが、配列で取得されるみたいなので『ids』としないとid取れませんでした。
また、nameについてもeach文で取り出してあげないと表示されませんでした悲しい。

そして結果がこちら。
Image from Gyazo
スクリーンショット 2019-12-21 14.32.36.png
......ねこもうさぎも被りました。

このあと何回もページ遷移してみました。
確かにランダムで表示されるようにはなったようですが、左右の値が毎回同じでした。
実際にDBテーブル内に存在する値からランダムに取得してくれるのはありがたいですが、
randam1とrandam2で別々に値を取得してくれるなんてそんな都合よく世界はできていなかったようです。

別の方法を試します。

2. Model.all.sample

モデルから全てのレコードを取得した上で1件抽出する感じ。
ゴリ押ししてみます。

items_controller.rb
def show
  #......省略

  @random1 = Item.all.sample
  @random2 = Item.all.sample

 #......省略
end
items/show.haml
- if @random1.present?
  =link_to item_path(@random1.id) do
    = @random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.id) do
    = @random2.name
- else
  .none

今回は配列でなくデータ単体を取ってこれたので、idsとかeach文なんてまどろっこしいことをせずに済みました。やったね。
気になる結果は......
Image from Gyazo
で!!!け!!!た!!!
random1とrandom2で別々の値が取ってこれてます。さすがゴリ押し。
これにて実装完了......と思ったその時。見つけてしまいました。

ActiveRecord でランダムなレコードが欲しい(1件、複数件)

『全件取ってランダムに1件取り出すのはダサい』

.........ダサい???

確かに全部とってランダムに1件はメモリやらなんやらに理解の浅い僕でも効率が悪いのは理解できます。
こういう時にindexが便利なのか......?

とにかくダサいのは嫌なので別の方法を試します。(終わりが見えない)

3. Model.offset(rand(Model.count)).limit(1)

記事の方法をパクり...参考にさせていただきました。ありがとうございます。
レコード未満のランダム生成された整数をoffsetでレコード取得の開始位置とし、1個だけデータを取ってくる感じです。

items_controller.rb
def show
  #......省略

  @random1 = Item.offset( rand(Item.count) ).limit(1)
  @random2 = Item.offset( rand(Item.count) ).limit(1)

 #......省略
end
items/show.haml
- if @random1.present?
  =link_to item_path(@random1.ids) do
    - @random1.each do |random1|
      = random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.ids) do
    - @random2.each do |random2|
      = random2.name
- else
  .none

......each文でないと取り出せませんでした......idsも復活しました......
しかしoffsetで開始を指定している分、Model.all.sampleの時よりはスマートなデータ取得ができている気がします。
さて結果は......
Image from Gyazo
......ランダムなってるやん。
さすがスマート。

『自分のデータは除いて......』の実装はしていないのでGIFでは可愛いうさぎが延々と出現してしまっていますが、
きちんとランダムにリンクが生成されています。

これで終わりにしようと思いましたが、僕は根に持つタイプなのでダサくない方法をもう一つ考えてみます。

4. Model.where('id >= ?', rand(Model.first.id..Model.last.id)).limit(1)

offset、 limitを使った書き方の書き換えとしてwhereに置き換える方法が紹介されていたので、試してみます。
randの引数でランダム数値生成の範囲指定を行いますが、今回は先頭レコードと最終レコードのidを範囲として指定しています。

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

items_controller.rb
def show
  #......省略

  @random1 = Item.where('id >= ?', rand(Item.first.id..Item.last.id)).limit(1)
  @random2 = Item.where('id >= ?', rand(Item.first.id..Item.last.id)).limit(1)

 #......省略
end
items/show.haml
- if @random1.present?
  =link_to item_path(@random1.ids) do
    - @random1.each do |random1|
      = random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.ids) do
    - @random2.each do |random2|
      = random2.name
- else
  .none

こちらもeach文、idsを用いて配列からデータを取り出して出力しています。
結果は......
Image from Gyazo

ランダムになってました。満足。

比較

rails consoleからSQL文とSQL発行の所要時間が確認できるので、
上記4つのものを確認・比較してみます。

1個目
スクリーンショット 2019-12-21 16.06.59.png
2個目
スクリーンショット 2019-12-21 16.06.08.png
3個目
スクリーンショット 2019-12-21 16.07.41.png
4個目
スクリーンショット 2019-12-21 16.08.35.png

......DBにデータが7つしかないので速さは大差ないですね。そりゃそうか。
SQL文は4個目では3回発行されています。あまりよろしくないですね......
スマートな実装としては3個目推奨でしょうか。

という訳でseeds.rbを読み込んでデータを1000件作ってみました。

seeds.rb
1000.times do |index|
  Item.create!(name: "アイテム#{index}",
              seller_id: 1, 
              description: "内容#{index}", 
              category_id: index,
              condition_id: index, 
              prefecture_id: index, 
              sendingmethod_id: index, 
              postageburden_id: index, 
              shippingday_id: index, price: index, 
              profit: index)
end

スクリーンショット 2019-12-21 22.39.10.png
もう一度検索スピードを検証してみます。

1個目
スクリーンショット 2019-12-21 22.41.53.png
2個目
スクリーンショット 2019-12-21 22.43.45.png
3個目
スクリーンショット 2019-12-21 22.45.02.png
4個目
スクリーンショット 2019-12-21 22.46.11.png

SQL発行の所要時間に大きく差が出ました。
1個目、2個目は3個目、4個目に比べて10倍以上の時間がかかってしまっています。
クエリ文が3回発行されてしまっていることを考えると、
今回試した手法の中ではoffsetを用いた3番目の方法がベストプラクティスとなります。

おわりに

今回はSQLについて理解を深めることができました。
Indexを用いた検索でも検証を行ってみたいですね。
大規模なWebアプリケーションになればなるほど『どんな手法で検索をするか』で処理スピードに大きな差が生まれてしまうことを実感できました。

また気が向いたらなんか書きます。

おわり。

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

しまうまプリントからEPSON Photo+への住所録の移行

背景

  • これまでプリンタを自宅に持ってなかった1が、今年嫁がどうしても欲しいとのことでEPSONのプリンタ Colorio EP-881AB を購入した
  • これまで年賀状はSaaSの しまうまプリント を利用していたが、せっかくプリンタを購入したので今年からは自分で印刷してみようと思った
  • EPSONプリンタ用の無料アプリとして EPSON Photo+ というものがあり、これを使えば年賀状が簡単に作成/印刷できる

ということで、しまうまプリントからEPSON Photo+へ住所録を移行する必要があり、format変換用のruby scriptも書いたので記録しておく。
(やる前はまー普通にできるやろと思ってたが、 EPSON Photo+の住所録用CSVフォーマット仕様が開示されておらず 、ヘルプデスクと何往復かのメールのやり取りが必要だった。。。)

Software version

Windows

  • Windows 8.1 Enterprise
  • EPSON Photo+: Version 3.1.0.0

Mac

  • macOS Catalina(version 10.15.1)
  • ruby 2.3.5p376 (2017-09-14 revision 59905) [x86_64-darwin18]

やり方

しまうまプリントからの住所録ダウンロード

  • ここ の 「宛名データのダウンロード」ボタンをクリックすることで xlsx ファイルをダウンロードできる (address_data.xlsx)
  • 手持ちのExcel等でダウンロードしたファイルを開き、CSVファイル形式で保存する (ここではaddress_data.csvという名前で保存されたと仮定する)

(参考) しまうまプリントのCSVフォーマット

headerの各カラムを列挙したのが以下。

印刷(する・しない)
グループ※必須(全角30文字まで)
送付先※必須(自宅・会社)
姓※送付先自宅の場合必須 (全角10文字まで)
名※送付先自宅の場合必須 (全角10文字まで)
姓カナ(全角10文字まで)
名カナ(全角10文字まで)
会社名※送付先会社の場合必須(30文字まで)
会社名カナ(全角30文字まで)
部署名(全角20文字まで)
役職名(全角20文字まで)
敬称※必須※送付先自宅の場合(様・殿・先生・君・ちゃん・さん・御中)※送付先会社の場合(御中・様・殿・先生・御一同様)
郵便番号※必須(7桁)
都道府県※必須
住所1※必須(全角20文字まで)
住所2(全角20文字まで)
住所3(全角20文字まで)
連名1姓※送付先自宅の場合のみご利用可能(10文字まで)
連名1名※送付先自宅の場合のみご利用可能(10文字まで)
連名1敬称※送付先自宅の場合のみご利用可能(様・殿・先生・君・ちゃん・さん・御中)
連名2姓※送付先自宅の場合のみご利用可能(10文字まで)
連名2名※送付先自宅の場合のみご利用可能(10文字まで)
連名2敬称※送付先自宅の場合のみご利用可能(様・殿・先生・君・ちゃん・さん・御中)
連名3姓※送付先自宅の場合のみご利用可能(10文字まで)
連名3名※送付先自宅の場合のみご利用可能(10文字まで)
連名3敬称※送付先自宅の場合のみご利用可能(様・殿・先生・君・ちゃん・さん・御中)
その他連名※送付先自宅の場合(ご家族御一同様)※送付先会社の場合(スタッフ御一同様、従業員御一同様、職員御一同様、の皆様へ)

マルチバイト文字(日本語)が入っているかつ一行が長いのでプログラムとしては扱いづらい。
以下script内では列数をハードコードしているため、もし上記フォーマットが変更されていたら、合わせて以下scriptのハードコード部分の列数を変更する必要がある

住所録のformat変換

  1. script をローカルにcopy
  2. 以下を実行
$ chmod +x convert_shimauma_to_epson.rb
$ ./convert_shimauma_to_epson.rb address_data.csv

これで output.csv というファイルが同じディレクトリに生成されます。

EPSON Photo+にインポート

ここ にインポート手順が記載されています。

既知の問題

  • 連名1については敬称1で指定した値がちゃんと反映されるが、連名2以降の敬称が反映されない。
    • 私の場合連名2以降が殆どなかったためimportしてから手入力してしまった。
    • もしここも変換したい方は是非EPSONのヘルプデスクに問い合わせて頂きたい。わかったらこちらにもコメントつけてもらえれば本記事も更新します。

(2019-12-22 更新)

うおー、outputの 住所1 は30文字制限なのかよ!
CSV importするときはエラー等出ず無条件で後ろが切られる模様。30文字超える場合はちゃんと 住所2 (マンション名等入力する欄)に入れてあげるようにせねば・・・。めんどくさい。
すでに印刷してしまったんだが。。。。

(2019-12-22 更新2)


  1. そもそも自宅では殆どプリンタなんて使わなかった(電子ファイルで事足りる)し、必要であればコンビニ等のネットプリントを利用していた 

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

2019年フレームワークのトレンドが見れるサイトの紹介

hotframeworks.com

URLはこちらになります。
http://hotframeworks.com/
a.jpg

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

Codeanywhereを使ってスマホでプログラミングをしよう!

前置き

こんにちは

通勤時間などのスキマ時間で、スマホでプログラミングの勉強が出来ないかと思いcodeanywhereというアプリを使ってみました。
課金が前提となりますが、結論としてはかなり使えます

codeanywhereは、いわゆるクラウドIDEというもので、ブラウザさえあればどんなマシンでもプログラミングができますよという代物です。
似たような製品にAWSのCloud9がありますが現時点でそちらはスマホ対応してないみたいです。

今回は、codeanywhereを使ってスマホのみでRubyのハロワをする手順を解説します。
(ちなみに、このQiitaの記事自体スマホから投稿してます。)

なおcodeanywhereのプランには無料のプランもあるのですが、7日間しか使えないので諦めて課金しましょう。
有料プランにも複数ありますが、僕はFreelancerを選択しました。
8ドルとの事ですが、日本円だと2019/12時点で1,116円でした。
BE560AD4-5052-4265-8858-189772492EB7.png

手順

まずは、プロジェクトを作成します。

0561B85F-785D-4ACB-8180-A4878CB3393E.jpeg

作成したプロジェクトの画面の右下の+ボタンを押します。
C6C2CBE2-1871-4C0F-9041-290C55B0EB40.jpeg

New connectionを選択
BD954288-7CA2-47BF-8B71-6E9B9F1C3380.jpeg

countainerを選択します。
余談ですがcloud9だと環境がEC2インスタンスだったのですが、こちらではコンテナとなります。
FBCE1048-371A-446C-9CB9-EF391202AAC6.jpeg

今回はRubyのコードを書きたいのでRubyのコンテナを選択します。
とりあえずUbuntuを選択します。(CentOSでも、お好みで。)
D876DD9B-225D-4854-A2B3-9BD1150FB8BF.jpeg

適当に名前をつけてCreateを押します。
81DF48C6-81D7-4A13-95EA-8683930E448F.jpeg

Deploying containerと表示されます。
しばらく待ちましょう。
74D53C92-382B-460B-8E77-FB6BEB9ABA76.jpeg

コンテナが出来たら、こんな風に詳細が表示されます。
RVMもRuby on Railsも最初から入ってるみたいです。
572125B0-89A7-4B10-B17D-AACE793C4198.jpeg

作成したコンテナをターミナルで操作してみましょう。
コンテナの画面の右にある、縦に並んだ・をクリックしてメニューを表示します。
SSH Terminalを選択します。
0AAC8E41-4158-4C65-B4AC-5AE6A157CBF7.jpeg

↓このようにターミナルが操作出来ます。
rubyやrailsのコマンドも、ちゃんと通る事を確認できます。

ruby -v

C37DBC21-1665-4F79-BFE7-E494A2237B6F.jpeg

rvm list knownで、使えるRuby version の確認もできますね。
B1CC9957-268B-4FBD-9DB5-C3BE5CE178B3.png

さて、コンテナの名前をタップして中身を確認します。
6782D8B1-5CBC-4682-8A19-8AACB1B9BA80.jpeg

まだ何も作って無いので、空ですね。
7D35CD69-9E1F-42AF-AC93-472D14E6A307.png

Codeanywhereの画面からrubyファイルを作れないか試したのですが、ちょっとやり方がわからなかったので↓のようにターミナルから作成します。

touch index.rb

6F38A3FE-7D4B-45C8-94E3-33AEB525FDBD.png

↓の通り、普通に作成出来ました。
26B748DF-1C1E-4375-B2CC-C6B9C6E30330.png

ハロワを入力してあげます。

puts “Hello world”

C7C4D88F-C53E-4DC2-928C-FAFDD2BA1CE8.png

右上の・でメニューを開いて、保存しましょう。
179B932F-B5ED-43D1-B491-F5177A326D3E.jpeg

右上の番号のついた□で、タブを移動できます。
ターミナルのタブに移動して、↓の通りRubyファイルを実行します。

ruby index.rb

3B584648-0579-46E5-896F-3AFF3DDBFADE.jpeg

↑の通り、普通に実行できましたね。

以上

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

[Ruby on Rails]slickの矢印の画像を変更する方法[JQuery]

実現したいこと

slickの画像(スライド)を次に送るための両側の矢印の画像を好きなものにする。

色々参考にしたがうまくいかなかったので、自分がうまくいったやり方を残しておきます。

前提

Ruby on Railsで開発を行っています。slickは導入できているものとします。
Rails 5.2.4
ruby 2.5.1
jquery-rails 4.3.5

手順

画像の用意

矢印にしたい画像をapp/asset/imagesフォルダに入れます。

矢印のデフォルト画像の変更

おそらく\2190と\2192の部分が、矢印のデフォルトの画像を指定している部分だと思います。なので、この部分を任意の矢印の画像に取り替えます。

slick-theme.scss
変更前
$slick-prev-character: "\2190" !default;
$slick-next-character: "\2192" !default;

変更後
$slick-prev-character: image-url("left.png") !default;
$slick-next-character: image-url("right.png") !default;

矢印の表示と位置調整

位置の調整は以下の記事を参考にさせていただきました。
https://qiita.com/milneo/items/3560cb01cba92c2ccb6f
これで、矢印が表示されたと思います。slider.scssはslickに対してcssを記述しているファイルです。名前などは人それぞれ違うと思うので気をつけてください。

slider.scss
.slick-prev{
  left: 50px;
  height: 80px;
  width: 80px;
  z-index: 10;
}

.slick-next{
  right: 50px;
  height: 80px;
  width: 80px;
  z-index: 10;
}

最後に

なんとか、矢印の画像の変更はできました。
ただ、まだコードの意味や自分が行ったことの意味を理解していないので、もっと勉強しなけらばなと思いました。

参考

https://qiita.com/milneo/items/3560cb01cba92c2ccb6f

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

ランダムで”いい感じ”にグループ分けしてSlack通知するbotをRubyで開発してみた

image.png

はじめに

こちらはSlack Advent Calendar 2019の21日目の記事となります。

今回は、仕事を通じて作成したグループ分けのbotについて詳しく書いていこうと思います。

当初は「悪用厳禁! Slack活用における情報収集 黒魔術」というタイトルで、Slackを導入している企業や組織での情報収集の小ネタについて書こうと思いましたが、転職して日が浅いのでブラックなジョークは止めておこう(※違法ではないです)という保守的思考が働いたのと、どうせならエンジニアらしいことを優先して書きたいと考えたためお蔵入りとなりました。

そちらの方も、今後機会があればコラム的な感じで書いていきたいと思います(リアクション頂ければ書くためのモチベーションが高まります)。

背景・目的

作るに至った理由

元々は前職での”社内のシャッフルランチのグループ分け”を補助するために、個人的に開発したものです。

グループ分けを簡単に行うことができるWebサービスはもちろん存在していましたが、ポチポチする時間が長く工数が掛かったり、メンバーの抜け漏れが多いという課題がありました。

そこで、コマンド一発でグループ分けを実行するスクリプトを一度書いてしまって、業務負荷を減らせないかと考えたことが背景にあります。

”いい感じ”とは?

自前で実装した理由の一つに、「剰余をグループにしない」というものがありました。

例を挙げて少し詳しく説明します。全員で27人のチームがあり、ランチのためにチームを5つのグループに分けたいとしましょう。この場合「6人グループを2つと、5人グループを5つ」がいわゆる”いい感じ”に当たります。

しかし、様々なグループ分けのコードをググって探してみると、「6人グループが4つと、3人グループが1つ」というような、余りで構成されたグループが出来上がってしまうものが多くありました。

これでは各グループで均等にメンバーが割り振られず残念なランチになってしまうので、そのような”余りものグループを作らない”という実装を行いました。

ソースコード

image.png

※社内ランチの社内呼称が”クアトロランチ”だったため、リポジトリ名もそれにあやかる形になっております

実装

依存関係・バージョン

スクリプト

以下のスクリプトをコマンドラインから実行することで、グループ分けを行います。

シャッフルをしている部分は、27行目members.shuffleメソッドの部分です。あと、個人的な嗜好によりメソッドを細かい責務で分けておりますので、読みづらい方も居るかと思います。

script/slack.rb
require 'slack-ruby-client'
require 'dotenv'
require 'csv'

Dotenv.load

today = Date.today.strftime("%Y/%m/%d")
groups, members = [], []
numbers_of_group = ENV['NUMBERS_OF_GROUP'].to_i
comment = ENV['COMMENT'].to_s
channel = ENV['CHANNEL'].to_s

def setup_slack
  set_slack_api_token
  client = Slack::Web::Client.new
  return client
end

def set_slack_api_token
  Slack.configure do |conf|
    conf.token = ENV['SLACK_API_TOKEN']
  end
end

def load_members(members)
  read_csv(members)
  members.shuffle
end

def read_csv(members)
  CSV.foreach('data/members.csv') do |member|
    members << member
  end
end

def create_groups(numbers_of_group, groups, members)
  numbers_of_group.times { create_group(groups) }
  generate_csv_file(members, numbers_of_group, groups)
end

def create_group(groups)
  empty_group = Array.new
  groups << empty_group
end

def generate_csv_file(members, numbers_of_group, groups)
  grouped_members_csv = CSV.generate do |csv|
    split_members(members, numbers_of_group, groups, csv)
  end
  save_csv_file(grouped_members_csv)
end

def split_members(members, numbers_of_group, groups, csv)
  members.each_with_index do |member, i|
    number = i % numbers_of_group
    group = groups[number]
    assign_member_into_groups(group, number+1, member[0], csv)
  end
  push_groups_into_csv_file(csv, groups)
end

def assign_member_into_groups(group, number, member, csv)
  group << number unless group.include?(number)
  group << member
end

def push_groups_into_csv_file(csv, groups)
  groups.each do |group|
    csv << group
  end
  return csv
end

def save_csv_file(grouped_members_csv)
  File.open('data/grouped_members.csv', 'w') do |file|
    file.write(grouped_members_csv)
  end
end

def send_slack_api_with_csv(client, today, comment, channel)
  client.files_upload(
    channels: channel,
    as_user: true,
    file: Faraday::UploadIO.new('data/grouped_members.csv', 'text/csv'),
    title: "#{today} quattro groups",
    filename: 'grouped_members.csv',
    initial_comment: comment
  )
end

client = setup_slack
members = load_members(members)
create_groups(numbers_of_group, groups, members)
send_slack_api_with_csv(client, today, comment, channel)

コード:https://github.com/f-teruhisa/quattro-bot/blob/master/script/slack.rb

.envファイル

以下の .env ファイルに設定情報を追記し、ルートディレクトリに格納しておきます。

SLACK_API_TOKEN= 
NUMBERS_OF_GROUP=
CHANNEL=
COMMENT=

設定する情報はそれぞれ以下の通りです。
envファイルのため、文字列をクォートで囲ったりする必要はありません。

SLACK_API_TOKEN=後述するSlackのAppを作成し取得するAPIトークン
NUMBERS_OF_GROUP=グループの数
CHANNEL=Slackのチャンネル(#は省略)
COMMENT=Slack通知する際に加えたい文章

ちなみに、COMMENT=Slack通知する際に加えたい文章に関しては、改行を含む場合のみシングルクォーテーションで囲むことで、改行を含んだメッセージを表現することができます。

csv

このスクリプトを実行するにあたり、以下の2つのcsvファイルを活用します。

members.csv

dataフォルダに、元データとなるグループ分けする対象のmember.csv ファイルを格納します。以下のサンプルデータの通り、1つのデータを1行1カラムで格納してください。

members(sample).csv
Fukumoto
Tanaka
Yamamoto
Inoue
Aida
Ueda
Suzuki
Sato
Nakamura
Fujioka
Ito
Hara
Ohara
Yamasaki
Kawai
Kondo

※元が社内ランチ用のプログラムなためmemberという名前になっています

grouped_members.csv

こちらは、スクリプトを実行した後に自動生成されるcsvファイルです(よって最初は存在しないファイルです)。

元データをグループ分けした結果が、grouped_members.csvとして吐き出されます。Slackにはこのcsvファイルをメッセージに添付してPOSTするという形を取っております。以下のサンプルデータは、先程のmember.csvを5グループに分ける設定をし実行してみた結果です。

grouped_member(sample).csv
1,Kawai,Yamamoto,Nakamura,Inoue
2,Tanaka,Yamasaki,Ueda
3,Fukumoto,Hara,Ohara
4,Aida,Fujioka,Ito
5,Sato,Suzuki,Kondo

便宜上”グループ○○(数字)”という指定ができコミュニケーションが円滑に行われるように、各グループに番号を振っています。また、番号を振ることで、番号を先頭とした序列が各グループ内で暗黙的に作られるように設計しました。

※参考:行動経済学「ナッジ」は政策を変えるのか?

実行手順

基本的にはGithubに乗せています...が、英語なのとSlackの操作までは記載しておらず、若干不親切なのでこちらで詳細に記載します。

SLACK_API_TOKENの取得」と「ワークスペースへのアプリケーション導入」の2つがゴールとなります。

Custom IntegrationsからSlack botを作成

Slackで開発できるbotにはいくつか種類があり、設定が複雑なので以下より画面を追って説明していきます。

image.png

今回は「Custom Integrations」からbotを開発していきます。
ちなみに、以下のような画面です。

image.png

上記の「Bots」をクリックすることで、Slackに登録するbotを設定していくことが可能です。

ちなみに、Slackのワークスペースが分かる場合は、以下のURLから直接遷移することができます。

https://{Slackのワークスペース名}.slack.com/apps/manage/custom-integrations

SLACK_API_TOKENを取得

「Bots」をクリックすると、以下のような画面が出てきます。
「新規作成」と「編集」のどちらでも、SLACK_API_TOKENは取得できます。

image.png

「新規作成」の場合はbotの名前を入力した後に、「編集」の場合は直接以下の画面に遷移するので、この画面でSLACK_API_TOKENを取得できます。後ほどenvファイルに書き込むので、コピーしておきましょう。

image.png

リポジトリをcloneする

冒頭に紹介したリポジトリを、ローカル環境の任意のフォルダにcloneしてください。

$ git clone https://github.com/f-teruhisa/quattro-bot.git

image.png

.envファイルに設定情報を記載

前半に紹介した.envファイルにSLACK_API_TOKEN含む各設定情報を追記し保存してください。グループの数やメッセージの内容は任意のもので構いません。

csvファイルに元データを格納

data/members.csvに、前半に紹介した通り、グループ分けしたいデータを一列に格納し保存してください。

ワークスペースへのアプリケーション導入

作成したbotからSlackの任意のチャンネルに通知されるように、アプリケーションを導入しましょう。

通知を飛ばしたいSlackのチャンネルに入り画面の左上、チャンネル名をクリックするとメニューバーが現れます。
このメニューから「Add an app」をクリックし、作成したbotを導入します。

image.png

「Add an app」をクリックすると以下のような画面が表示されるので、先程設定したbotの名前で検索すると表示されるはずです。選択して「Add」を選びましょう。

image.png

これで、Slack側でもbotから通知を受けるための準備が整いました。

コマンド実行

さて、ここまでで準備は終わりです。
以下のコマンドを実行して、グループ分けとSlack通知を行いましょう。

$ ruby script/slack.rb

ちなみに、実行には依存gemが必要ですがGemfileにまとめておりますので、そちらを参考にしてgemのインストールも合わせて行ってください。以下のコマンドを一度に実行すれば、インストールが完了するかと思います。

$ gem install dotenv\
      csv\
      slack-ruby-client 

Slack通知

設定した情報が正しければ、以下のようにSlackに通知が飛んでくると思います。
一度設定してしまえば、あとはコマンドを実行するだけなので、グループ分けに悩まずに済みます。

image.png

※実際に、仕事でアップデートを担当するgemを分けた時に使った通知メッセージ

今後考えていること

それなりに使う機会に恵まれたbotですが、以下のような課題感もあるので、今後ヒマがあれば徐々に対処していきたいです。

定期運用に向いていない

今回のbotはインスタントなグループ分けには重宝するのですが、部署や名前などの属性は考慮していないため、普通にガンガン衝突します。そのため、(偶然ですが)同じヒトやモノと何度も同じグループになることが多々あります。

衝突を防ぐためには、属性値の考慮や過去の結果を格納するDBを用意するなどの工夫が考えられます。

また、コマンドを叩かないとグループ分けやSlackへの通知が行われないため、実行者が忘れたり寝坊すると死にます。

これは、後にも説明するGoogle App Script(GAS)への移行やLamda化などで対応が可能であると考えておりmす

GUIで実行できない

工数を考慮しGUIは実装しなかったのですが、ターミナル操作を行うため非エンジニアのメンバーが利用するハードルは高いです(一応セットアップと実行するコマンドを教えれば事足りますが)。

この辺は軽めのUIを作って、herokuにデプロイすることでブラウザでの操作を行う拡張ができるでしょう。

csvの管理工数

社員情報など、マスタ情報がスプレッドシートやエクセルで管理され、氏名が縦に並んでいる(≒まんまコピペしてmember.csvに格納できる)ことを前提にcsvからデータを取り出す処理を書いていますが、「新入社員など新しいデータが必要な際に追加を忘れ、ナチュラルにハブってしまう」可能性があります。

これは、「(前職で)社内にRubyエンジニアが多くメンテナンスしやすいだろう」という安易な理由でRubyを選定したことがやや裏目に出てしまいました。

マスタデータをスプレッドシートで管理している会社も多い(前職はそうでした)ので、GASで実装するほうがシンプルに保てたと感じます(Git管理しにくい、スプレッドシートに依存するというデメリットはありますが)。また、GASではサーバレスに定期実行を行うことも可能なので、先に上げた”定期運用に向いていない”という課題にも対処できます。

さいごに

最後までお付き合い頂きありがとうございました。

筆者はこのbotスクリプトの開発が、人生初のいわゆる”コーポレート・エンジニアリング”だったのですが、自分の書いたスクリプトを何度も使ってもらい、実際に価値を生んでいる瞬間を何度も見ることができるのは、エンジニアとしても成功体験であったと振り返っています。

これからも「イケてないなあ...この作業」というプロセスを見かけたら、積極的にハックしていけるような精神を大事にエンジニアとして成長していきたいと思いました。

みなさんもSlackというすばらしいチャットツールを最大限に活用し、ときにはハックしてみると面白い経験ができるかもしれません。ぜひ試してみてください。

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

まつもとゆきひろさんの講演【若手エンジニアが市場価値を上げるために必要なこと】まとめ

こんにちは。KENと申します。

タイトルの通り、まつもとゆきひろさんさん(本文中ではMatzさんと呼ばせていただいています)の講演会にオンラインで参加してきましたので、そこで仰っていたことを忘れないようにまとめようと思います!

この講演の目的

  1. 参加者にエンジニアとして今後を考えてもらう。
  2. 参加者に何かを始めるきっかけを与える。

そもそもなぜ市場価値を高めた方が良いのか?

市場価値が高いと得をする。
主に下の2点で得をする。

  • 給料を上げられる。(正しい判断をしてくれる会社であれば)
  • ストレスを減らせる。イライラすることを減らせる。

なぜストレスが減る?

本当にやめるかどうかは別としていつでも今の会社を辞められる状態になれるから。
→我慢しなくても良くなる。つまり心理的安全を確保できる。
→笑顔で働くことができる。

Matzさんオススメのストレスを減らす技

There must be a reasonを3回唱えること!
例えば運転中後ろの車にすごく煽られて、しまいには猛スピードで抜かれた時。イライラしますよね。
そういうときに、「あーあの人はたぶん、今会社に遅刻しそうで、遅れたらこっぴどく?られるから急いでいるのかな?」など理由を考えてみる。
こうすることでイライラする自分に言い聞かせることができる。

では市場価値とは何か

自分の価値?スキル?知識?

市場価値とはつまり他人からの評価です。
市場価値 = 他人からの評価

つまり自分で市場価値をコントロールするのは不可能に近い。
相手目線の判断だから。
自分の視点と他人の評価にはギャップがある。
だからスキルさえ上がれば市場価値が上がるというのは幻想にすぎない。

Rubyも同じ。
MatzさんがRubyのヒットをコントロールすることはできなかった。

コントロールはできないけど、市場価値を高める努力はできる
MatzさんがRubyを作る際にできたのは精一杯良い言語を作ることのみ。
つまり「人事を尽くして天命を待つ」!

特に下記の要素を得る努力はとても大事。

  • インデックスを作る(誰に聞けば、どう調べればその情報を得られるのかを知っていること)
  • 経験
  • キャリア (掛け合わせは強い エンジニアの後エンジニア採用人事とか)
  • コネクション (例えばgoogleの採用。普通のルートだとコンピューターサイエンスのmaster持っていないと面接してもらえないが、リファラルだと持っていなくても面接してくれることもあったり。)

でもこれらは全部自分の視点になりがち。

市場価値を高める方法

  • インベントリ
  • コネクティング・ドッツ
  • 情報発信(Twitter, Qiita, GitHub, Quora, OSS Github)

①インベントリ

自分のことについての棚卸し。まとめをする。
つまりちゃんと自分興味、関心、自分と他人の違う点を知ろう。

他人との違いは大切

日本では同調圧力とかもあるが、同調圧力への鈍感さも大事。 
違うからこそ価値が生まれる!
もちろん労働の管理者側からすると、同じような人材である方が管理しやすいし、交換しやすいという同質性のメリットはある。

でも同質性を重視する組織とはWin Winの関係が築けない。
Matzさん的には同調圧力への鈍感さを伸ばし、違いを認めてくれる企業とのWinWinの関係性を作ることをオススメする。

7つの習慣によると持続可能な取引は2種類のみ。Win WinかNo Deal。つまり同質性を重要視するような企業とはWin Loseになる必要はない。No Dealで良いのだ。

②コネクティング・ドッツ

スティーブ・ジョブズの有名な言葉。
好きなことや関心ごとを追求していった結果、それが繋がって線となり何かが新しいモノが生まれる。

つまり自分の興味、関心をインベントリで知ったら、それを全力でやろう。

Matzさんの場合
プログラミング、心理学への興味があり、英語などもたまたま使う機会に恵まれて勉強していった。そういう組み合わせがあってRubyが生まれた。まさにコネクティング・ドッツの人生。実は高校時代、理系なのにも関わらず英語、国語が得意で、数学や物理は苦手分野だった。

③情報発信

何か行動し始めたら、それをTwitterでも何でもいいから外に発信しよう。
他人の視点に立ち、あなたが人を採用する側だとすればどんな人が欲しいだろうか?と考え、自分を魅力的に見せることを意識しよう。

あなたが全く外に発信してもないのに、あなたの才能をたまたま見出してくれるという漫画のようなことはまず起こらない。。
だから他人があなたを見つけられるように、発見してもらえるように発信しよう。

ちゃんと魅せる相手を選ぶ。

ちゃんとWin Winの関係を築けるところ(会社など)にアピールしよう。

相手を侮ったり、下に見てはいけない。
全員を自分よりスゴイ所を持っていると思っておく。

すごく大切なあと二つのこと

①笑顔

笑ってみよう
ゴキゲンでいよう。あなたの魅力がプラスになる。

②ストーリーの力

自分のエピソードを紐づけて話す人は色んな人を惹きつける力がある。
話が魅力的な人は自分が体験したストーリーをたくさん持っている。ネタ帳を持とう!

まとめ

講演に参加する前は、市場価値なんてスキルさえ上げれば上がるんじゃないかと安直に考えていました。
しかし価値や評価というのは自分ではなく、他の人が決めるもの。
いかに自分という人間を他人に伝えるのか。この大切さを改めて認識できた良い機会でした。

では最後まで読んでくださりありがとうございました!!

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

herokuにruby on railsアプリをmysqlを使用してデプロイする。deploy

前提条件

nodeとyarnは必ず最新にしておいてください。
それは書きません。

プロジェクトの作成

noracorn-railsというプロジェクトで作成します

ghq get https://github.com/noracorn/noracorn-rails.git
rails new noracorn-rails
cd noracorn-rails

scaffoldで、適当にサイトを作成

rails generate scaffold person name:string age:integer
rake db:migrate

トップページをいったんscaffoldで作成したものにする

noracorn-rails/config/routes.rb
Rails.application.routes.draw do
  resources :people
  root 'people#index'
end

立ち上がることを確認しましょう

以下のコマンドを打ってから、http://localhost:3000/にアクセス
Personのscaffoldにアクセスできることを確認する

bundle install
rails server

herokuでアプリを作成する

heroku login
heroku create noracornrails

herokuにmysqlを追加

(herokuにクレジットカードを登録してないと追加できません。)

heroku addons:add cleardb:ignite -a noracornrails

database設定

noracorn-rails/database.yml
production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>

以下のコマンドを打つと、データベース接続情報が表示されます。

heroku config

こんな感じで表示されます

mysql://user_xxx:password_xxx@host_xxx/dename_xxx?reconnect=true

表示された接続情報をherokuの環境変数に入れていく

heroku config:set DATABASE_URL="mysql2://user_xxx:password_xxx@host_xxx/dename_xxx?reconnect=true"
heroku config:set DATABASE_USERNAME="user_xxx"
heroku config:set DATABASE_PASSWORD="password_xxx"

config.assets.initialize_on_precompile = falseを、application.rbに入れる

noracorn-rails/config/application.rb
module NoracornRails
  class Application < Rails::Application
    config.load_defaults 6.0
    config.assets.initialize_on_precompile = false
  end
end

production.rbのconfig.assets.compileをtrueにする

noracorn-rails/config/environments/production.rb
config.assets.compile = true

Gem Fileを編集する。

ローカル環境とテストでは、sqlite3を使用してherokuではmysqlを使用するようにした。
上のほうで定義されているsqlite3は、コメントアウトしておいてください。

#gem 'sqlite3', '~> 1.4'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'sqlite3', '~> 1.4'
end

group :development do
  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'sqlite3', '~> 1.4'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :production do
  gem 'mysql2'
end

bundle installする

bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include"

bundle install

すべてコミットして、herokuにpush

git add .
git commit
git push origin master
heroku git:remote --app noracornrails
git push heroku master

herokuのmysqlをマイグレーション

heroku run rails db:migrate

アプリが動いているか確認する

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

Ruby on Railsでドメイン駆動設計をやってもいいじゃないか。

挨拶

松下と申します。みんなの株式を運営しています。

ドメイン駆動設計の導入した動機

最初にドメイン駆動設計を導入しようとしたのは2017年の末頃でした。

ドメイン駆動設計を導入しようとした最初の動機はフレームワークやgemに依存しないPORO(Plain Old Ruby Object)なコードを増やす事によって、保守性(テスト容易性、理解容易性、変更容易性)を高めようとした事だったと思います。

10年近くRuby on Railsのアプリを無計画に実装を繰り返した結果、肥大したコントローラやモデルやヘルパー、改修する事による影響範囲が読めないライブラリや自作gemなどが散見され、テストのカバレッジも酷い状況でした。

最初はデザインパターンを部分的に導入してみたりもしてみましたが、レイヤードアーキテクチャを元にしたドメイン駆動設計をしっかりやろうという事になるまではそれほど時間はかかりませんでした。

現在ではバッチ処理や複雑なドメインは単体でのマイクロサービス化する事で本体アプリをスリムにする事を平行で行いつつ、クリーンアーキテクチャのオニオンアーキテクチャを参考にした本体アプリケーションのリファクタリングを進めています。

ユビキタス言語とリファクタリング

ドメイン駆動を導入するに当たって大事にしようと決めた事は、チームの全員がユビキタス言語を意識して使う事と、ドメイン知識は納得いくまで話し合いながら実装する事です。

ユビキタスは日本語名と英語名(クラスやメソッドとして利用する)の両方の視点からDomainやValueObjectを細かく決め、スプレッドシートで管理しています。

一度決めたユビキタス言語でも、必要があって全員の同意が取れれば、必要に応じてアップデートします。(その場合はソースも全て書き換えるのでそれなりの作業にはなります)

それまで各技術者が自由に実装していたのですが、共通の理解を得るための話し合いをする時間が増えました。設計レビューもしっかり行いましたが、一旦形にした後でも、納得がいかなければ何度も話し合いながらリファクタリングを行う事が当たり前になりました。

ディレクトリ構成と登場クラス

だいたい下記の様に配置しています。

app/
 ├─ assets/      # CSS
 ├─ controllers/ # コントローラ
 ├─ domains/     # ドメイン
 │   └─ users/
 │       ├─ user.rb       # ドメインモデル
 │       ├─ factory.rb    # ファクトリ
 │       └─ repository.rb # レポジトリ
 ├─ helpers/     # ヘルパー
 ├─ models/      
 │   └─ user_record.rb # データモデル
 ├─ services/    # アプリケーションサービス
 ├─ use_cases/   # ユースケース
 │   └─ users/
 │       ├─ show_interactor.rb # インタラクタ
 │       ├─ show_presenter.rb  # プレゼンタ
 │       └─ show_view_model.rb # ビューモデル
 └─ views/       # ビューテンプレート
     └─ users/
         └─ show.html.erb

Controller

class UsersController < ApplicationController
  def show
    @vm = Users::ShowInteractor.new.call(params[:id].to_i)
  end
end

コントローラからはユースケースに合わせたインタラクタを呼びます。
一つの画面に複数のユースケースが有る場合は複数のインタラクタを呼びます。

CleanArchitecture的にはインタラクタの返り値はvoidにしてオブザーバパターンなどでビューを描画した方が良いのかもしれませんが、
フレームワークの制約で描画処理(render)が一度しか呼べないといった事もあり、インスタンス変数にビューモデルをアサインしてテンプレート側で表示する様にしています。

Interactor

class Users::ShowInteractor
  include UseCaseInteractor

  def initialize(params = {})
    @repository = params.fetch(:repository, Users::Repository)
    @presenter  = params.fetch(:presenter, Users::ShowPresenter)
  end

  # @param [Integer] user_id
  # @return [Users::ShowViewModel] 
  def call(user_id)
    user = @repository.find(user_id)
    @presenter.new.call(user)
  end
end

コンストラクタで依存性の注入をできる様にしているのでテストが書きやすくなります。
同じドメインを取得しても表示側の都合が違う場合はプレゼンタを置き換える事で表示側の都合だけを切り替える事ができます。
複数のレポジトリからドメインモデルを集める必要が有る場合はDTO(OutputData)を組み立ててプレゼンタに渡す場合もあります。

Repository / Factory / User / UserRecord

class Users::Repository
  include DomainRepository

  def self.find(id)
    record = UserRecord.find_by(id: id)
    Users::Factory.build(record)
  end
end

class Users::Factory
  include DomainFactory

  def self.build(record)
    Users::User.new(
      nickname: Nickname.new(record.nickname),
      age: to_age(record.birthday),
    )
  end

  private def to_age(birthday)
    age = ((Time.zone.now - date_of_birth.to_time) / 1.year.seconds).floor
    Age.new(age)
  end
end

class Users::User
  include DomainEntity

  attr_reader :nickname, :age

  def initialize(params)
    @nickname = params.fetch(:nickname)
    @age = params.fetch(:age)
  end
end

class UserRecord < ApplicationRecord
  self.table_name = 'users'
end

レポジトリはドメインモデル(集約)を返します。
ActiveRecordはDAOとしてのみ利用していています。
複雑なテーブル取得の条件が必要な場合はQueryObjectクラスを作成する事もあります。
ValueObjectも必要に応じて作成しています。

Presenter / ViewModel

class Users::Presenter
  include UseCasePresenter

  def call(user)
    Users::ViewModel.new(
      nickname: with_nickname_title(user.nickname),
      age: with_age_unit(user.age)
    )
  end

  private def with_nickname_title(nickname)
    "#{nickname}さん"
  end

  private def with_age_unit(age)
    "#{age}歳"
  end
end

class Users::ViewModel
  include UseCaseViewModel

  # @return [String] ニックネーム
  attr_reader :nickname

  # @return [String] 年齢
  attr_reader :age

  def initialize(params)
    @nickname = params.fetch(:nickname)
    @age = params.fetch(:age)
  end
end

ヘルパーはViewのグローバルを汚染するのでなるべく利用しない様にしています。
UIの都合はプレゼンタに実装して、ビューモデルを組み立てます。
テンプレート側ではビューモデルの属性を表示するだけの実装になる様にしています。

Ruby on Rails と ドメイン駆動設計

Ruby on RailsのRailに乗る事こそが至上であるという方には、この様な取り組みを疑問視する方もいらっしゃると思います。

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end
end 

確かに今回のソースの例では、上記の様にコントローラに一行書いて、年齢の処理はモデルに書けばそれだけで同じ動きが得られます。

ですが、同じサービスを長く続けていれば、データプロバイダが変わる事によってテーブルの定義は何度も見直されますし、意図してこなかった用途で貯めたデータを再利用したいという企画も出ますし、外部のDBやAPIから直接データを取得して作るサービスなどを作るという事もでてきます。

データプロバイダ側の都合や既存データの設計上の都合に合わせてコントローラやビューを一々組み直す事には大変な労力がかかります。
改修による影響範囲が読めなければそもそも改修する許可すら出す事が難しいという事になりかねません。
保守性の高い状態を保つ為にはRuby on Railsのレールを外す必要は有ると私は思いますし、長期的には保守性を保つ事こそが開発スピードを上げるという事にもなるのではないでしょうか。

ドメイン駆動設計というと難しいイメージがあるとよく言われますし、エヴァンス本を読むと眠くなると言う話も良く聞きますが、重要なのは丁寧なオブジェクト指向とコミュニケーションとイテレーションです。

Rubyだってオブジェクト指向言語なんです。
静的型付け言語の良さが押される昨今の風潮も理解できますが、動的型付けだってしっかりチームでコミュニケーションを取って扱えば記述量を抑えた見通しのよいコードにできます。

スピード感重視でRuby on Railsでサービスを作って、
気がついたらコードが無秩序に肥大化してメンテナンスに困っているという事例はそれなりに有るのではないでしょうか。

それで幸せになれるなら、Ruby on Railsでドメイン駆動設計をやってもいいじゃないか。と思います。

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

マイグレーションファイルの作成、変更、削除まで

今回は、物件のファイルを作ることを想定します。

流れ

マイグレファイル作成→カラムすべて間違い→もう一度入れ直したい

1.マイグレファイルを作成する

$ rails g migration CreateHomes

2.作成したマイグレファイルをエディターで編集する

create_homes.rb
class CreateHomes < ActiveRecord::Migration[5.2]
  def change
    create_table :homes do |t|
      t.string :"物件名"
      t.string :"賃料"
      t.string :"住所"
      t.text :"備考"
    end
  end
end

3.マイグレーションをターミナル上で入力

$ rails db:migrate

4.マイグレファイルを削除するため、削除用のファイルを作る

rails g migration RemoveColumnsFromeHomes

5.作ったファイルを編集する

remove_columns_from_homes.rb
class RemoveColumnsFromHomes < ActiveRecord::Migration[5.2]
  def change
    remove_column :Homes, :"物件名", :string
    remove_column :Homes, :"賃料", :text
    remove_column :Homes, :"住所", :text
    remove_column :Homes, :"備考", :text
  end
end

6.マイグレーションをターミナル上で入力

$ rails db:migrate

※こうすることで、再び作り直すことができます。
rollbackでも削除は、可能ですが複数開発のときに何を編集したのか分からなくなるのでやめましょう。

7. カラムを追加するためのマイグレファイル作成

$ rails g migration AddColumnsToHomes

8.マイグレファイルを編集

add_columns_to_homes.rb
class AddColumnsToHomes < ActiveRecord::Migration[5.2]
  def change
    add_column :homes, :property, :string
    add_column :homes, :rent, :text
    add_column :homes, :address, :text
    add_column :homes, :age, :text
    add_column :homes, :remarks, :text

  end
end


何かを分からないことがありましたら、コメントを頂ければ1日以内に返信するように致します。

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

Railsをherokuでデプロイする方法

herokuでのデプロイは、AWSに比べてめちゃめちゃ簡単だったので、まとめました!

(githubへのpush、会員登録、クレジットカード登録など完了後)

herokuにログインする

デプロイしたいアプリに移動したら

$ heroku login

何かキーを押してくださいと言われるので、適当にエンターキーを押す。

herokuのページに推移する。

loginを押してターミナルに戻る

ログイン完了

作ったアプリをherokuとリンクさせる

デプロイしたいアプリに移動したら

$ heroku create アプリ名

アプリ名がURLに入るのでしっかり考えましょう
すでにそのアプリ名がherokuで使われている場合はできません

データベースを作成する

※データベースが必要な場合のみ

ターミナル

$ heroku addons:create cleardb:ignite

これでデータベース作成は完了しました。

mysql2の環境変数を指定

初期設定だとmysql2の設定ができていないのでそこを設定していきます。
現在のmysqlの設定を確認します。

$ heroku config

以下のような長い文字列が出てくるので、それに応じて環境変数を入力していきます。

CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true

heroku configで表示された文字列をもとに、以下のように入力していきます。

$ heroku config:add DB_USERNAME='<ユーザー名>'
$ heroku config:add DB_PASSWORD='<パスワード>'
$ heroku config:add DB_HOSTNAME='<ホスト名>'
$ heroku config:add DB_NAME='<データベース名>'
$ heroku config:add DB_PORT='3306'
$ heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'

エラー対策の微調整

config/environments/production.rb
 #初期はfalseなので変更
 config.assets.compile = true 

 #config.assets.js_compressor = :uglifierを変更
 config.assets.js_compressor = Uglifier.new(harmony: true)

デプロイ

$ git push heroku master

データベースのmigrate

$ heroku rake db:migrate

アプリの立ち上げ

$ heroku open

これでブラウザに、作ったアプリが表示されます!

補足

万が一、エラーが出てしまった場合は

$ heroku logs -t 

で確認できるので、修正し、ローカルからgithubへのプッシュ。
そして再度

$ git push heroku master

をすれば大丈夫です。

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

Ruby 2.7 rc1 の動作確認をdocker上で行う

以下のようにコマンドを実行する。

% docker run --name ruby_2_7_0_rc1 -it ruby:2.7.0-rc1 /bin/bash
% irb
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Minitest::Assersionsのassert系メソッドを全部試してみた

はじめに

この記事は Ruby Advent Calendar 2019 21日目の記事です。
今回はRubyのテストフレームワークであるMinitestのassert系メソッドを全部試してみました。
この記事がMinitestを使っている人に少しでも役立てばいいなと思います。
環境は以下のとおりです。

Ruby

ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux]

Minitest

Minitest::VERSION #=> "5.13.0"

それでは頑張っていきましょう。

assert(test, msg = nil)

testが真(nilfalse以外)の場合にアサーションが成功します。
msgを指定するとアサーション失敗時に指定したメッセージを表示します。
(これ以降紹介するメソッドでもmsgを引数に取れるメソッドがありますが、すべて同じ機能のため、ここでのみ紹介します。)

成功例

assert true
assert 0
assert :sym
assert "hello"

失敗例

assert false
Expected false to be truthy.

失敗例(メッセージあり)

assert nil, "nilだよ"
nilだよ

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert

assert_empty(obj, msg = nil)

obj.empty?が真の場合にアサーションが成功します。
empty?を実装していないオブジェクトを渡した場合はメッセージが表示され、アサーションに失敗します。

成功例

assert_empty({}) # {}がブロックと判定されてしまうため、()で括る
assert_empty []
assert_empty ""

失敗例

assert_empty({name: "TomoProg"})
assert_empty [1, 2, 3]
Expected {:name=>"TomoProg"} to be empty.
Expected [1, 2, 3] to be empty.

失敗例(empty?を実装していない)

assert_empty 0
Expected 0 (Integer) to respond to #empty?.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_empty

assert_equal(exp, act, msg = nil)

exp == actが真の場合にアサーションが成功します。
Minitestのバージョン6からはexpnilを指定するとアサーションが失敗するようになるようです。
そのため、expnilを指定した場合、assert_nilを使うように警告が出ます。

成功例

assert_equal 1, 1
assert_equal "sample", "sample"

失敗例

assert_equal 2, 3
Expected: 2
  Actual: 3

expにnilを指定した場合

assert_equal nil, nil
DEPRECATED: Use assert_nil if expecting nil from test/sample_test.rb:5.
This will fail in Minitest 6.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_equal

assert_in_delta(exp, act, delta = 0.001, msg = nil)

expactの差分((exp - act).abs)がdelta以下の場合にアサーションに成功します。
浮動小数点数の比較の際に誤差を許容する場合に使うようです。

成功例

assert_in_delta 0.001, 0.002
assert_in_delta 0.3, (0.1 + 0.2), 0.000001

失敗例

assert_in_delta 1.256, 1.255
Expected |1.256 - 1.255| (0.001000000000000112) to be <= 0.001.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_in_delta

assert_in_epsilon(exp, act, epsilon = 0.001, msg = nil)

誤差に関しての理解不足もあり、このメソッドに関してはよく分かりませんでした。
assert_in_deltaと同じように浮動小数点数の誤差を許容する場合に使うようですが、
assert_in_delta絶対誤差を許容するのに対し、こちらは相対誤差を許容するようです。

参考になりそうなリンクを貼っておきます。
Minitestで絶対誤差と相対誤差を利用したテストを実行する

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_in_epsilon

assert_includes(collection, obj, msg = nil)

collection.include?(obj)が真の場合にアサーションに成功します。
include?(obj)を実装していないオブジェクトを渡した場合はメッセージが表示され、アサーションに失敗します。

成功例

assert_includes [1, 2, 3], 2
assert_includes({name: "TomoProg"}, :name) # Hash#include?(key)はハッシュにkeyがあれば真となる

失敗例

assert_includes [1, 2, 3], 0
Expected [1, 2, 3] to include 0.

失敗例(include?(obj)を実装していない)

assert_includes 1, 1
Expected 1 (Integer) to respond to #include?.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_includes

assert_instance_of(cls, obj, msg = nil)

obj.instance_of?(cls)が真の場合にアサーションに成功します。

成功例

assert_instance_of Array, [1, 2, 3]
assert_instance_of Integer, 1

失敗例

assert_instance_of Integer, 1.2
Expected 1.2 to be an instance of Integer, not Float.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_instance_of

assert_kind_of(cls, obj, msg = nil)

obj.kind_of?(cls)が真の場合にアサーションに成功します。
assert_instance_ofに似ていますが、assert_kind_ofはサブクラスでもアサーションに成功します。

成功例

class Base; end
class SuperClass < Base; end

assert_kind_of Base, SuperClass.new
assert_kind_of SuperClass, SuperClass.new

失敗例

assert_kind_of Integer, 1.2
Expected 1.2 to be a kind of Integer, not Float.

assert_instance_ofとの違い

class Base; end
class SuperClass < Base; end

assert_instance_of Base, SuperClass.new # assert_instance_ofではサブクラスを指定すると失敗する
assert_kind_of     Base, SuperClass.new # assert_kind_ofではサブクラスでも成功する

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_kind_of

assert_match(matcher, obj, msg = nil)

matcher =~ objが真の場合にアサーションに成功します。

成功例

assert_match /rin/, "string"
assert_match /\d{3}-\d{4}-\d{3}/, "123-4567-890"

失敗例

assert_match /rig/, "string"
Expected /rig/ to match # encoding: UTF-8
#    valid: true
"string".

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_match

assert_mock(mock)

mock.verifyが真の場合にアサーションに成功します。
Minitestではメソッド呼び出しが正しく行われているかを確認するために、モックを作ることができます。
verifyはモックに定義したメソッドがすべて呼び出されていればtrueを返し、呼び出されていないメソッドがあればMockExpectationErrorを返します。

サンプルクラス

class Sample
  def initialize(obj)
    @obj = obj
  end

  def call
    @obj.method1
  end
end

成功例

# モックにメソッドを定義する
# (第1引数にメソッド名、第2引数に戻り値を指定する)
mock = Minitest::Mock.new
mock.expect "method1", true

# Sample#callを呼び出す
Sample.new(mock).call

# モックに定義したすべてのメソッドが呼ばれたため、このアサーションは成功する
assert_mock mock

失敗例

# モックにメソッドを定義する
# (第1引数にメソッド名、第2引数に戻り値を指定する)
mock = Minitest::Mock.new
mock.expect "method1", true
mock.expect "method2", true

# Sample#callを呼び出す
Sample.new(mock).call

# method2が呼ばれていないため、このアサーションは失敗する
assert_mock mock
MockExpectationError: expected method2() => true

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_mock

assert_nil(obj, msg = nil)

obj.nil?が真の場合にアサーションに成功します。

成功例

assert_nil nil

失敗例

assert_nil 1
Expected 1 to be nil.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_nil

assert_operator(o1, op, o2 = UNDEFINED, msg = nil)

o1.__send__(op, o2)が真の場合にアサーションに成功します。
2項演算子をテストするためのアサーションです。
o2UNDEFINEDの場合は後述するassert_predicateが呼び出されます。
UNDEFINED定数はMinitest::Assersionsで定義されており、Object.newが代入されていました。

成功例

assert_operator 5, :<=, 6

失敗例

assert_operator 5, :<=, 4
Expected 5 to be <= 4.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_operator

assert_output(stdout = nil, stderr = nil) { || ... }

標準出力がstdout、標準エラー出力がstderrの場合にアサーションに成功します。
ブロックに標準出力、標準エラー出力を扱う処理を記述して使います。
正規表現を渡すことも可能です。
また、nilを指定した場合はテストされません。

成功例

# 標準出力、標準エラー出力をテストする
assert_output("sample\n", "stderr_sample\n") do
  puts "sample"
  warn "stderr_sample" # Kernel.#warnは指定された文字列を標準エラー出力に出力する
end

# 正規表現も指定できる
assert_output(/str/) do
  puts "string"
end

# nilを指定した場合はテストされない
assert_output(nil, "stderr_sample\n") do
  warn "stderr_sample"
end

失敗例

assert_output("sample\n") do
  puts "saaaample"
end
In stdout.
--- expected
+++ actual
@@ -1,4 +1,2 @@
-# encoding: UTF-8
-#    valid: true
-"sample
+"saaaample
 "

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_output

assert_path_exists(path, msg = nil)

File.exist?(path)が真の場合にアサーションに成功します。

ディレクトリ構成

sample/
  test1.txt

成功例

assert_path_exists "sample/test1.txt"

失敗例

assert_path_exists "sample/test2.txt"
Expected path 'sample/test2.txt' to exist.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_path_exists

assert_predicate(o1, op, msg = nil)

o1.__send__(op)が真の場合にアサーションに成功します。
テストしたいopが複数ある場合は配列で持たせると綺麗に書けそうです。

成功例

ops = %i(all? any?)
ops.each { |op| assert_predicate [1, 2, 3], op }

失敗例

assert_predicate [1, 2, 3], :empty?
Expected [1, 2, 3] to be empty?.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_predicate

assert_raises(*exp) { || ... }

*expに指定した例外がどれか一つでもブロック内で発生した場合にアサーションに成功します。
最後に文字列を指定するとメッセージとして扱われ、アサーションの失敗時に出力されます。
また、アサーションに成功した場合は例外クラスを返します。

成功例

assert_raises(ZeroDivisionError) do
  1 / 0
end

# 例外が戻り値となる
exp = assert_raises(ZeroDivisionError, ArgumentError) do
  [1, 2, 3].include?
end
assert_kind_of ArgumentError, exp

# ブロック内で例外が複数起きる場合は先に起きた例外が戻り値となる
exp = assert_raises(ZeroDivisionError, ArgumentError) do
  1 / 0
  [1, 2, 3].include?
end
assert_kind_of ZeroDivisionError, exp

失敗例

assert_raises(ZeroDivisionError) do
  1 / 1
end
ZeroDivisionError expected but nothing was raised.

失敗例(メッセージ有り)

assert_raises(ZeroDivisionError, ArgumentError, "No Error") do
  [1, 2, 3].include?(1)
end
No Error.
[ZeroDivisionError, ArgumentError] expected but nothing was raised.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_raises

assert_respond_to(obj, meth, msg = nil)

obj.respond_to?(meth)が真の場合にアサーションに成功します。

成功例

assert_respond_to([1, 2, 3], :shift)
assert_respond_to({name: "TomoProg"}, :dig)

失敗例

assert_respond_to([1, 2, 3], :unknown_method)
Expected [1, 2, 3] (Array) to respond to #unknown_method.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_respond_to

assert_same(exp, act, msg = nil)

obj.equal?(act)が真の場合にアサーションに成功します。
equal?object_idで比較するため、assert_equalとは違う結果となる場合があります。

成功例

assert_same 1, 1
assert_same :sym, :sym
assert_same nil, nil

失敗例

assert_same [1, 2, 3], [1, 2, 3]
Expected [1, 2, 3] (oid=47357985755040) to be the same as [1, 2, 3] (oid=47357985755100).

assert_equalと違う結果になる例

# 中身が同じ配列を準備する
array1 = [1, 2, 3]
array2 = [1, 2, 3]

# assert_equalでは成功する
# ==で比較するため、中身が同じであれば真となる
assert_equal array1, array2

# assert_sameでは失敗する
# equal?で比較するため、object_idが異なると偽となる
assert_same  array1, array2

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_same

assert_send(send_ary, m = nil)

このメソッドは非推奨のようです。
理由を探したところ、下記のサイトに記述がありました。
日々雑記 — Minitest 5.10.0でdeprecateになった機能

assert_sendがdeprecateになった理由がちょっとわからなかったのでtwitterで聞いてみた所、単純に使っている所を見たことが無い為、との事でした。

send_aryに第一要素にレシーバとなる任意のオブジェクト、第二要素にメソッド名、 第三要素にパラメータをそれぞれ指定した配列を指定し、指定したオブジェクトのメソッドが真であればアサーションに成功します。

なぜかこのメソッドだけメッセージの引数名がmsgではなくmでした。

成功例

assert_send [[1, 2, 3], "include?", 1]
DEPRECATED: assert_send.

失敗例

assert_send [[1, 2, 3], :include?, 4]
DEPRECATED: assert_send.
Expected [1, 2, 3].include?(*[4]) to return true.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_send

assert_silent() { || ... }

標準出力、標準エラー出力に何も出力されない場合にアサーションに成功します。
assert_output("", "")と書くのと同じ意味です。

成功例

assert_silent { 1 + 1 }

失敗例(標準出力)

assert_silent { puts "hello" }
In stdout.
--- expected
+++ actual
@@ -1,3 +1,2 @@
-# encoding: UTF-8
-#    valid: true
-""
+"hello
+"

失敗例(標準エラー出力)

assert_silent { warn "warning" }
In stderr.
--- expected
+++ actual
@@ -1,3 +1,2 @@
-# encoding: UTF-8
-#    valid: true
-""
+"warning
+"

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_silent

assert_throws(sym, msg = nil) { || ... }

指定したブロック内でsymに対してthrowされるとアサーションに成功します。

成功例

assert_throws(:exit) do
  [1, 2, 3].each do |a|
    [4, 5, 6].each do |b|
      throw :exit
    end
  end
end

失敗例

assert_throws(:exit) do
  [1, 2, 3].each do |a|
    [4, 5, 6].each do |b|
      throw :unknown
    end
  end
end
Expected :exit to have been thrown, not :unknown.

ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_throws

まとめ

Minitest::Assersionsのassert系のメソッドをすべて試してみました。
Minitestを業務で使い始めてもうすぐ3年が経ちますが、こんなにあったとは知りませんでした。

様々なassertメソッドを試してみて一番感じたのは
適切に使い分けることで失敗の原因がより分かりやすくなる
という点です。

例えば、以下のアサーションは結果はどちらも失敗です。
しかし、後者のほうが何が原因で失敗したのかを分かりやすく伝えてくれます。

assert [1, 2, 3].include?(0) # Expected false to be truthy.
assert_includes [1, 2, 3], 0 # Expected [1, 2, 3] to include 0.

適切なメソッドを使い分け、分かりやすいテストコードを書いていきましょう。

それではまた。

TomoProg

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

Railsでerbをhamlに変換する方法

Hamlって便利ですよねー!
ただrailsの初期設定ではerbファイルとして、生成されてしまいます。

どうせならHamlに統一してコーディングしましょう。

gemのインストール

Gemfile

gem 'haml-rails'

を入れて

ターミナル

$ bundle install
$ rails haml:erb2haml

これで一括変換されます。

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

Railsでアプリを作る簡単な流れ

一からアプリを立ち上げるとなると、意外と、何からしていいか分からない人もいると思うので、まとめました。

(アプリのダウンロードなど、環境構築などは完了していること)

アプリを作成する

保存したい場所に移動して

私はデータベース名は、mysqlでやってます。
指定しなければ、SQLiteというものが入ります。

ターミナル

$ rails new アプリ名 -d mysql(データベース名)

gemのインストール

ターミナル

$ bundle install

コントローラーを作成する

投稿サイトを作る場合のコントローラーを作成します。
※コントローラー名は複数形にしましょう

ターミナル

$ rails g controller posts(コントローラー名)
posts.controller.rb
class PostsController < ApplicationController
  def index(アクション名)
    #空でいいので作成する
  end  
end

データベースを作成する

データベースが必要であれば作成します。
通常であれば2つのファイルが生成されます。

ターミナル

$ rails db:create

モデルを作成する

データベースが必要な場合はモデルを作成します。
※モデル名は単数形にしましょう

ターミナル

$ rails g model post(モデル名)

モデルを作成すると、マイグレーションファイルも同時生成されます。
必要なカラムがあれば追加します。

20190000_xx_xx.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      #t.(型名) :(カラム名), (オプション)
      t.text :content, null: false
      t.timestamps
    end
  end
end

カラムの追加が完了したら

ターミナル

$ rails db:migrate

これでデータベースの準備は完了です。

トップページのルーティングを作成する

routes.rb
 #root to: "コントロ-ラー名#アクション名"
 root to: "posts#index"

トップページのviewを作成する

app/views/posts(コントローラー名)の中に

index(アクション名).html.erbを作成
index.html.erb
 HELLO WORLD

railsを起動する

ターミナル

$ rails s

localhost:3000で起動します

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

railsでapplication.scssと同じディレクトリのscssファイルが読み込まれないときの対処法

背景と対象

usersコントローラを作成したときに生成された、app/assets/users.scssに記述したscssが反映されない。つまりはアセットパイプラインの仕組みが適用されていない状況になってしまっている状況。全部app/assets/application.scssに書けばcssの適用は出来ますがめっちゃ見ずらいのでやめた方がいいです。このような状況でつまっている方の役に立てればと思い書きました。ちなみにアセットパイプラインについては以下のページがおすすめです!
https://diveintocode.jp/blogs/Technology/AssetPipeline

解決方法

とっても簡単です!

app/assets/application.scssのコメントアウトの最下部に*= require_selfとrequire_tree を以下のように追加してください。それぞれのコードの意味はコード内に書いてあります。ちなみにこれらの順番はどっちでもよく、require_selfを先に書けばapplication.scssが先に読み込まれます。後に書けばapplication.scss以外のscssファイルが先に読み込まれます。

app/assets/application.scss
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_self まずはこのファイルを読み込むという記述 追加!
 *= require_tree  app/assets以下のcssファイルを読み込んでいる 追加!
 */
}

これでアセットパイプラインの仕組みを利用して分かりやすく、すっきりしたコードが書けます!

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

RubyVM::AbstractSyntaxTree を使って AASM の定義を抽出する

はじめに

本記事は STORES.jp Advent Calendar 2019 21日目の記事です。

Ruby2.6 から RubyVM::AbstractSyntaxTree という、 Ruby コードを抽象構文木に変換できる API を提供する module が使えるようになり、今年の RubyKaigi 2019 でも関連したセッションが行われていました。
これを使って何かできないものかと考えて思いついたのが、最近業務で利用している AASM という gem を使った、状態遷移の定義を取り出す...というものでした。

そこでこの記事では、 RubyVM::AbstractSyntaxTree を利用して AASM を使った状態遷移定義を取得するコードについて書きます。1

そもそも AASM とは?

AASM はオブジェクトの状態遷移を定義・管理する機能を提供する gem です。
class にステートの一覧や状態遷移のためのイベントを DSL によって記述することで、各ステートに対応した定数や状態遷移を行うためのメソッドを生成してくれます。また、定義と異なる遷移を行おうとすると状態遷移に失敗するので、意図しない状態遷移を防ぐことができます。
具体的な使い方を簡単に解説すると、 aasm メソッドに渡すブロック内で

  • state メソッドにステート名を渡すとその名前のステートを管理できるようになる
    • ステート名に対応した定数が生成される
    • initial_state: true オプションを付与することで、初期ステートが定義できる
  • event メソッドにイベント名を渡すことで、ステートを遷移させるメソッドが生成される
    • どのステートからどのステートへ遷移できる、というフローはブロック内の transitions によって定義する

複雑になりがちな状態管理を宣言的に書くことで見通しを良くしつつ、秩序を保ちやすく実装できるので便利です。

task.rb
class Task < ApplicationRecord
  include AASM

  aasm column: :status do
    state :waiting, initial: true
    state :in_progress, :pending, :finished

    event :start do
      transitions from: :waiting, to: :in_progress
      transitions from: :pending, to: :in_progress
    end

    event :stop do
      transitions from: :in_progress, to: :pending
    end

    event :finish do
      transitions from: :in_progress, to: :finished
    end
  end
end

task = Task.create
task.status
# => "waiting"
task.start!
task.status
# => "in_progress"
task.start!
# AASM::InvalidTransition: Event 'start' cannot transition from 'in_progress'.

状態遷移の定義を抽出する

本題です。
先ほどの例の Task クラスの状態遷移の定義を抽象構文木にしてごにょごにょし、初期ステート、ステートのリスト、イベントを取得できるようなオブジェクトを返すことができるようにしました。

GitHub - ta-chibana/aasm_parser: Parsing AASM definition with RubyVM::AbstractSyntaxTree

以下のように利用します。

aasm = AasmParser.parse_file('./task.rb')
aasm.initial_state
# => :waiting
aasm.state_names
# => [:waiting, :in_progress, :pending, :finished]
aasm.events.map(&:name)
# => [:start, :stop, :finish]
aasm.events.flat_map(&:transitions).map { |e| "#{e.from} => #{e.to}" }
# => ["waiting => in_progress", "pending => in_progress", "in_progress => pending", "in_progress => finished"]

内部では RubyVM::AbstractSyntaxTree を利用して task.rb を抽象構文木に変換し、目的の値を取得しています。
本記事では今回書いたコードのうち、初期ステートの取得を行う処理についてだけ解説します。

image.png

初期ステートの取得は以下の手順で段階的に行ってみました。

  1. task.rb を抽象構文木に変換
  2. aasm メソッドの呼び出し時に渡されている block を抽出
  3. block から state メソッドの呼び出しを抽出
  4. state メソッドの呼び出しから initial_state: true オプションが付いているものを抽出
  5. state メソッドの呼び出しの第一引数を取得(= 初期ステート)

手順1: task.rb を抽象構文木に変換

状態遷移の定義は aasm メソッドに渡された block 内にあり、これを RubyVM::AbstractSyntaxTree を使って取り出せる状態にするために task.rb を抽象構文木に変換します。変換は以下の1行で行うことができます。

node = RubyVM::AbstractSyntaxTree.parse_file('./task.rb')

結果として RubyVM::AbstractSyntaxTree::Node のインスタンスが返されます。 RubyVM::AbstractSyntaxTree::Node#type#children を持つので、それらを利用しつつ目的のノードを探していきます。

手順2: aasm メソッドの呼び出し時に渡されている block を抽出

RubyVM::AbstractSyntaxTree::Node インスタンスに対して pp node と実行すると以下のように出力され、ノードの type やノード間の親子関係を把握するのに便利です。開発時はこの構造を見ながら目的の値を抽出する道筋を立てました。
以下は手順1で変換した結果を pp に渡した結果です。

(SCOPE@1:0-21:3
 tbl: []
 args: nil
 body:
   (CLASS@1:0-21:3 (COLON2@1:6-1:10 nil :Task)
      (CONST@1:13-1:30 :ApplicationRecord)
      (SCOPE@1:0-21:3
       tbl: []
       args: nil
       body:
         (BLOCK@2:2-20:5
            (FCALL@2:2-2:14 :include
               (ARRAY@2:10-2:14 (CONST@2:10-2:14 :AASM) nil))
            (ITER@4:2-20:5
               (FCALL@4:2-4:22 :aasm
                  (ARRAY@4:7-4:22
                     (HASH@4:7-4:22
                        (ARRAY@4:7-4:22 (LIT@4:7-4:14 :column)
                           (LIT@4:15-4:22 :status) nil)) nil))
               (SCOPE@4:23-20:5
                tbl: []
                args: nil
                body:
                  (BLOCK@5:4-19:7
                     (FCALL@5:4-5:33 :state
                        (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting)
                           (HASH@5:20-5:33
                              (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial)
                                 (TRUE@5:29-5:33) nil)) nil))
                     (FCALL@6:4-6:43 :state
                        (ARRAY@6:10-6:43 (LIT@6:10-6:22 :in_progress)
                           (LIT@6:24-6:32 :pending) (LIT@6:34-6:43 :finished)
                           nil))
                     (ITER@8:4-11:7
                        (FCALL@8:4-8:16 :event
                           (ARRAY@8:10-8:16 (LIT@8:10-8:16 :start) nil))
                        (SCOPE@8:17-11:7
                         tbl: []
                         args: nil
                         body:
                           (BLOCK@9:6-10:50
                              (FCALL@9:6-9:50 :transitions
                                 (ARRAY@9:18-9:50
                                    (HASH@9:18-9:50
                                       (ARRAY@9:18-9:50 (LIT@9:18-9:23 :from)
                                          (LIT@9:24-9:32 :waiting)
                                          (LIT@9:34-9:37 :to)
                                          (LIT@9:38-9:50 :in_progress) nil))
                                    nil))
                              (FCALL@10:6-10:50 :transitions
                                 (ARRAY@10:18-10:50
                                    (HASH@10:18-10:50
                                       (ARRAY@10:18-10:50
                                          (LIT@10:18-10:23 :from)
                                          (LIT@10:24-10:32 :pending)
                                          (LIT@10:34-10:37 :to)
                                          (LIT@10:38-10:50 :in_progress) nil))
                                    nil)))))
                     (ITER@13:4-15:7
                        (FCALL@13:4-13:15 :event
                           (ARRAY@13:10-13:15 (LIT@13:10-13:15 :stop) nil))
                        (SCOPE@13:16-15:7
                         tbl: []
                         args: nil
                         body:
                           (FCALL@14:6-14:50 :transitions
                              (ARRAY@14:18-14:50
                                 (HASH@14:18-14:50
                                    (ARRAY@14:18-14:50 (LIT@14:18-14:23 :from)
                                       (LIT@14:24-14:36 :in_progress)
                                       (LIT@14:38-14:41 :to)
                                       (LIT@14:42-14:50 :pending) nil)) nil))))
                     (ITER@17:4-19:7
                        (FCALL@17:4-17:17 :event
                           (ARRAY@17:10-17:17 (LIT@17:10-17:17 :finish) nil))
                        (SCOPE@17:18-19:7
                         tbl: []
                         args: nil
                         body:
                           (FCALL@18:6-18:51 :transitions
                              (ARRAY@18:18-18:51
                                 (HASH@18:18-18:51
                                    (ARRAY@18:18-18:51 (LIT@18:18-18:23 :from)
                                       (LIT@18:24-18:36 :in_progress)
                                       (LIT@18:38-18:41 :to)
                                       (LIT@18:42-18:51 :finished) nil))
                                 nil)))))))))))

雑な見方としては、以下の部分を例にすると、1行目のノードは type が FCALL のノードで、後ろに続く :include (ARRAY@2:10-2:14 (CONST@2:10-2:14 :AASM) nil) が子のノードとなります。それぞれ RubyVM::AbstractSyntaxTree::Node#type, RubyVM::AbstractSyntaxTree::Node#children の戻り値に対応しています。

            (FCALL@2:2-2:14 :include
               (ARRAY@2:10-2:14 (CONST@2:10-2:14 :AASM) nil))

これを図にすると以下のようになり、 include AASM の行を表すノードであることがわかります。
(開発当初は、 type の意味については解説されたドキュメントが見つからなかったのであくまで推測だったのですが、社内で共有したところ --dump=parsetree_with_comment オプション付きで実行すれば各ノードの説明が表示されるとの情報をいただけました。圧倒的感謝です :bow:

image.png

--dump=parsetree_with_commentで実行
$ ruby --dump=parsetree_with_comment -e 'include AASM'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 1, location: (1,0)-(1,12))
# | # new scope
# | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
# +- nd_tbl (local table): (empty)
# +- nd_args (arguments):
# |   (null node)
# +- nd_body (body):
#     @ NODE_FCALL (line: 1, location: (1,0)-(1,12))*
#     | # function call
#     | # format: [nd_mid]([nd_args])
#     | # example: foo(1)
#     +- nd_mid (method id): :include
#     +- nd_args (arguments):
#         @ NODE_ARRAY (line: 1, location: (1,8)-(1,12))
#         | # array constructor
#         | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#         | # example: [1, 2, 3]
#         +- nd_alen (length): 1
#         +- nd_head (element):
#         |   @ NODE_CONST (line: 1, location: (1,8)-(1,12))
#         |   | # constant reference
#         |   | # format: [nd_vid](constant)
#         |   | # example: X
#         |   +- nd_vid (constant): :AASM
#         +- nd_next (next element):
#             (null node)

これを踏まえると、 aasm メソッドのブロックに対応している部分は以下の部分が怪しそうな気配が感じられます。

            (ITER@4:2-20:5
               (FCALL@4:2-4:22 :aasm
                  (ARRAY@4:7-4:22
                     (HASH@4:7-4:22
                        (ARRAY@4:7-4:22 (LIT@4:7-4:14 :column)
                           (LIT@4:15-4:22 :status) nil)) nil))
               (SCOPE@4:23-20:5
                tbl: []
                args: nil
                body:
                  (BLOCK@5:4-19:7
                     (FCALL@5:4-5:33 :state
                        (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting)
                           (HASH@5:20-5:33
                              (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial)
                                 (TRUE@5:29-5:33) nil)) nil))

このノードを抜き出すために、まず以下の条件でノードを抽出し、その後 BLOCK 要素を取得する流れにしました。

  • typeITER (ブロック付きメソッド呼び出し?)
  • 子要素に typeFCALL(関数呼び出しを表す?)の要素を持つ
  • ↑の FCALL 要素の最初の子要素が :aasm

そのために書いたコードが以下で、 find_from_node でノードを掘り下げつつ aasm_node?true を返すノードが見つかる、もしくはすべてのノードを走査し終えるまで処理を行います。

lib/aasm_parser/aasm_node_finder.rb
def find_from_node(node)
  return nil unless node.respond_to?(:type)
  return node if aasm_node?(node)

  find_from_children(node.children)
end

def find_from_children(children)
  return nil if children.blank?

  head, *tail = children
  result = find_from_node(head)
  return result if result

  find_from_children(tail)
end

def aasm_node?(node)
  return false unless node&.type == :ITER

  first_child = node.children.first
  return false unless first_child.type == :FCALL

  first_child.children.first == :aasm
end

条件に該当するノードが見つかれば、取得できた ITER ノードの2つ目の子要素である SCOPE ノードから BLOCKノードを取得すればOKです。

手順3: aasm の block から state メソッドの呼び出しを抽出

手順2で取得した BLOCK ノードの構造を見てみます。

                  (BLOCK@5:4-19:7
                     (FCALL@5:4-5:33 :state
                        (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting)
                           (HASH@5:20-5:33
                              (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial)
                                 (TRUE@5:29-5:33) nil)) nil))
                     (FCALL@6:4-6:43 :state
                        (ARRAY@6:10-6:43 (LIT@6:10-6:22 :in_progress)
                           (LIT@6:24-6:32 :pending) (LIT@6:34-6:43 :finished)
                           nil))

BLOCK ノード直下に state メソッドの呼び出しを表していそうなノードが見えるので、 BLOCK ノードの子要素から以下の条件で抽出すれば良さそうです。

  • typeFCALL
  • 最初の子要素が :state

そのための実装が以下です(↑の条件で select しているだけ)。

lib/aasm_parser/aasm/block.rb
def state_nodes
  ast_block.children.select do |node|
    node.type == :FCALL && node.children.first == :state
  end
end

手順4: state メソッドの呼び出しのリストから initial_state: true オプションが付いているものを抽出

手順3で取得できた state メソッドの呼び出しは、元のコードの task.rb でいうと以下の部分です。

    state :waiting, initial: true
    state :in_progress, :pending, :finished

また、取得できた state メソッド呼び出しのノードは以下ような構造になっています。

                     (FCALL@5:4-5:33 :state
                        (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting)
                           (HASH@5:20-5:33
                              (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial)
                                 (TRUE@5:29-5:33) nil)) nil))
                     (FCALL@6:4-6:43 :state
                        (ARRAY@6:10-6:43 (LIT@6:10-6:22 :in_progress)
                           (LIT@6:24-6:32 :pending) (LIT@6:34-6:43 :finished)
                           nil))

どちらの FCALL ノードも子要素として ARRAY ノードを持っており、その中には :waiting:in_progress などの Symbol が含まれていることから、引数を表していそうです。
また、1つ目の state メソッドの呼び出しではオプション引数として initial: true を渡していますが、ノードの構造では (LIT@5:10-5:18 :waiting) の他に HASH ノードが含まれていたり、 HASH ノードの孫要素に (LIT@5:20-5:28 :initial)(TRUE@5:29-5:33) があり、これらが含まれていることを条件に initial_state: true であるかどうかが判断できそうです。

以上をまとめると initial_state オプションが付いている要素かどうかを調べるためには、

  • FCALL ノードの子要素である ARRAY ノードの子要素に HASH ノードが存在する(= オプション引数が存在する)
  • ↑の HASH ノードの子要素である ARRAY 要素に、子要素として :initial を持つ LIT ノードが存在する
  • ↑の LIT ノードの次の要素が TRUE ノードである

という条件で判断します。これらの条件を確認するコードが以下の initial_state? です。

lib/aasm_parser/aasm/state.rb
def initial_state?
  return false if options.blank?

  lit_node_index = options.find_index do |e|
    e.type == :LIT && e.children[0] == :initial
  end

  return false if lit_node_index.nil?

  options[lit_node_index + 1].type == :TRUE
end

# state メソッドに渡された引数のノードを抽出する
def state_arguments
  # @state_node に、 FCALL ノードの
  # RubyVM::AbstractSyntaxTree::Node インスタンスが格納されている
  @state_node
    .children
    .last # state メソッドに渡された引数を表す ARRAY ノード
    .children
    .compact
end

def options
  @options ||= begin
    hash_node = state_arguments.find { |e| e.type == :HASH }
    return [] if hash_node.nil?

    hash_node
      .children
      .first # オプション引数の内容を表す ARRAY ノード
      .children
  end
end

# state の名前を取得する
# 手順5で名前を取得するときに利用します
def names
  state_arguments
    .select { |e| e.type == :LIT }
    .flat_map(&:children)
end

#names を呼び出すと state の名前を抽出することができるようにしました(手順5で使います)。

手順5: initial_state: true オプション付きの state メソッド呼び出しの第一引数を取得

手順3で抽出した state メソッドの呼び出しを表すノードの中から、 initial_state: true オプションが付いているかを判定するには手順4の initial_state? を実行すれば良いです。
また、その名前を得るためには手順4のコードにチラッと出てきた names を利用します。 state メソッドには複数のステート名が渡せるので、その都合上複数の名前が返るようになっていますが、 initial_state オプションが付いている場合はステートは一つしか渡されていないはず?なので最初の要素を取得すれば初期ステート名を得ることができます。

lib/aasm_parser/aasm/block.rb
def initial_state
  state = states.find(&:initial_state?)
  return nil if state.nil?

  state.names.first
end

def states
  state_nodes.map { |node| State.new(node) }
end

おわりに

無事に目的の値を取得することができた一方、以下のようなあらゆる問題が存在します。

  • AASM のあらゆるオプションに対応できていない(:if, :unlessやコールバックが設定できます)
  • AASM に破壊的な変更があるとほぼ作り直しのような状態になる
  • どうしても複雑な処理になってしまうので修正が容易でない
  • AST生成時に対象のコードは実行されないので、実行しないと目的の値が取れない(ステートが Symbol でなく定数・変数で記述されているなど)ケースだと詰む
  • などなど...

これらの問題を解決するにはとても良い方法があって、 RubyVM::AbstractSyntaxTree を使わずに AASM を使う という方法が有効です。つまりこうです。

Task.aasm.states.map(&:name)
# => [:waiting, :in_progress, :pending, :finished]
Task.aasm.events.map(&:name)
# => [:start, :stop, :finish]
Task.aasm.initial_state
# => :waiting
Task.aasm.events.flat_map(&:transitions).map { |e| "#{e.from} => #{e.to}" }
# => ["waiting => in_progress", "pending => in_progress", "in_progress => pending", "in_progress => finished"]

これですべての問題が解決しそうです。
結果的に作らなくても良いものを生んでしまったわけですが、 RubyVM::AbstractSyntaxTree を使ってみるという目的は達せられたのでヨシとします。

感想としては、取りたいノードの構造がわかっているなら段階的に抽出するのではなく一発で抽出するようにできたのではないかなあとか、この知識を使える現実的な場面(なさそう)に遭遇したらまた挑戦してみたいなあと思うなどしました。ただ、普段書かないようなコードなので楽しく取り組めました。

以上、 RubyVM::AbstractSyntaxTree を使ってみたくてコードを書いた記録でした。

参考


  1. おや?と思った方、お察しください。釘に見えてしまったのです。 

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

文言最初の一歩

0.はじめに

初投稿となります

まずは「漢文風プログラミング言語」
文言 を紹介させていただきます

漢文風プログラミング言語「文言」の基本文法を読み解いてみた - @GandT

導入

wenyan

GitHub よりソース取得

npm install ではないらしい

実行

npm startではないらしい

当方windowsユーザーです

実行方法わからないので

‘’’
cd site
ruby -run -e httpd . -p 3000
‘’’

ブラウザより

http://localhost:3000/ide.html
でエディター起動

サンプルアプリ実行

おわりに

わからなすぎで、実行だけならオンラインからも出来たのですが、縦書きレンダリングとruby出力を夢見て頑張ってみます

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

#Stripe API + #Ruby gem で定期支払いの請求を作成する例 ( 顧客作成・デフォルトの支払い方法を登録・プランの作成・税率情報の登録など含む )

Code

No need to payment intent, payment source attache.

require 'stripe'

Stripe.api_key = ENV['STRIPE_SECRET_KEY']

product = Stripe::Product.create(name: 'Gold plan')
plan = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 1000, product: product.id)
tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false)
customer = Stripe::Customer.create
payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01})
customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id)
subscription = Stripe::Subscription::create(
                customer: customer.id,
                default_payment_method: customer_payment_method.id,
                items: [{ plan: plan.id }],
                default_tax_rates: [tax_rate]
              )

customer = Stripe::Customer.retrieve(customer.id)
subscription_invoice = Stripe::Invoice.retrieve(customer.subscriptions.data[0].latest_invoice)

p subscription_invoice

Result

$ STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxx pry ~/tmp/stripe.rb
#<Stripe::Invoice:0x3fc7e35d01ac id=in_1FrZnyCmti5jpytUqUqqcjVX> JSON: {
  "id": "in_1FrZnyCmti5jpytUqUqqcjVX",
  "object": "invoice",
  "account_country": "JP",
  "account_name": "yumainaura",
  "amount_due": 1100,
  "amount_paid": 1100,
  "amount_remaining": 0,
  "application_fee_amount": null,
  "attempt_count": 1,
  "attempted": true,
  "auto_advance": false,
  "billing_reason": "subscription_create",
  "charge": "ch_1FrZnyCmti5jpytUQ9PMH3z1",
  "collection_method": "charge_automatically",
  "created": 1576803442,
  "currency": "jpy",
  "custom_fields": null,
  "customer": "cus_GOMXfIqzJKSF12",
  "customer_address": null,
  "customer_email": null,
  "customer_name": null,
  "customer_phone": null,
  "customer_shipping": null,
  "customer_tax_exempt": "none",
  "customer_tax_ids": [

  ],
  "default_payment_method": null,
  "default_source": null,
  "default_tax_rates": [
    {"id":"txr_1FrZnwCmti5jpytUlv7jF2HT","object":"tax_rate","active":true,"created":1576803440,"description":null,"display_name":"Tax Rate","inclusive":false,"jurisdiction":null,"livemode":false,"metadata":{},"percentage":10.0}
  ],
  "description": null,
  "discount": null,
  "due_date": null,
  "ending_balance": 0,
  "footer": null,
  "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_uCN38P2xjLu5iSsXHOU6LCozKT",
  "invoice_pdf": "https://pay.stripe.com/invoice/invst_uCN38P2xjLu5iSsXHOU6LCozKT/pdf",
  "lines": {"object":"list","data":[{"id":"sli_fe1f1f1bb0aa55","object":"line_item","amount":1000,"currency":"jpy","description":"1 × Gold plan (at ¥1,000 / month)","discountable":true,"livemode":false,"metadata":{},"period":{"end":1579481842,"start":1576803442},"plan":{"id":"plan_GOMXJzgxs31NbS","object":"plan","active":true,"aggregate_usage":null,"amount":1000,"amount_decimal":"1000","billing_scheme":"per_unit","created":1576803440,"currency":"jpy","interval":"month","interval_count":1,"livemode":false,"metadata":{},"nickname":null,"product":"prod_GOMWB1WfirJ9E5","tiers":null,"tiers_mode":null,"transform_usage":null,"trial_period_days":null,"usage_type":"licensed"},"proration":false,"quantity":1,"subscription":"sub_GOMXwLV5Qyelwu","subscription_item":"si_GOMXiUNouc5U5J","tax_amounts":[{"amount":100,"inclusive":false,"tax_rate":"txr_1FrZnwCmti5jpytUlv7jF2HT"}],"tax_rates":[],"type":"subscription","unique_id":"il_1FrZnyCmti5jpytUt0gmfTrU"}],"has_more":false,"total_count":1,"url":"/v1/invoices/in_1FrZnyCmti5jpytUqUqqcjVX/lines"},
  "livemode": false,
  "metadata": {},
  "next_payment_attempt": null,
  "number": "4506ADA5-0001",
  "paid": true,
  "payment_intent": "pi_1FrZnyCmti5jpytUPbn6rlhH",
  "period_end": 1576803442,
  "period_start": 1576803442,
  "post_payment_credit_notes_amount": 0,
  "pre_payment_credit_notes_amount": 0,
  "receipt_number": null,
  "starting_balance": 0,
  "statement_descriptor": null,
  "status": "paid",
  "status_transitions": {"finalized_at":1576803442,"marked_uncollectible_at":null,"paid_at":1576803443,"voided_at":null},
  "subscription": "sub_GOMXwLV5Qyelwu",
  "subtotal": 1000,
  "tax": 100,
  "tax_percent": 10.0,
  "total": 1100,
  "total_tax_amounts": [
    {"amount":100,"inclusive":false,"tax_rate":"txr_1FrZnwCmti5jpytUlv7jF2HT"}
  ],
  "webhooks_delivered_at": null
}

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2856

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

ずら〜っと、Railsでデータベースを確認する方法一覧

データベースクライアントの起動
$ rails dbconsole

こうなる
sqlite>

テーブル一覧の見方
sqlite> .tables

指定したテーブルのカラムの確認方法(スキーマの確認)
.schema テーブル名

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

WindowsにRails環境を導入してみる

WindowsにRailsを導入しようとしたらめちゃくちゃ手こずったので、記録しておこうと思って書きました

初めに

ここからRubyをダウンロードする、Devkitがついてるやつがいいらしい
インストールが完了したらコマンドプロンプトを開いてgem install railsを実行
問題なくインストールできたようなので次のステップへ

rails new hogeを実行するとガーっとファイルが作られていって安心していたら変なのが出る

Could not find gem 'sqlite3 (~> 1.4) x64-mingw32' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems.

意訳:ふえぇ、SQLite3 v1.4が見つからないよぉ
Railsを始めてsqlite3まわりのエラーで躓いている人たちへを見るとSQLite3でエラーが頻発して阿鼻叫喚としてるとか書いてある、怖い

Gemfileを編集する

# Use sqlite3 as the database for Active Record
 gem 'sqlite3', '~> 1.4'

2行目のSQLite3のバージョンを下げてみる

# Use sqlite3 as the database for Active Record
 gem 'sqlite3', '~> 1.3.6'

ディレクトリ移動してbundle install

hoge>bundle install

Bundle complete! 14 Gemfile dependencies, 70 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

勝ったなガハハ、サーバ立ててlocalhost:3000にアクセスしてやろう。

hoge>rails server

C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2.1/lib/active_support/dependencies.rb:325:in `require': cannot load such file -- sqlite3/sqlite3_native (LoadError)

勝ってなかった。
SQLite3くんが悪さをしているように見えるので消してやりましょう

hoge>gem uninstall sqlite3
Successfully uninstalled sqlite3-1.3.13-x64-mingw32

gem install sqlite3 --platform rubyでSQLite3を再インストール

hoge>gem install sqlite3 --platform ruby
Temporarily enhancing PATH for MSYS/MINGW...
Installing required msys2 packages: mingw-w64-x86_64-sqlite3
Building native extensions. This could take a while...
Successfully installed sqlite3-1.4.2
Parsing documentation for sqlite3-1.4.2
Installing ri documentation for sqlite3-1.4.2
Done installing documentation for sqlite3 after 1 seconds
1 gem installed

通った。
なおサーバは立たない、なんでや。

Railsのバージョンを下げる

rails 環境構築~ヘルスケアwebサービスを自分で作る医者の日記~【Windows】RubyとRuby on Raisの環境構築でSQLite3まわりで苦労したのコメント欄にRailsのバージョンを下げたらいけたと書いてあったので早速gem i -v 5.2.3 railsを実行
終わったぽいのでrails new hogeでプロジェクトを作るとすんなり通った。
これはいけたのでは?
恐る恐るrails server実行

キャプチャ.PNG

対戦ありがとうございました。

原因

詳しいことは不明ですが、Rails6.0.2.1とSQLite3 v1.4の相性がよくなかったのかなと思います

最終的な環境

Ruby 2.6.5p114
Gem 3.0.3
Rails 5.2.3
SQLite3 1.4.2

最後に

Qiitaで記事を書くのは初めてなので、変なところがありましたらご指摘いただけると幸いです。

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

Rubyを使って予約システムを使いやすくハックしよう

はじめに

こんにちは。GMOアドマーケティングのyoshishinです。
皆さんは普段、よく使うWEBサービスなどありますか?
自分は最近、予約システムをよく使うようになったのですが、毎回同じ操作をしており、効率的ではないと感じています。
多くの人が使いやすいように作られているとは思うのですが、人によって感じ方は様々で、手順が決まっているのであればいつもの設定をボタン一つでできたら便利ですし、中には自分専用の画面がほしかったりするかもしれません。
今回はこのような予約システムを使いやすくするためのハックを考えていきたいと思います。

目標

予約システムから必要な情報を取り出し、使いやすいJSON形式に整形して任意のレスポンスを返すことを目標とします。
JSON形式であれば、データを元に画面を作ったり、思い通りに操作することが容易となります。

開発環境

まずは開発環境を整えます。
今回メインで使うライブラリはnokogiriです。
HTMLという素材からノコギリで必要なものを切り出していくイメージですね。
Rubyが使える状態であれば、以下のコマンドでインストールできます。

gem install nokogiri

Railsを使う場合はこのgemが含まれているため、bundle installすることで追加されます。

公式サイト
http://www.nokogiri.org/

以下、公式サイトのサンプルです。

require 'nokogiri'
require 'open-uri'
# HTMLドキュメントを取得し、解析
doc = Nokogiri::HTML(open('https://nokogiri.org/tutorials/installing_nokogiri.html'))

puts "### Search for nodes by css"
doc.css('nav ul.menu li a', 'article h2').each do |link|
  puts link.content
end
puts "### Search for nodes by xpath"
doc.xpath('//nav//ul//li/a', '//article//h2').each do |link|
  puts link.content
end

puts "### Or mix and match."
doc.search('nav ul.menu li a', '//article//h2').each do |link|
  puts link.content
end

xpass,cssを使い、レスポンスを確認しながら目的のデータを取得します

解析

次に解析です。
Chromeであれば、画面を右クリックして、検証を選択することで、HTML構造を確認できます。
HTMLの構造から特徴をまとめていきます。

スクリーンショット 2019-12-18 22.59.35.png

普段使っているWEBサイトであれば、どういう構造で、どんなデータが欲しいのかはすぐに想像できると思いますし、何らかの不満を感じているのであれば、こうすれば使いやすくなるという改善案もあると思います。

対象システムでは日にちごとにいくつかの時間があり、時間毎に予約枠を持っているという特徴がありました。

設計

専用画面を作り、登録した設定情報を元に予約できるようになれば便利になると思うので、JSON形式のレスポンスを返し、そこから画面を表示できるようにしていきます。
解析時の特徴から、以下のようなJSON形式にすると使いやすそうなので、データを整形していきます。

[{"day"=>"12/23", "times"=>[
  {"time"=>"12:00", "stock"=>0},
  {"time"=>"12:15", "stock"=>0}, {"time"=>"12:30", "stock"=>0}, {"time"=>"12:45", "stock"=>0},...
]

データ取得

理想の形が定まっているのであれば、あとはnokogiriでひたすらデータを切り出し、設計したJSON形式にデータを格納していきます。
作ったデータを呼び出すメソッドを用意すればJSON API化は完了です。

対象サイトをスクレイピングして整形したJSONを返し、それをRailsの画面で表示してみました。

スクリーンショット 2019-12-21 6.32.45.png

まとめ

以上の手順でJSON API化することができました。
ここまでできれば後は使いやすい画面を作るなり、設定に従った自動予約、チャットシステムとの連携など、思い通りに操作することができると思います。
予約システムだけでなく、他の仕組みにも応用できると思うので、毎日多くの時間を費やしているシステムがあれば、使いやすくハックすることも検討してみてはいかがでしょうか?

注意点として、実際に使うタイミングで自動予約するのであれば手動でアクセスするのと比べて負荷は変わりませんが、バッチを使った予約の空き枠通知など、定期的にアクセスする実装はサーバーの負荷になってしまうため、控えた方がよいと思います。

以上です。

明日は、M.Nさんによる「TAXELの回遊率を少しだけ最適化した話」です。
引き続き、GMOアドマーケティング Advent Calendar 2019 をお楽しみください!

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

railsアセットプリコンパイルまとめ

  • javascript
  • scss

javascript

プロジェクトフォルダのapp/assets/javascript以下のファイルをすべて読み込む。
railsの初期ファイルのapplication.jsは何かというと、assets/javascript以下のファイルをすべて読み込みまとめたものにあたる。勝手にファイルをまとめてくれるので、application.jsに追加でrequireしなくてもよいのである。
しかしgemなどで追加した機能を読み込みたい場合は、application.jsにrequireする必要がある。

これはどこで使用するのかというともちろんviewである。だいたいは、layouts/application.html.erbに書かれている。<%= javascript_include_tag 'application', オプション %> のように記述すればよい。'application'の部分は、他のファイルでもよいがPATHを通す必要がある。

scss

railsでbootstrapをgemで追加する場合はcssではなく拡張子がscssのファイルになる。ここでのcssとscssの違いは記述の仕方である。cssは、javascriptと同じように//=requireのよういに記述する必要がある。また//=require_treeがあるのだがこれは、javascriptと同じですべてのファイルをまとめて読み込むというのを意味している。cssにもapplication.cssというファイルがあり、javascriptと使い方は同じである。

それに比べてscssは、@importのようにして機能をインポートする必要がある。そしてcssと違ってファイルを自動的に読み込んでくれないため、一つ一つインポートする必要がある。
例えば、A.scssというファイルとB.scssというファイルがあるとする。そしてjavascriptと同じで、application.scssというファイルがある。scssではA.scssとB.scssの両方を適用したいのならapplication.scssに、@import 'A.scss'; @import 'B.scss';と記述する必要がある。

bootstarpのテンプレートをrailsに適用するときの注意

以上の知識より、ダウンロードしたテンプレートファイルのcss部分とjs部分をそれぞれ、assets配下に配置する。
テンプレートのファイルには、bootstrap.min.cssなどのファイルが含まれているが、これはbootstrapの機能を使えるようにしますよというファイルである。なのでrails側ですでにgemでbootstrapを追加している場合は、assets配下に配置してはならない。
jquery.min.jsなどのファイルもあるがこれも同様である。

ダウンロードしたテンプレートのファイルが機能を提供するものなのか、それはrails側ですでに追加されているのかを判断してからssets配下に置くとよい

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

Rails6 マイグレーションファイルの記載ミスでマイグレートしてしまったらrollbackを使おう

目的

  • マイグレーションファイルの記載を間違え、マイグレートしてしまった時の対処法をまとめる

筆者がやらかしたこと

  1. マイグレーションファイルの記載をミスる。
  2. その状態でマイグレートしてしまう。
  3. データベースを確認したところ期待した方になっていない。
  4. 現在のマイグレーションファイルを削除する。
  5. 再度、マイグレーションファイルを作成し正式な記載にをした。
  6. マイグレートを実行した。
  7. データベースが期待したものになっていた。

間違えてマイグレートをしてしまった時はrollebaskをしよう

  • マイグレート後にDBに異変を感じたらすぐに下記のコマンドを実行してマイグレート前の状態に戻す。

     $ rails db:rolleback
    
  • そのあとでマイグレーションファイルの記載を確認、修正を行い再度、マイグレートを行う。

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

RubyをFlightRecoderを使ってブラックボックス分析する

はじめに

性能分析や障害分析で詳細なログ情報が取得たいときは良くあります。

私は普段はRuby製のバッチやWebアプリを本番で動かさないのであまり気にした事が無かったのですが、パッと思い付かなかったのでJFR(JDK Flight Recorder)をRubyに適用出来るかを試してみました。

JFRというブラックボックス分析向けのJVMの仕組みで低遅延のプロファイラを常時本番環境のアプリに適用してCPUやメモリ、GCログはもちろんスレッドダンプやI/O、カスタムイベントを1ファイルに集約保存する仕組みです。
これによって、障害時の原因の特定をスピーディーに実施できます。

今回はTruffleRubyを使う事でJFRを有効にしてみました。

TruffleRubyのインストール

まずはインストールです。rbenvとかで入れたくなるのですが、その場合JVM版が入らないのでGraalVMからインストールを行います。

こちらからCommunity版をインストールするか、SDKMANを使ってインストールします。下記はSDKMANのケースです。

# GraalVMのインストール
$ sdk install java 19.3.0.r11-grl  
$ java -version 
openjdk version "13.0.1-BellSoft" 2019-10-15
OpenJDK Runtime Environment (build 13.0.1-BellSoft+9)
OpenJDK 64-Bit Server VM (build 13.0.1-BellSoft+9, mixed mode, sharing)

続いてTruffle Rubyのインストールです。ここからの手順はGraalVMのインストール方法に依存せず共通です。

$ gu install ruby  
$ ruby --jvm --version
truffleruby 19.3.0, like ruby 2.6.2, GraalVM CE JVM [x86_64-darwin]

インストールはこれで完了です。

検証アプリを作成

動作テスト用のアプリをSinatraで作成します。

$ bundler init
$ echo 'gem "sinatra"' >> Gemfile 
$ bundler install 

下記のようにapp.rbを書きます。

require 'sinatra'

get '/' do
  'Hello world!'
end

JFRの取得

アプリを起動させてJFRを取得します。以下のようなJVMオプションをRubyに渡します。--vm.XX:...のようにする事でJVMオプションを指定できます。

$ ruby --jvm --vm.XX:StartFlightRecording=settings=profile,filename=myapp.jfr app.rb

下記のメッセージが出ればJFRが有効になっています。

Started recording 1. No limit specified, using maxsize=250MB as default.
Use jcmd 53009 JFR.dump name=1 to copy recording data to file.

プロセス終了時にmyapp.jfrという名前で作成されます。

VisualVMで見てみる

JFRはバイナリなのでJDK Mission Controlをインストールして解析したり、jfrコマンドでJSONに変換したりして分析します。
GraalVMにはjvisualvmが同梱されているのでこちらを利用することも可能です。VisualVMでJFRを開くと以下のような情報が取得できます。

image.png
image.png
image.png

他にもGCログとかヒストグラムとかスレッドロックとか色々見れますが割愛。VisualVMのJFRサポートはまだ絶賛開発中なので、本格的に見るならJMCの方がUIもこなれていてオススメです。
image.png

カスタムイベントの追加

Flight Recorderにはカスタムタグと言って任意のイベントを追加する仕組みがあります。これを使うことでログを吐くのと同じ感覚でJFRファイルの中に情報を組み込めます。

以下のようにjdk.jfr.Eventクラスを継承したコードをJavaで書きます。

java/src/profile/HttpRequestEvent.java
package profile;

import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;

/**
 *
 * @author koduki
 */
@Category({ "Application Profile" })
@Label("HTTP Request")
public class HttpRequestEvent extends Event {

    @Label("Method")
    public String method;

    @Label("URL")
    public String url;
}

続いてRubyでJavaコードを呼び出します。

require 'sinatra'
Java.import 'profile.HttpRequestEvent'

get '/' do
  event = HttpRequestEvent.new
  event.begin
  r = 'Hello world!'
  event.end

  event.setUrl("/")
  event.setMethod("get")
  event.commit

  r
end

Java.importでJavaのコードをrequireします。JRubyとTruffleRubyで構文が少し違うのでご注意ください。
begin/endで囲んだ範囲の処理をdurationとして自動で記録します。その後、先ほどJavaのクラスで指定した任意のプロパティに値を入れる事ができます。

続いて以下のコマンドで実行します。

$ javac java/src/profile/HttpRequestEvent.java -d java/classes
$ ruby --jvm --vm.classpath=./java/classes/ --vm.XX:StartFlightRecording=settings=profile,disk=true,filename=myapp.jfr app.rb

新しいオプションとして--vm.classpathを足しています。こちらで作ったJavaのカスタムイベントをクラスパスに通します。

準備はできたのでabコマンドで適当にリクエストを投げてみます。

$ ab -n 5000 -c 10 "http://localhost:4567/" 

こちらrubyのプロセスを終了してmyapp.jfrを見てみると以下のようにカスタムイベントが表示されます。

image.png
image.png

今回は単純にリクエストを入れましたが障害時に便利な情報を入れておくと良いでしょう。ちなみに閾値の設定とかもできます。

ちなみにJMC以外で例えばMetabaseとかで分析したい場合は2019-12-09
JFRをBigQuery/Metabaseのオレオレダッシュボードで可視化する
をご参照ください。

まとめ

Rubyの本番時の障害分析はおそらくNewRelicなどのAPMを中心に行われてるのでは無いかと思われます。

ただスレッドロックとか細かい情報も見たいことは本番運用してたらありそうなので、JFRはAPMよりも障害分析に特化した機能が多くRubyでも使おうと思えば使えるのは助かります。

今回はビジネスロジックの中に直書きしましたが、Rubyなのでメソッド呼び出しをフックして自動で書き込むとかFW側にJFRのロジックを入れれば、グッと使いやすくなると思います。

TruffleRubyはパフォーマンスが注目されがちですが、こうしたJavaのエコシステムと連携できるのも魅力の一つかと考えています。

ただ、スタックトレースがRuby側まで追いきれないようなので、この辺はやはりRubyネイティブのものがあると良いですね。

それではHappy Hacking!

参考

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