初心者エンジニアが1ヶ月スタートアップでDB設計からAPI実装まで学んだ話

プログラミングを始めて11ヶ月で、スタートアップの自社サービス開発を初めて経験しました。入って1ヶ月目で「DB設計」→「マイグレーション」→「API実装」までを経験したので、それを通して学んだことをアウトプットします。

 

DB設計

DB設計は最初に、「このDB・テーブルはどのような目的・方法で使用するのか?」を理解して、仕様を考えることが重要。仕様・テーブル設計がまとまった早い段階でレビューをもらうと差し戻しなどの無駄な時間が大幅に減り、スムーズに仕事が進むことを学んだ。

個人的にDB設計はマインドマップを使用すると思考が固まりやすく便利なので、今後もマインドマップDB設計を極めていく!

XMind: http://jp.xmind.net/

 

① エンティティの定義

DBに保存する全てのデータを洗い出す。

② 正規化

扱う全てのデータを洗い出し、以下の正規化を行う

第一正規化: 1つのカラムには1つの値しか入らないようにテーブルを分割する
第二正規化: 種類ごとにエンティティをテーブル分割する(間違ったデータが保存されるのを防ぐ効果もある)
第三正規化: 従属関係にあるカラムの組み合わせが主キー以外で見つかった場合にテーブル分割をする
第四正規化: 主キーに対して複数の同じ種類のレコードが存在する場合にテーブル分割をする
第五正規化: 分割した2つのテーブルだけでは従属性がわからない場合そのテーブル間に両方のIDを持つ中間テーブルを作る

③ データ型の決定

各テーブルカラムに入るデータを想定し、データ型を決定していく

【主なデータ型】
・INT: 整数型(TINYINT ZEROFILL型と同様)
範囲 : ‘-2147483648から2147483647’

・VARCHAR: 可変長文字列
範囲 : 0から65535バイト

・TEXT: TEXT型
範囲 : 最長65,535 (216 – 1) バイト

・FLOAT: 浮動小数点数型
フォーマット : ‘0.000..’
範囲 : ‘-3.402823466E+38 から -1.175494351E-38’, ‘1.175494351E-38 から 3.402823466E+38’

・DATE: 日付型
フォーマット : ‘YYYY-MM-DD’
範囲 : ‘1000-01-01’ から ‘9999-12-31’

・DATETIME: 時間単位の日付型
フォーマット : ‘YYYY-MM-DD HH:MM:SS’
範囲 : ‘1000-01-01 00:00:00’ から ‘9999-12-31 23:59:59’

・TIMESTAMP
フォーマット : ‘YYYY-MM-DD HH:MM:SS’
範囲 : ‘1970-01-01 00:00:01’ から ‘2037-12-31 23:59:59’

④ 制約の決定

入りうるデータを想定し制約を決めていく。(ここで、またレビューをもらうとよい)

・NOTNULL制約: NULLはSQLを扱う上で色々な問題を引き起こす原因になるので、カラムには可能な限りNOTNULL制約を付与する
・INDEX: カラムのデータを複製し、データを取得する際に検索が行いやすいようにする
・CHECK制約: カラムの取りうる値の範囲を設定する

⑤ ER図に起こす

MySQLworkbenchなどのツールを使って、①〜④までのまとまった内容をER図におこす。

MYSQL workbenchにおけるER図の見方 - Qiita
#ER図の構成要素 -エンティティ・・・データのまとまり(下図では全体を囲っている四角) -アトリビュート(属性)・・・エンティティの中の属性情報(下図ではfilm_id SMALLINT以下のデータ) -リレーション・・・エンティテ...

 

 

DB設計をRailsマイグレーションに落とす

ここは、レビューなどで確認をとったER図を指定通りに、Railsマイグレーションに起こす作業。型などの指定の仕方に注意しながら、単純なミスを減らすように意識する。

class CreateHoges < ActiveRecord::Migration
	def change
		create_table :hogehoges do |t|
			# hugaテーブルにリレーションを定義(indexはる・外部キー制約・NOTNULL制約)
			t.references :huga, index: true, foreign_key: true, null: false
			# string型のtitleを定義
			t.string :title, null: false
			# text型のbodyを定義
			t.text :body, null: false
			# integer型のtypeを定義(整数がカラムに入るため)
			t.integer :type, null: false
			# float型のpointを定義(浮動小数点がカラムに入るため、nullの場合が考えられる時のみNOTNULL制約をつけない)
			t.float :point
			# datetime型のdateを定義
			t.datetime :date
			t.timestamps null: false
		end
	end
end

 

リレーションを定義する場合は、modelにも定義が必要

[HugaHuga.rb]
class HugaHuga < ActiveRecord::Base
  belongs_to :test
  # class_name(model名と別の名前でリレーションを定義する場合)
  has_many :hoges, class_name: 'Hogehoge'
end
[Hogehoge.rb]
class Hogehoge < ActiveRecord::Base
  belongs_to :test
  # foreign_key(複数の外部キーを指定する場合)
  belongs_to :huga, class_name: 'hugahuga', foreign_key: :hugahuga_id
end

 

マイグレーション実行

$ bin/rails db:migrate
// 特定のmigrationだけやり直す
$ bin/rails db:migrate:redo VERSION=20181114054924

 

Railsマイグレーションのテストを書く

ここでやるのは、主にリレーションが正しく定義できているか

[hugahuga.spec.rb]
require 'rails_helper'
 RSpec.describe HugaHuga, type: :model do
  describe 'relation' do
    it { is_expected.to belong_to(:test) }
    it { is_expected.to have_many(:hoges) }
  end
end
[hogehoge.spec.rb]
require 'rails_helper'
 RSpec.describe Hogehoge, type: :model do
  describe 'relation' do
    it { is_expected.to belong_to(:test) }
    it { is_expected.to belong_to(:huga) }
  end
end

 

RSpecテスト実行

$ bundle exec rspec
// 特定のRSpecのテスト
$ bundle exec rspec spec/tests_spec.rb

 

 

RailsAPI実装

RailsAPI実装を始める前に、「どのような挙動を期待するのか?」を抽象化から具体的な仕様に落としていく。仕様に落としていく中で「誰がこのAPIをたたくのか(ユーザーなのかシステムなのか)?」「何の値がparamsで渡ってくるのか」「エンドポイントはどうするか?」などを最初にしっかり固めることが重要だということを学んだ。

 

① バリデーションを最初に定義

desc '[実装するAPIの説明]',
               headers: [アクセストークンなどの認証がある場合など]
               detail: [認証などの詳細]
               entity: [entity化してJSONフォーマットでレスポンスを返す指定など]
               # forbidden: Client Error403(アクセス拒否)、invalid_parameter: パラメータバリデーション, unauthorized: Client Error401(認証失敗), conflict: Client Error409
               http_codes: [*I18n.t([:forbidden, :invalid_parameter, :unauthorized, :conflict], scope: :sc)]

 

② paramsを定義

params do
	# type:、allow_blank:を定義する
    requires :hoge_id, type: Integer, allow_blank: false, desc: 'Hoge'
    requires :hugas, type: Array do
		optional :title, type: String, allow_blank: false, desc: 'Title'
        optional :body, type: String, allow_blank: true, desc: 'Body'
        optional :text, type: String, desc: 'Text'
end

 

③ API本体

基本的に複雑な処理を書く(複数のクエリを指定する)場合などは、serviceに処理を書くのがよい。

例) GET処理
post '[エンドポイントパス]' do
	begin
	# API処理を書く
	# 複雑な処理を書く(複数のクエリを指定する)場合などは、serviceに処理を移す
	SEARCH_LIMIT = 10
	SEARCH_PLUCK = 'name'
	# serviceのsearchメソッドを使う
	result = Service.search(search_text: params[keyword], limit: SEARCH_LIMIT, pluck: SEARCH_PLUCK)
	end
end
class Service
	# searchメソッドを使う(search_text:デフォルト値なし、limit: デフォルト値nil、pluck: デフォルト値nil)
    def self.search(search_text:, limit: nil, pluck: nil)
	# バリデーション処理を書く
	# pluckの値が渡って来なかった(デフォルト値nil)の場合[]を返すなど
    return [] if pluck.nil?
	
	# 前方一致検索
    search_result = Hoge.where("title like ? or body like ?", "#{search_text}%", "#{search_text}%")
        .order(list_order: :desc).limit(limit).pluck(pluck)
    end
    search_result
  end
end

 

 

Rspecの実装

specテストの実装を始める前に、「意図しない値が渡ってきた場合」などを考え、まずテストする要件を洗い出す作業が非常に重要ということを学んだ。またその上で、レビューをもらうと差し戻しなどの無駄な時間が大幅に減る。

describe '[APIのエンドポイント[GET]]' do
    let(:method) { :get }
    let(:path) { '[APIのエンドポイント]' }
	# デフォルトのパラメーター指定(ここでは、context別にパラメーターを指定していくので未設定)
    let(:parameters) { {} }
    
	# parametersが空配列の場合(error: ["keywordが存在しません"]を期待する)
    context 'Invalid parameters (invalid value)' do
      let(:parameters) { {} }
      it_behaves_like 'status_code_and_response_body' do
        let(:status_code) { 400 }
        let(:result) { { error: ["keywordが存在しません"] } }
      end
    end
	# 空白のparametersが送られた場合([]を期待する)
    context 'Search text is blank' do
      let(:parameters) { { keyword: '' } }
      it_behaves_like 'status_code_and_response_body' do
        let(:status_code) { 200 }
        let(:result) { [] }
      end
    end
	# keyword: 'Ho'のparametersが送られた場合(["HogeHoge","","hogehoge","","Hoge"]を期待する)
    context 'order + limit3' do
      let(:parameters) { { keyword: 'Ho' } }
      it_behaves_like 'status_code_and_response_body' do
        let(:status_code) { 200 }
        let(:result) { ["HogeHoge","","hogehoge","","Hoge"] }
      end
    end
end

 

コメント