:bank: サービスで利用する金融機関コードのAPIを公開するには 全国銀行協会 から 金融機関店舗情報 を購入するのでしょうが、まず入手までに時間がかかります。そして入手できても厳しいハードルがあって、メディアが :cd: CD-ROM で届きます。さらに動作OSが Windows(日本語) という!もちろんファイルのコードは シフトJIS です。

そんなわけでOSSの金融機関コードを使ってプロトタイプを作ります。全銀協の注文フォームを入力する時間より早くAPIが完成するでしょう。
ZenginCode source dataJavaScriptPython にも対応していますが Ruby on Rails で作成しました。

image.png

:octocat: GitHub に今回のコードを公開しています。

Tutorial

Rails New

  • Ruby on Rails は --api のオプションで簡単に REST API を作成することができます。
$ rails new zengin-code-search --api
$ cd zengin-code-search/

Generate Scaffold

  • モデルは一つの金融機関に複数の店舗が属する関係なので bank has many branches を構築します。
  • 名前には漢字と片仮名さらに平仮名とローマ字があるので、それぞれに項目を作成します。
$ rails generate scaffold bank code name name_kana name_hira name_en
$ rails generate scaffold branch code name name_kana name_hira name_en bank:references

:pencil: KanaHiraISO 15924 で分類されているコードで、ちなみに漢字は Hani です。

Rails Database Migration

  • codeUNIQUE制約 を設定します。他に NOT NULL制約 などデータベースの設定をします。
db/migrate/20171201132032_create_banks.rb
class CreateBanks < ActiveRecord::Migration[5.1]
  def change
    create_table :banks do |t|
      t.string :code, null: false, default: '', index: { unique: true }
      t.string :name, null: false, default: ''
      t.string :name_kana, null: false, default: ''
      t.string :name_hira, null: false, default: ''
      t.string :name_en, null: false, default: ''

      t.timestamps
    end
  end
end
  • bank_idcodeUNIQUE制約 を設定します。他に NOT NULL制約 などデータベースの設定をします。(大事なことなので)
db/migrate/20171201132039_create_branches.rb
class CreateBranches < ActiveRecord::Migration[5.1]
  def change
    create_table :branches do |t|
      t.string :code, null: false, default: ''
      t.string :name, null: false, default: ''
      t.string :name_kana, null: false, default: ''
      t.string :name_hira, null: false, default: ''
      t.string :name_en, null: false, default: ''
      t.references :bank, foreign_key: true

      t.timestamps
    end
    add_index :branches, [:bank_id, :code], unique: true
  end
end
  • rails db:migrate コマンドでデータベースを作成します。
$ rails db:migrate

Nested Resources

  • ネストしたリソースのパターンはよく使います。ですが rails generate コマンドで作成したコードを少し編集する必要があります。

Rails Models

  • has_many :branches は金融機関が複数の店舗を持つことを示します。
  • dependent: :destroy は金融機関が削除されると店舗も削除されます。
app/models/bank.rb
class Bank < ApplicationRecord
  has_many :branches, dependent: :destroy
end
  • belongs_to :bank は店舗が金融機関に属することを示します。
app/models/branch.rb
class Branch < ApplicationRecord
  belongs_to :bank
end

Rails Controllers

  • POST:create, GET:index, GET:show, PATCH/PUT:update, DELETE:destroy のHTTPとRubyのメソッドで CRUD に対応しています。
app/controllers/banks_controller.rb
class BanksController < ApplicationController
  before_action :set_bank, only: [:show, :update, :destroy]

  # GET /banks
  def index
    @banks = Bank.all

    render json: @banks
  end

  # GET /banks/1
  def show
    render json: @bank
  end

  # POST /banks
  def create
    @bank = Bank.new(bank_params)

    if @bank.save
      render json: @bank, status: :created, location: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /banks/1
  def update
    if @bank.update(bank_params)
      render json: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # DELETE /banks/1
  def destroy
    @bank.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def bank_params
      params.require(:bank).permit(:code, :name, :name_kana, :name_hira, :name_en)
    end
end
  • 入れ子にするには Branch クラスを @bank.branches に変更します。
app/controllers/branches_controller.rb
class BranchesController < ApplicationController
  before_action :set_bank
  before_action :set_branch, only: [:show, :update, :destroy]

  # GET /branches
  def index
    @branches = @bank.branches

    render json: @branches
  end

  # GET /branches/1
  def show
    render json: @branch
  end

  # POST /branches
  def create
    @branch = @bank.branches.new(branch_params)

    if @branch.save
      render json: @branch, status: :created, location: [@bank, @branch]
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /branches/1
  def update
    if @branch.update(branch_params)
      render json: @branch
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # DELETE /branches/1
  def destroy
    @branch.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find(params[:bank_id])
    end

    def set_branch
      @branch = Branch.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def branch_params
      params.require(:branch).permit(:code, :name, :name_kana, :name_hira, :name_en, :bank_id)
    end
end

Rails Routes

  • resources のルーティングを doend に記述するこで入れ子にします。
config/routes.rb
Rails.application.routes.draw do
  resources :banks do
    resources :branches
  end
end

Rails Server

  • rails server コマンドで起動して URL にアクセスします。
$ rails server
$ open http://localhost:3000/banks

:pencil: APIを作るだけなら数分で完成します。

Rails Database Seed

  • 金融コードは ZenginCode のライブラリを使ってデータベースに登録します。
  • Gemfile ファイルに require: false を記述すると明示的に require を記述しないとロードされません。
Gemfile
gem 'zengin_code', require: false
  • zengin_code を使うのは Seed にのみになので require をこちら記述します。
db/seeds.rb
require 'zengin_code'

ZenginCode::Bank.all.each do |original_code, original_bank|
  puts "== #{original_code}:#{original_bank.name}"
  bank = Bank.find_or_initialize_by(code: original_code)
  bank.name = original_bank.name
  bank.name_kana = original_bank.kana
  bank.name_hira = original_bank.hira
  bank.name_en = original_bank.roma
  bank.touch unless bank.new_record?
  bank.save!

  original_bank.branches.each do |original_code, original_branch|
    puts "-- #{bank.code}:#{bank.name} #{original_code}:#{original_branch.name}"
    branch = bank.branches.find_or_initialize_by(code: original_code)
    branch.name = original_branch.name
    branch.name_kana = original_branch.kana
    branch.name_hira = original_branch.hira
    branch.name_en = original_branch.roma
    branch.touch unless branch.new_record?
    branch.save!
  end
end

puts "Bank: #{Bank.count}, Branch: #{Branch.count}"
  • rails db:seed コマンドをで金融コードを登録します。
$ rails db:seed
Bank: 1345, Branch: 31169

:pencil: 数分かかりますがサンプルデータが作成されます。

Rails Server

  • 入れ子で URL にアクセスすることができます。
$ rails server
$ open http://localhost:3000/banks/1
$ open http://localhost:3000/banks/1/branches/1

Overriding Named Route Parameters

Rails Models

  • to_param メソッドに変更する項目を記述します。
app/models/bank.rb
class Bank < ApplicationRecord
  has_many :branches, dependent: :destroy

  def to_param
    code
  end
end
app/models/branch.rb
class Branch < ApplicationRecord
  belongs_to :bank

  def to_param
    code
  end
end

Rails Controllers

  • find(params[:id]) メソッドを find_by(code: params[:code]) メソッドに変更します。
app/controllers/banks_controller.rb
class BanksController < ApplicationController
  before_action :set_bank, only: [:show, :update, :destroy]

  # GET /banks
  def index
    @banks = Bank.all

    render json: @banks
  end

  # GET /banks/:code
  def show
    render json: @bank
  end

  # POST /banks
  def create
    @bank = Bank.new(bank_params)

    if @bank.save
      render json: @bank, status: :created, location: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /banks/:code
  def update
    if @bank.update(bank_params)
      render json: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # DELETE /banks/:code
  def destroy
    @bank.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find_by(code: params[:code])
    end

    # Only allow a trusted parameter "white list" through.
    def bank_params
      params.require(:bank).permit(:code, :name, :name_kana, :name_hira, :name_en)
    end
end
app/controllers/branches_controller.rb
class BranchesController < ApplicationController
  before_action :set_bank
  before_action :set_branch, only: [:show, :update, :destroy]

  # GET /banks/:bank_code/branches
  def index
    @branches = @bank.branches

    render json: @branches
  end

  # GET /banks/:bank_code/branches/:code
  def show
    render json: @branch
  end

  # POST /banks/:bank_code/branches
  def create
    @branch = @bank.branches.new(branch_params)

    if @branch.save
      render json: @branch, status: :created, location: [@bank, @branch]
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /banks/:bank_code/branches/:code
  def update
    if @branch.update(branch_params)
      render json: @branch
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # DELETE /banks/:bank_code/branches/:code
  def destroy
    @branch.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find_by(code: params[:bank_code])
    end

    def set_branch
      @branch = @bank.branches.find_by(code: params[:code])
    end

    # Only allow a trusted parameter "white list" through.
    def branch_params
      params.require(:branch).permit(:code, :name, :name_kana, :name_hira, :name_en, :bank_id)
    end
end

Rails Routes

  • param オプションに変更する項目を記述します。
config/routes.rb
Rails.application.routes.draw do
  resources :banks, param: :code do
    resources :branches, param: :code
  end
end

Rails Server

  • 金融コードで URL にアクセスすることができます。
$ open http://localhost:3000/banks/0001
$ open http://localhost:3000/banks/0001/branches/001

:pencil: FriendlyId も同様の機能のライブラリです。

Search API

  • 検索機能には SearchCop のライブラリを使います。
Gemfile
# https://github.com/mrkamel/search_cop
gem 'search_cop'

Rails Models

  • search_scope のオプションの left_wildcard: false で前方一致検索ができます。
app/models/bank.rb
class Bank < ApplicationRecord
  include SearchCop

  search_scope :search do
    attributes names: [:name_kana, :name_hira, :name_en]
    options :names, left_wildcard: false
  end

  has_many :branches, dependent: :destroy

  def to_param
    code
  end
end
app/models/branch.rb
class Branch < ApplicationRecord
  include SearchCop

  search_scope :search do
    attributes names: [:name_kana, :name_hira, :name_en]
    options :names, left_wildcard: false
  end

  belongs_to :bank

  def to_param
    code
  end
end

:pencil: 片仮名と平仮名とローマ字を検索の対象に設定しています。

Rails Controllers

  • 検索のコントローラを作成します。
$ rails generate controller search/banks index
$ rails generate controller search/branches index

search メソッドで前方一致検索ができます。

app/controllers/search/banks_controller.rb
class Search::BanksController < ApplicationController

  # GET /search/banks
  def index
    @banks = Bank.search(params.require(:query))

    render json: @banks
  end
end
app/controllers/search/branches_controller.rb
class Search::BranchesController < ApplicationController
  before_action :set_bank

  # GET /search/banks/:bank_code/branches
  def index
    @branches = @bank.branches.search(params.require(:query))

    render json: @branches
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find_by(code: params[:bank_code])
    end
end

:bulb: params.require(:query)query パラメータを必須にすることができます。

Rails Routes

  • 検索のルーティングを記述します。
config/routes.rb
Rails.application.routes.draw do
  namespace :search do
    resources :banks, param: :code, only: [:index] do
      resources :branches, only: [:index]
    end
  end

  resources :banks, param: :code do
    resources :branches, param: :code
  end
end

Rails Server

  • 入れ子で検索の URL にアクセスすることができます。
open http://localhost:3000/search/banks?query=mizu
open http://localhost:3000/search/banks/0001/branches?query=hon

Model Serializer

  • レスポンスの項目に id やタイムスタンプが含まれるので ActiveModelSerializers のライブラリで必要な項目のみを設定します。
Gemfile
# https://github.com/rails-api/active_model_serializers
gem 'active_model_serializers'
$ rails generate serializer bank
$ rails generate serializer branch
app/serializers/bank_serializer.rb
class BankSerializer < ActiveModel::Serializer
  attributes :code, :name, :name_kana, :name_hira, :name_en
end
app/serializers/branch_serializer.rb
class BranchSerializer < ActiveModel::Serializer
  attributes :code, :name, :name_kana, :name_hira, :name_en
end

Tips

OSSのライブラリだけで簡単に金融コードのAPIができましたね。フロントエンドは React でインクリメンタルサーチができるコンポーネントを作るところまで書くと長くなるのでまたの機会に公開します。この記事の作成中に Rails ガイド(日本語)Overriding Named Route Parameters の翻訳がないのに気がついて Pull Request をしました。こんな感じにOSS開発に参加したいなと思ったら OSS Gate のワークショップに参加してみましょう。