RoadMovie

write down memos or something I found about tech things

【Jest, enzyme】 useSelectorやuseDispatchを使ったFunctionComponentをうまくテストする

ReactのテストでJestやenzymeを使うことは多いと思うのですが、React v16.8以降でFunctionComponentでの書き方を使う機会が増えたにもかかわらず、useSelectorやuseDispatchあたりをclassでなくFunctionComponentで使ったテストケースがあまり見つからず困ったのでサンプルをいくつか書いてみます。

typescriptで書いてますが、使わない場合はよしなに読み替えていただければと思います。

The product code

かなり単純化したものになりますが、下記のようなコードに対してテストを書きたい場合です。

import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import * as actions from './actions'

const UserList: React.FC = () => {
  const dispatch = useDispatch()
  const users = useSelector(state => state.reducer.users)
  const companies = useSelector(state => state.reducer.companies)

  const submit = (userId: number) => {
    dispatch(actions.postUser(userId))
  }

  return (
    <div>
      {users.map(user =>
        <div>
          <p>{user.name}</p>
          <button onClick={() => submit(user.id)}>submit</button>
        </div> 
      )}
    </div>
  )
}

Writing Test code with mocking

単純なのですが、いろいろハマるところがあったのでひとつずつ解いていきます。
※ やり方はここで書く以外にも何通りかあります

# UserList.spec.tsx

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { shallow } from 'enzyme'
import * as actions from './actions'

jest.mock('react-redux') // -> ① 
jest.mock('./actions')       // -> ②

まず、① react-redux をimportして jest.mock しておくのが最初のポイントです。② actionsもここでmockしておくと良いです。

const useSelector = useSelector as jest.mock // -> ③

useSelectorはここでmockしましょう。いよいよ具体的にテストを書いていきます。

describe('UserList', () => {
  afterEach(() => {
    jest.resetAllMocks() // -> ④
  })

  let wrapper
  beforeEach(() => {
    const users = [{ id: 1, name: 'userName' }] as User[]
    const companies = [{ id: 1, divisionName: 'R&D' }]
    
    useSelectorMock.mockReturnValueOnce(users) // -> ⑤
    useSelectorMock.mockReturnValueOnce(companies) // -> ⑥
    useDispatch.mockReturnValue(jest.fn()) // -> ⑦

    wrapper = shallow(<UserList />)
  })

  it('renders view', () => {
    expect(wrapper.text()).toEqual(
      expect.stringContaining('userName')
    )
  })
})

これでテストには通ると思います。④でafterEachとしてmock化の解除を行います。beforeEachの中が需要で、⑤⑥でReturnValueOnceを設定することで、それぞれuseSelectorの返すべき値をusersとcompaniesに振り分けています。simulateなどで再レンダリングさせる際は、このペアをもう一度事前に登録しておくことを忘れないようにしてください。⑦はuseDispatchのmockです。

Additionally

ついでに、actionのテストも簡単に見てみましょう。

describe('click the button', () => {
  let spy
  beforeEach(() => {
    // -> actionsは最初にmock化してあります
    spy = jest.spyOn(actions, 'postUser')

    // -> 先程のbeforeEachが効いているscope内として、
    // あと2つ再レンダリング用に書きます
    useSelectorMock.mockReturnValueOnce(users)
    useSelectorMock.mockReturnValueOnce(companies)
  })

  it('calls api', () => {
    wrapper.find('button').simulate('click')
    expect(spy).toHaveBeenCalled()
  })
})

こんな感じです。 import * as actions のmock化とかも地味に躓きましたが、上記のような感じで上手く行けると思います。他にも act 周りとかで細かいtipsはあるのですが、それはまたの機会に。enzymeのshallow, mountになれるのもjestテストを快適に書くポイントですよね。

StorybookをReact, Typescriptな環境に導入する

StorybookをReact, Typescript, Atomic Designな環境に導入する手順を紹介します。

Why Storybook?

Frontendの開発を行っていると、再利用性を高めたいという意識が湧いてくると思うのですが、それをチーム間で共有したり、デザイナーさんと認識を合わせるのが容易でなかったりします。また、開発者も「このコンポーネントどこでどう使ってたっけ?」と言うように、視認性を持ちながら検索性も高くするというのがなかなか難しかったりします。

そこで、Storybook ( https://storybook.js.org ) の出番です。

How to install Storybook in my environment?

まず、先にも記述しましたが、今回は、下記の要件のアプリへの導入を紹介します。

  • React.js
  • yarn
    • npmな方は適宜読み替えてください
  • Typescript
    • 特に使ってなければ問題ないのですが、Typescript特有のハマリポイントの紹介もしています
  • SCSS (CSS)
    • SCSSを利用しているコンポーネントの使用にもいくつかハマリポイントがあったのでそこも紹介しています。
  • Atomic Design
    • こちらもこれで運用していなくても全く問題なく読めますが、例として考えやすいのでこちらをベースにしています。

公式ドキュメントはこちらです。
https://storybook.js.org/docs/guides/guide-react/

また、Storybookには数多くのaddonがあり、それらを利用することでより便利にすることが可能です。この記事の中でもある程度メジャーそうなaddonをインストールしながら進めていきます。



まずは下記のコードをFrontend root(package.jsonなどを置いている場所)な場所で実行しましょう。

$ npx -p @storybook/cli sb init

これでかなりの部分をいい感じに自動で用意してくれます。下記のコマンドを打てばひとまずstorybookを確認することができます。(デフォルトで、Welcome.jsとButton.jsが用意されているはず)

$ yarn storybook

f:id:mr7myself:20200205132749p:plain

いい感じですね。 さて、ここからが本題です。サンプルコードは .js で書かれていますが、 .ts あるいは .tsx で作りたいですよね。そうするといくつか追加で設定が必要になってきます。

その前に、軽くstorybookのディレクトリ構造などに触れておきたいのですが、一番はじめのコマンドで作られたのを見るとわかるように、storybookは下記の2つのディレクトリをベースに作っていきます。

stories
├── atoms/
└── molecules/
.storybook
├── config.js
├── main.js
├── manager.js
└── preset.js

.storybook

(各ファイルはあとから出てくるので今わからなくても問題ないです)

  • config.js
    • 全体に反映させたい設定などを行います
  • main.js
    • webpack.config.jsのstorybook用という感じのファイルです。loaderの設定などを行います
  • manager.js
    • addonの初期設定を行います
  • preset.js
    • こちらはtypescript用に一行設定が必要なファイルです

stories

storybookでは、ひとつひとつのファイルをstoryと呼び、ファイル名.stories.拡張子 という形で保存すると、storybookに反映されます。このディレクトリ構造は一旦atomic designに沿って、atoms, moleculesというような形にしてあります。



次に進む前に、前述したaddonをいくつか入れておきましょう。

$ yarn add -D @storybook/addon-knobs @storybook/addon-viewport @storybook/addon-storysource react-docgen-typescript-loader @storybook/addon-info @storybook/addon-console

typescript用のライブラリも入れておきます。

$ yarn add -D @types/storybook__react @types/storybook__addon-info @types/storybook__addon-actions @types/storybook__addon-knobs

ちなみに、どのライブラリだったか忘れたのですが、2020/02/05現在でcore-jsのv2に依存しているのがありまして(stableで最新がv3なのでイケてないのですが)、core-js v2を入れなければなりません。実コードでv3使ってる方は、addonを外すようにしましょう。core-js v2の入れ方は下記のとおりです。

$ yarn add -D core-js@2

さて、各ライブラリが何をやっているのかは別途検索していただければと思いますが、簡単に紹介しておくと、

  • addon-info
    • componentが受け取るpropsの情報や、componentの使い方をviewに表示してくれる
  • addon-knobs
    • propsの内容をviewから変更してデザインの変化などを確認できる

このあたりは便利です。



では、早速typescriptに合わせて設定を行っていきましょう。まずは、簡単なところから。 .storybook/preset.js に下記のように書いて保存しましょう。

module.exports = ['@storybook/preset-typescript']

次に、.storybook/main.js を、下記のように書いて保存します。addonの設定なども既に記述しています。

const path = require('path')

module.exports = {
  stories: ['../stories/**/*.stories.tsx'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-knobs/register',
    '@storybook/addon-actions/register',
    '@storybook/addon-viewport/register',
    {
      name: '@storybook/preset-typescript',
      options: {
        tsLoaderOptions: {
          configFile: path.resolve(__dirname, './tsconfig.json')
        }
      }
    }
  ],
  webpackFinal: async config => {
    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      use: [
        {
          loader: require.resolve('babel-loader')
        },
        // Optional
        {
          loader: require.resolve('react-docgen-typescript-loader')
        }
      ]
    })
    config.module.rules.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader?modules', 'sass-loader'],
      include: path.resolve(__dirname, '../')
    })
    config.resolve.extensions.push('.ts', '.tsx')
    return config
  }
}

webpack.config.jsと同じように、typescriptとscssのloaderの設定をここで行います。addonの登録もここで行います。

これで一通りの設定はおしまいです。例として、サンプルのWelcome.jsとButton.jsを Welcome.tsxButton.tsx に変更して、 yarn storybook でstorybookを立ち上げてみましょう。うまくいきましたか?

Advanced (さらに便利に)

グローバルなaddonの初期設定

Storybookのパネルが下に表示されると思うのですが、邪魔なので右側にしたい。といった場合に、 .storybook/manager.js にその設定を書いておけば、常に右側になります。

import { addons } from '@storybook/addons'

addons.setConfig({
  panelPosition: 'right'
})

すべてのstoriesにaddonを適用

addon-infoaddon-knobs は便利なので、すべてのstoriesに適用させておきたくなります。 .storybook/config.js に下記のように書いて保存しましょう。

import { addDecorator } from '@storybook/react'
import { withInfo } from '@storybook/addon-info'
import { withKnobs } from '@storybook/addon-knobs'

import '../css/app.css'

addDecorator(withInfo({ inline: true }))
addDecorator(withKnobs)

ついでに、実コードのcssをインポートする方法も載せておきました。こうしたstoriesへのグローバルな設定はここで行なえます。

静的ファイルがstorybookでうまく読み込めない

storybookの立ち上げコマンドを工夫すればできるようになります。package.json にかかれている実行コマンドを下記のように修正しましょう。

"storybook": "start-storybook -p 6006 -s ./staticファイルのディレクトリ名",


これでかなり便利にStorybookを使えるようになったと思います。最後にサンプルとして上記の設定を行った上でのコードを載せておきます。

import React from 'react'
import { action } from '@storybook/addon-actions'
import { storiesOf } from '@storybook/react'
import { text, boolean } from '@storybook/addon-knobs'

import Button from '@/components/atoms/Button' // storyに載せたいコンポーネントをインポート

storiesOf('atoms', module).add('Button', () => (
  <Button
    title={text('Title', 'テキスト')}
    disabled={boolean('Disabled', false)}
    onClick={action('clicked')}
  />
))

storybook sample view
storybook

以上です。Storybookはatomic designを推進するのにもかなり相性良さそうですね!

【実践: 詳しくわかる】TerraformでCircleCIを通してAWSにECS環境を自動構築する方法

まず最初に今回の記事で扱う内容を書かせていただきます。

1. docker-composeで開発環境を構築し
2. masterにpushした時のみCircleCIでECRにDockerイメージを構築し
3. 同時にCircleCIからterraformで本番環境のインフラをAWSにECSで整え
4. Deployを完了する

ということを実際のコードを交えながら解説していきたいと思います。つまるところmasterにプッシュしたら自動でDockerイメージが作られて自動で環境整えてデプロイされている状態を目指す、という話です。Blue/Greenデプロイと呼ばれるやつの自動化ですね。


技術的には、下記のものを扱いますが、ところどころご自身の技術スタックと違っていても読み替えられるかと思います。

  • terraform v0.12.18 (v0.11とv0.12で少し違いがあるので注意)
  • Rails (他のWAFでも読み替えられると思います)
  • CircleCI ( Continuous Integration and Delivery - CircleCI )
  • AWS & ECS (AWSに関してはECS以外にも様々なサービスをterraformを通して利用します)


Terraformの構成としては、環境ごとのVPC内に、2つのavailability zoneを用意し、それぞれの中にpublic, private subnetを1つずつ用意します。ロードバランサがpublicにいて、Clusterはprivateにいるような形です。また、web app環境としてstagingとproductionを用意し、RDS(MySQL)とつなぎます。Route53で設定しているドメインの設定や、taskでmigrationを走らせるところなどもカバーしています。


1. docker-composeで開発環境を構築

まずは手始めに一番簡単なところから行きます。最早あらゆる視点から考えて開発環境はdocker化しておいたほうが楽なので必須ですね。ここは簡単にyamlファイルだけ載せておきます。

ポイントとしては、Dockerfileのdev用を用意しておくことでしょうか。Rails envの設定などをproductionとわけたり、productionではサイズの小さいimageを使いたいなどの要件に対応できるようにしておきます。

2. masterにpushした時のみCircleCIでECRにDockerイメージを構築

次に、CircleCIの設定をしていきましょう。本当はTerraformから書いたほうが順序的には良いと思うのですが、Terraformの方は長くなるので、後にまわします。CircleCIをRailsに組み込むあたりは、それほど難しくないと思うので割愛します。CircleCI公式サイトの手順に沿って行ってください。

早速ですが、先に .circleci/config.yml の中身を載せます。その後、重要なポイントを解説していきます。

いくつかポイントになりそうな部分を見ていきましょう。

テスト

このファイルでもいくつかポイントが有るのですが、まず、CircleCI上でテストをまわすところです。CIでテストを回す方法は色々ありますが、ここではローカルでテストを回すのと同じように、docker-composeで構築してテストをさせています。jobs -> testの部分になります。deploy前にテストを回すようにしています。

条件によってデプロイする

このファイルでは、masterブランチにマージされたときにstagingにデプロイして、tagが打たれたときにproductionにデプロイするようにしています。この設定は filters で行えます。

ECRにイメージを作る

まずこの話に入る前に、CircleCIにはorbsという仕組みがあり、それを使えばある程度処理が楽になることを知っておきましょう。これにより、ECRへのpushは非常に簡単になっています。workflowの aws-ecr/build-and-push-image がその設定になります。この際にいくつかパラメータが必須になっているので、設定しています。ひとつポイントとして、 extra-build-args というパラメータを設定でき、ここで渡したものは、Rails側でENVとして受け取ることができます。こうすることで環境変数の管理は楽になります。また、 ${CIRCLE_SHA1} は、CircleCI側で乱数を生成して設定してくれるもので、自分で設定する必要はありません。ちなみに、ENV(環境変数)はそもそもどこに設定するのかという話ですが、CircleCIに環境変数を設定する箇所があります。(もしわかりにくければ画像キャプチャを載せることを検討します)

Terraformでデプロイする

Terraformの中身をまだ見ていないので細かいところはわからないと思うのですが、流れとしては、

  • git cloneでterraformのrepositoryを落としてくる(ここではterraform_ecs)
  • terraform getでmoduleを使用可能にする
  • terraform init
  • terraform applyで実際にデプロイする

という感じです。applyの部分にいくつかポイントがあるのですが、まず、 -input=false -auto-approve これでterraformが対話的に聞いてくるのをdisabledしています。その代わりに必要な変数などを -var-var-file で補っています。-varはgit管理したくない機密情報を、-var-fileは環境ごとに違う値を設定したいだけで特にpublicでも問題ないものをファイルに設定して読み込むようにしています。

3. 4. CircleCIからterraformで本番環境のインフラをAWSにECSで整えデプロイ完了

さて、いよいよ本題です。CircleCIでterraform applyするところは先程書いたので、terraformの中身を見ていきましょう。
※ terraformのインストールや、terraform自体の詳しい使い方はご自身で調べてください

Repositoryはこちら github.com

terraformのディレクトリ構成のベストプラクティスはいくつかあるようですが、今回は下記のように配置しています。modulesで使いまわしできるものを用意し、staging, productionなどのように環境ごとにmainファイルを配置します。

.
├── modules
│   ├── ecs
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── policies
│   │   │   ├── ecs-autoscale-role-policy.json
│   │   │   ├── ecs-autoscale-role.json
│   │   │   ├── ecs-execution-role-policy.json
│   │   │   └── ecs-task-execution-role.json
│   │   ├── tasks
│   │   │   ├── db_create_task_definition.json
│   │   │   ├── db_migrate_task_definition.json
│   │   │   └── web_task_definition.json
│   │   └── variables.tf
│   ├── networking
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   └── rds
│       ├── main.tf
│       ├── output.tf
│       └── variables.tf
├── production
│   └── production.tf
└── staging
    ├── _main.tf
    ├── route53.tf
    ├── staging_key
    ├── staging_key.pub
    ├── terraform.tfvars
    └── variables.tf

modules/ecs

まずはmodulesのecsの中から見ていきましょう。こちらになります。
https://github.com/mr-myself/terraform_ecs/blob/master/modules/ecs/main.tf

ECS自体の説明は端折りますが、web appが動くものをserviceとして管理し、migrationなどの単発で良いものはtaskとして定義します。ここでは、まず、Clusterを環境名をprefixに用意し、その中にtaskを用意していきます。細かい説明をするよりはファイルを見ていただいたほうが早いかと思いますが、ロードバランサの設定などやAutoScallingの設定などもここで行っています。

modules/networking

次にnetworkです。vpcgatewayの設定を行います。最初に書いたpublic, private subnetもここで定義します。
https://github.com/mr-myself/terraform_ecs/blob/master/modules/networking/main.tf

modules/rds

次にRDS周り。subnet, security group, dbのインスタンス定義などを行っています。MySQLの5.7として作っています。
https://github.com/mr-myself/terraform_ecs/blob/master/modules/rds/main.tf

補足として、上記の各所で出てくる、 var.xxx という記述ですが、名前の通り変数を渡しています。使いたい箇所で var.xxx のように書き、同じディレクトリ内のvariables.tfに使いたい変数を定義し、 staging/_main.tf などから値を渡すことができます。

staging/__.tf

最後にstaging環境の方を説明して終わりにします(productionもほぼ同じなので)。まずは、アクセスキーを作る必要があるので、staging_keyとしてkey-genで作っておいてください。今回はsampleなので空のファイルを配置していますが、通常はprivate keyはこんな風にさらさないようにしてください。

デフォルトの値としていくつか用意しておきたいものは、terraform.tfvarsに予め値を記載しています。また、短いのでさらっと書きますが、 route53.tfドメインの設定などを行っています。

最後に、 _main.tf です。まず、terraformはstateをどこかに保持しておく必要があり、保持することができる場所は様々あるのですが、今回はせっかくガッツリAWSを使っているので、S3に置くような設定にしています。

terraform {
  backend "s3" {
    encrypt = "true"
    region = "ap-northeast-1"
    bucket = "terraform_ecs-tfstates"
    key = "terraform.tfstate"
    acl = "bucket-owner-full-control"
  }
}

あとは、先程説明してきたmodulesをstaging環境として使えるように、変数に値を入れていっているだけです。




以上となります。さらっと書いたのですが、構築中はいろいろ模索したので、ハマリポイントを思い出したらまた追記していきたいと思います。わかりにくい点などあればコメントいただければと思います。terraformで構築するのは初めてだったのですが、一旦作ってしまうと楽ですし良いですね。

Reactでドロップダウンメニューを実装する

react menu dropdown

Reactでドロップダウンメニューを作成する機会があったのですが、いくつかハマりポイントがあったので書いておきます。まずはじめに要件としては下記のことがあげられます。

  1. ボタンを押すとdropdownメニューが開く
  2. ドロップダウンメニュー以外の場所を押すとblurを発火しメニューを閉じる
  3. メニューボタンはトグルになっていて、open or closeできる
  4. 中のリンクももちろん押せる

なお、Reactはhooksを使用可能なversion以上とします。



実際に書き始めるとなぜこの記事が必要になるか気づかれるかと思うのですが、例えば簡単に下記のように書いてみたとします。

const Menu = () => {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <div>
      <img src={Button} onClick={() => setIsOpen(!isOpen)} onBlur={() => setIsOpen(false)} />
      {isOpen &&
        <ul>
          <li>menu1</li>
          <li>menu2</li>
        </ul>
      }
    </div>
  )
}

export default Menu


一見特に問題なさそうに見えるかもしれませんが、これではonBlurが発火しません。メニューの方にonBlurを当てるのが正しいと思い、下記のように修正してみます。

const Menu = () => {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <div>
      <img src={Button} onClick={() => setIsOpen(!isOpen)} />
      {isOpen &&
        <ul onBlur={() => setIsOpen(false)} >
          <li>menu1</li>
          <li>menu2</li>
        </ul>
      }
    </div>
  )
}

export default Menu


これでもblurの発火がうまくいかないはずです。これらはメニューにフォーカスが当たらないことが原因なので、そこをuseRefなどを使って修正してみます。

const Menu = () => {
  const [isOpen, setIsOpen] = useState(false)
  const menuRef: any = useRef()

  useEffect(() => {
    isOpen && menuRef.current.focus()
  }, [isOpen])

  return (
    <div>
      <img src={Button} onClick={() => setIsOpen(!isOpen)} />
      {isOpen &&
        <ul  
          onBlur={() => setIsOpen(false)} 
          ref={menuRef}
          tabIndex={1}
         >
          <li><a href="/somewhere">menu1</a></li>
          <li>menu2</li>
        </ul>
      }
    </div>
  )
}

export default Menu


ちょっと動く気配がするようになってきました。ここではrefを使ってfocus対象を決め、isOpenの値が変わったときにfocusを当てるようにしています。忘れがちなポイントとして、tabIndex={1} としておかないと、divなどの要素にfocusを当てれないので注意です。ただ、これだとまだ完璧にはうまく行きません。メニューの中の要素をクリックしてみると、リンクに飛べないことがわかります。これはReact特有の問題のようですが、子をクリックすると親のblurが先に発火してしまうので、子のイベントを発火できなくなるようです。これはちょっとイマイチな解決方法ですが、timeoutを使って回避しましょう。下記が完成形です。

const Menu = () => {
  const [isOpen, setIsOpen] = useState(false)
  const menuRef: any = useRef()

  useEffect(() => {
    isOpen && menuRef.current.focus()
  }, [isOpen])

  return (
    <div>
      <img src={Button} onClick={() => setIsOpen(isOpen ? false : true)} />
      {isOpen &&
        <ul  
          onBlur={() => setTimeout(() => setIsOpen(false), 100)} 
          ref={menuRef}
          tabIndex={1}
         >
          <li><a href="/somewhere">menu1</a></li>
          <li>menu2</li>
        </ul>
      }
    </div>
  )
}

export default Menu

これでうまく1~4の要件が満たせました。(しれっと上で変えましたが、imgにセットしたonClickのところもblurで変な挙動をしないためのポイントです。)

Ectoチートシート(データ取得系)

EctoとはORマッパーで、DBとのインターフェースになってくれるものです。RailsでいうActiveRecordです。 Phoenix/ElixirはわかりやすいけどEctoで躓く、という方向けのチートシートになります。Rails使いでPhoenix書いてみてる方が多いと思うので、RailsActiveRecord)の文法と比較して載せています。

英語問題ない方はEctoのGetting Startedがめちゃわかりやすいので、そちらを読んでみることをおすすめします。(※下記のサンプルもそちらを抽出したものです)
Getting Started — Ecto v3.1.7

前提として、FriendsというDB内のPeopleというtable(なのでモデル名は単数のPerson)を使った操作です。取得系のみ載せており、create, update, deleteは上記のドキュメントを読んでください。


対象のテーブルの最初の1件を取得

# ActiveRecord
Person.first

# Ecto
Friends.Person |> Ecto.Query.first |> Friends.Repo.one


対象のテーブルの最後の1件を取得

# ActiveRecord
Person.last

# Ecto
Friends.Person |> Ecto.Query.last |> Friends.Repo.one


対象のテーブル全件取得

# ActiveRecord
Person.all

# Ecto
Friends.Person |> Friends.Repo.all


対象のテーブルのid: 1を取得

# ActiveRecord
Person.find(1)

# Ecto
Friends.Person |> Friends.Repo.get(1)


特定カラムの条件で1件取得

# ActiveReocrd
Person.find_by(first_name: “Ryan”)

# Ecto
Friends.Person |> Friends.Repo.get_by(first_name: "Ryan")


特定カラムの条件でフィルタリングし全件取得

# ActiveRecrod
Person.where(last_name: “Smith”)

# Ecto
Friends.Person |> Ecto.Query.where(last_name: "Smith") |> Friends.Repo.all


注意 Ectoで変数名を条件内に使いたい場合は、pin operatorを使う必要があります。

Friends.Person |> Ecto.Query.where(last_name: ^last_name) |> Friends.Repo.all

Elixir on PhoenixでDBConnectionエラーでアプリが落ちる場合の解決策

環境: Elixir 1.9.1, Phoenix 1.4を使用

エラー内容は下記の通り。mixにライブラリを追加した後に起こる。急にDBConnectionと言われてDB周りのバグかと思い調べたがそうではなく、mixの書き方の問題だった。

web_1   |
web_1   | Generated your_app app                                                                                                                                            
web_1   | ** (Mix) Could not start application your_app: YourApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: YourApp.Repo
web_1   |     ** (EXIT) shutdown: failed to start child: DBConnection.ConnectionPool                                                                                         
web_1   |         ** (EXIT) exited in: GenServer.call(DBConnection.Watcher, {:watch, DBConnection.ConnectionPool.Supervisor, {DBConnection.ConnectionPool.Pool, {#PID<0.374.$>, #Reference<0.342264912.689831938.179060>, Postgrex.Protocol, [types: Postgrex.DefaultTypes, repo: YourApp.Repo, telemetry_prefix: [:your_app, :repo], otp_app: :your_app, timeout: 15000, username: "postgres", password: "postgres", database: "your_app_dev", hostname: "db", port: 5432, pool_size: 10, pool: DBConnection.ConnectionPool]}}}, infinity)                                                                                                                                                                    
web_1   |             ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't $
tarted

起こったこととしては下記と同様。 elixirforum.com

解決策としては下記のリンクの通り。

www.amberbit.com

ライブラリを入れる時に mix.exs の下記パートに applications: [:library_name] を書くとREADMEにあることが多々あるが、Elixirがよしなにやってくれるので書かなくてよい。

def application do
  [
    mod: {YourApp.Application, []},
    extra_applications: [:logger, :runtime_tools] 
  ]
end

私が今回入れてこのバグを引き起こしたのは下記のライブラリ。こちらのREADMEを読むと、applicationsパートにライブラリ名を追加しているが、それは不要。 GitHub - fdietz/elixir-feed-parser: Elixir Feed Parser

エラー文言から理解しにくいバグなので、参考になれば。これPhoenixのバージョンいくつ以降は書かないでよくなったみたいな感じなのかも。Elixir, Phoenixは初学者なのでそのあたりの知見がない。

Rails + webpacker on Dockerの環境をdocker-composeで構築する

dockerは知識として入れてはいましたが、実際にdocker上で開発をしたことはなかったので、勉強がてらやってみました。そんなにハマりどころもなく、便利なので今後はこれを基本にしていくと思います。

まず前提として、今回はdocker-composeで開発環境を作るというところまでを想定していて、本番運用はスコープ外とします。細かい手順は覚えていないので、ファイルベースで進めていきます。

# docker-compose.yml

version: '2'
services:
  rails: &app_base
    build:
      context: .
      dockerfile: "Dockerfile.dev"
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    env_file:
      - "./.env.dev"
    volumes:
      - ".:/app"
    volumes_from:
      - data
      - bundle
    ports:
      - "3000:3000"
    depends_on:
      - db
    tty: true
    stdin_open: true
  webpack:
    <<: *app_base
    command: bash -c "rm -rf /app/public/packs; /app/bin/webpack-dev-server"
    ports:
      - "3035:3035"
    depends_on:
      - rails
    tty: false
    stdin_open: false
  bundle:
    image: app_rails # 開発環境による。前述のrails imageの名前を記載
    volumes:
      - /bundle
  data:
    image: "busybox"
    volumes:
      - "/local_path:/var/lib/mysql/data" # どこでも良いのでローカル環境にsharedとしてdirectoryを用意する
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes_from:
      - data
    command: --innodb-use-native-aio=0 --ignore-db-dir=data

だいたい読んで頂けばわかるかと思いますが、rails, webpack, data, dbという4つを用意しています。volumesを".:/app" とすることで、通常の開発のように手元で好きなエディタで開発できるのが良いです。また、dbもbusyboxサンドボックス的に気軽に用意できるので、例えば案件によって違うバージョンを使いたい、違うDBを使いたいとかでも問題なく対応できます。

# Dockerfile.dev

FROM node:11.12.0 as node
FROM ruby:2.6.1

ENV LANG C.UTF-8
ENV BUILD_PACKAGES="ruby-dev bash" \
    DEV_PACKAGES="libxml2-dev libxslt-dev tzdata" \
    RUBY_PACKAGES="ruby-json nodejs"

RUN apt-get update -qq -y && \
    apt-get upgrade -y && \
    apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    wget \
    ca-certificates \
    $BUILD_PACKAGES \
    $DEV_PACKAGES \
    $RUBY_PACKAGES \
    libfontconfig1 && \
    rm -rf /var/lib/apt/lists/*

ENV ENTRYKIT_VERSION 0.4.0

RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
    && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
    && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
    && mv entrykit /bin/entrykit \
    && chmod +x /bin/entrykit \
    && entrykit --symlink


ENV YARN_VERSION 1.15.2

COPY --from=node /opt/yarn-v$YARN_VERSION /opt/yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/

RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn \
    && ln -fs /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg

ENV BUNDLE_JOBS=4 \
    APP_DIR=/app/ \
    BUNDLE_PATH=/bundle/

RUN mkdir $APP_DIR

WORKDIR $APP_DIR

COPY ./Gemfile $APP_DIR
COPY ./Gemfile.lock $APP_DIR
RUN gem install bundler
RUN bundle config build.nokogiri --use-system-libraries
RUN bundle install --no-deployment --quiet --path /bundle
RUN yarn install --check-files
COPY ./ $APP_DIR

CMD [ \
    "prehook", "rm /app/tmp/pids/server.pid" \
    "prehook", "ruby -v", "--", \
    "prehook", "bundle install -j3 --quiet --path /bundle", "--"]

Dockerfileはこんな感じです。こちらは基本的なインストール周りを行い準備しているだけという感じですね。必要に応じて変更してください。Railsの場合、ポイントとしてconfig/database.yml をどう書くかというところで迷うかと思うのですが、こちらもdockerの記述をうまく使えばとても簡単です。

# config/database.yml

default: &default
  adapter: mysql2
  host: db
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  timeout: 5000

development:
  <<: *default
  database: app_development

test:
  <<: *default
  database: app_test

production:
  <<: *default
  host: <%= ENV['HOST'] %>
  database: app_production
  username: username
  password: <%= ENV['PASSWORD'] %>

defaultのhostがdbになっているところに注目してください。これはdocker-compose.ymlで指定したdbを指しています。これだけでうまく連携してくれるので簡単ですよね。

基本的には以上となりあます。あとは適当にdocker-compose, dockerコマンドを組み合わせて開発をすすめましょう。例えば下記のような感じでコマンドを使えます。

$ docker-compose up
$ docker-compose run rails rake db:create
$ docker-compose run rails bundle exec rails db:migrate
$ docker-compose exec rails bundle exec rails c
$ docker-compose logs -f # see logs

以上です。