Edited at

Zengin Code Search API on Rails

More than 1 year has passed since last update.

: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 のワークショップに参加してみましょう。