$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

MySQLのALTER TABLEでTEXT NOT NULLなカラムをエラー無しで追加する

課題

MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.6 データ型デフォルト値によると、

  • MySQLのTEXTカラムにはデフォルト値を設定できない
  • 厳密な SQL モードを有効にした場合、NOT NULLなカラムがINSERTに含まれていないとエラーが発生する

そのため、以下のようなスキーマが存在していて

CREATE TABLE `article` (
  `id` BIGINT UNSIGNED NOT NULL,
  `title` VARCHAR(100) NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

アプリケーションから次のようにINSERT文を発行しているとして

INSERT INTO `article`
  SET id = :id,
      title = :title
  • 先にALTER TABLE article ADD COLUMN body TEXT NOT NULLすると、INSERT文がエラーになってしまう
  • 先にアプリケーションをデプロイして、INSERTのSETにbodyを追加しても、存在しないカラムへのセットになってしまい、INSERT文がエラーになってしまう

という風に、ALTER TABLEが先でもアプリケーションデプロイが先でもエラーが発生してしまう。

解決策

先にDEFAULT句付きのVARCHARなカラムを追加し、デプロイしてINSERT句を直した後、TEXTカラムに変更すると良い。

  • ALTER TABLE article ADD body VARCHAR(100) NOT NULL DEFAULT '';
  • アプリケーションをデプロイして、SETにbodyを追加する
  • ALTER TABLE article MODIFY body TEXT NOT NULL;

こうするとエラーを起こさずにALTER TABLEが成功した。

補足: 全部VARCHARでいいんじゃないか問題

varcharとtextの違い(mysql innodb) - lxyuma BLOGのように、全部VARCHARで良いんじゃないかという話題がある。しかし、https://dev.mysql.com/doc/refman/5.6/ja/column-count-limit.html によると、

  • すべてのテーブル (ストレージエンジンには無関係) の最大行サイズは 65,535 バイトです
  • すべてのカラムの合計長は最大行サイズを超えられないので、このサイズはカラム数 (およびサイズの可能性もあり) を制約します
  • BLOB カラムと TEXT カラムは、その内容が行の残りとは別に格納されるので、行サイズに対してそれぞれ 9 から 12 (1から4+8) バイトになります。

ということで、VARCHARで長い文字列長を使うと一気に行サイズを使い果たして死にそう。

Next.jsのrouterをモックして、ページの単体テストをする

課題

例えばこのページにあるような例をテストしたいとする。

import { useRouter } from 'next/router';
import Layout from '../components/MyLayout';

const Page = () => {
  const router = useRouter();

  return (
    <Layout>
      <h1>{router.query.title}</h1>
      <p>This is the blog post content.</p>
    </Layout>
  );
};

export default Page;

この時、useRouterで取得できるrouterはNext.jsが裏でいい感じに作成しているため、enzymeとかを使ってPageのユニットテストをしようとしても、routerがundefinedでqueryが取れず死んでしまう。

解決策

テストのときだけrouterをモックしてしまうと良い。ユーティリティとして以下のようなものを用意する。

test/element.ts

import React from 'react';
import { NextRouter } from 'next/router';
import { RouterContext } from 'next/dist/next-server/lib/router-context';

// NextのrouterをmockしたReactElementを返す。
// 例えば以下のようにすればSampleComponentに渡るrouter.queryをモック出来る
// withMockedRouter(
//   {query: { hoge: "fuga", foo: "bar" }},
//   <SampleComponent />,
// );
export function withMockedRouter(router: Partial<NextRouter> = {}, children: React.ReactElement): React.ReactElement {
  const mockedRouter = {
    route: '',
    pathname: '',
    query: {},
    asPath: '',
    push: async () => true,
    replace: async () => true,
    reload: () => null,
    back: () => null,
    prefetch: async () => undefined,
    beforePopState: () => null,
    events: {
      on: () => null,
      off: () => null,
      emit: () => null,
    },
    ...router,
  };

  return <RouterContext.Provider value={mockedRouter}>{children}</RouterContext.Provider>;
}

これを使えば先程のテストは以下のように書ける(jestとenzymeを利用)。

pages/post.test.ts

import React from 'react';
import { withMockedRouter } from '../test/element';
import { mount } from 'enzyme';
import Page from './post';

describe('Post Page', () => {
  it('正しく表示できる', () => {
    const wrapper = mount(
      withMockedRouter(
        {
          query: {
            title: 'ページタイトル',
          },
        },
        <Page />
      )
    );
    expect(wrapper.find('h1').text()).toBe('ページタイトル');
  });
});

これでrouterの情報を使うページもユニットテスト出来るようになった。

補足

Next.jsがどのようにrouterを作っているかは、Next.jsのページ遷移・ページロードの仕組みをコードリーディングで追いかけた - $shibayu36->blog;で調査したので、興味があればこちらをどうぞ。

「初めてのGraphQL」読んだ

最近は開発でGraphQLを使うようになってきたので、一度基本から勉強しようと思い、読んだ。

  • 基本の部分が一通り学べて良かった。サンプルコードも多く参考になる
  • Apollo Clientの具体的な使い方がいろいろ学べたことが良かった。特にキャッシュの扱いが難しかったので、その部分を知ることができた
  • 応用の話題であるファイルアップロード周りのことが見れたのも良かった

読書ノート

* __schemaや__typeを使うとスキーマを取得できる 1015
* input型使うと、mutationだけでなく検索のクエリとかも綺麗になりそう 1453
* スキーマへの"""でのドキュメント記法 1552
* apollo clientのqueryにはポーリングがある 2646
* logout時のapolloのキャッシュ削除がわかりやすい 2917
* ファイルアップロードはform multipartでやるモデルか、先にアップロードするモデルがありそう 3224