delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

【Reactの勉強】環境構築(Project基盤の作成)

概要

前回に引き続き、環境構築を行う。
今回はProject基盤の作成を行う。(Project基盤の作成 = Create React Appコマンド(後述)の生成物をGitにコミットするまで)

適宜、以下の本を参考にさせて頂きながら進める。(以下、『りあクト!』)

oukayuka.booth.pm

勉強の題材

勉強の題材として、まずは日本大学文理学部情報科学科の教授の方がクリエイティブ・コモンズで公開してくださっている以下のチュートリアルをやってみたいと思っている。
zenn.dev

※ この記事の中では、まだチュートリアルには着手しないが、Gitのリポジトリ名が何でこれなの?って思うかもしれないので一応。

バージョン

「Reactの勉強」で使用している主なソフトウェアのバージョンは以下の通り。

  • node: 15.5.0
  • react-create-app: 4.0.1
  • react: 17.0.1
  • react-dom: 17.0.1
  • typescript: 4.1.3
  • eslint: 7.17.0
  • prettier: 2.2.1

環境構築

Create React Appを使う。

ReactでProjectを作成する際に、『りあクト!』でも推奨されていて、公式ドキュメントでも推奨されている方法として、create-react-appというコマンドを実行する方法があるので、この方法でやってみる。

ja.reactjs.org
create-react-app.dev

npxコマンドが使えることの確認

公式のインストール方法ではnpxという「 npm 5.2 から利用できるパッケージランナーツール」を使ってインストールしているので、まずはこのコマンドが使えることを確認する。

$ npx -v
7.3.0

→ 使えた。

以下の記事がとても参考になったが、npxコマンドはパッケージをインストールせずに直接実行するとのことである。
dev.classmethod.jp

以下に引用したように、公式はcreate-react-app本体をnpmでインストールすることを推奨していないので、npxを使っているのはそういう意図があるのだろう。

If you've previously installed create-react-app globally via npm install -g create-react-app, we recommend you uninstall the package using npm uninstall -g create-react-app or yarn global remove create-react-app to ensure that npx always uses the latest version.

※ 引用元
create-react-app.dev

実際に、npxでcreate-react-appを実行した後に、インストール済みのパッケージの一覧を表示してみると、create-react-app本体はインストールされていないことが分かる。

$ npm ls
xxx@ /path/to/dir
├── @testing-library/jest-dom@5.11.7
├── @testing-library/react@11.2.2
├── @testing-library/user-event@12.6.0
├── react-dom@17.0.1
├── react-scripts@4.0.1
├── react@17.0.1
└── web-vitals@0.2.4

※ ちなみに、create-react-app本体をインストールした場合は以下のように表示される。

$ npm ls
xxx@ /path/to/dir
└── create-react-app@4.0.1
実行する

公式の手順に従いcreate-react-appコマンドを実行するのだが、『りあクト!』や公式ドキュメントによると、React x TypeScriptで開発する際は、オプションに--template typescriptを付ける必要があるとのことである。

create-react-app.dev

従って、以下のコマンドを実行する。

$ npx create-react-app react_tutorial_nihon_u_1 --template typescript

react_tutorial_nihon_u_1の部分で指定した名前が、package.jsonnameの値になるので、ここは適宜読み替える。(基本的にはGitのリポジトリ名と同じ名前にするのが良いのかな?と思った。)

初回実行時は以下のように聞かれるので、「y」を入力する。

$ npx create-react-app react_tutorial_nihon_u_1 --template typescript
Need to install the following packages:
  create-react-app
Ok to proceed? (y) y

インストールが成功すると以下のようなディレクトリ・ファイル群が生成される。(node_modulesは除外している。)

$ tree react_tutorial_nihon_u_1 -I node_modules
react_tutorial_nihon_u_1
├── README.md
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   └── setupTests.ts
└── tsconfig.json

2 directories, 19 files

この際に、.gitignoreも生成してくれて、/node_modulesなどは生成された.gitignoreファイルの中に既に含まれているので、追加する必要はない。
※ ただし/.vscodeなどは含まれていなので、必要に応じて追加する。

インストールされたパッケージの一覧は以下だった。

$ npm ls
xxx@0.1.0 /path/to/react_tutorial_nihon_u_1
├── @testing-library/jest-dom@5.11.8
├── @testing-library/react@11.2.2
├── @testing-library/user-event@12.6.0
├── @types/jest@26.0.19
├── @types/node@12.19.11
├── @types/react-dom@16.9.10
├── @types/react@16.14.2
├── react-dom@17.0.1
├── react-scripts@4.0.1
├── react@17.0.1
├── typescript@4.1.3
└── web-vitals@0.2.4

yarnについて

node.jsのパッケージマネージャーにはnpm以外に、yarnというFacebookが独自で開発したものがあり、シェアもかなりあるらしい。

https://classic.yarnpkg.com/ja/docs/getting-startedclassic.yarnpkg.com

『りあクト!』もyarnを推奨していたが、今回は、以下の理由からyarnは使わずにnpmを使用することにする。

  • npmとyarn二つあると、packageをどっちでインストールするか混乱する。
  • 今回はReact入門が目的なので、本質ではないところではまりたくない。

※ また、以下のスライドも流し読みしただけだが、npmとyarnの違いとか勉強になった。
speakerdeck.com

その他のモジュールをインストールする。

その他、『りあクト!』で紹介されていたモジュールをインストールする。

  • yarnは今回は使わないことにした。
  • TypeScriptはcreate-react-app実行時に既にインストールされている。

ので、以下を追加でインストールする。

  • ts-node
  • typesync
ts-node

nodeでTypeScriptを実行するモジュールらしい。
www.npmjs.com

以下のコマンドでインストールする。
(開発環境でしか使用しないモジュールだと思うので、「-D = --save-dev」)をつけてインストールする。

$ cd react_tutorial_nihon_u_1
$ npm install -D ts-node

npm installを実行する場所は、create-react-appを実行したディレクトリの1階層下なので注意。

typesync

TypeScriptのサードパーティライブラリの型定義ファイルを自動でインストールしてくれるモジュールらしい。
www.npmjs.com
qiita.com

※ JSで書かれたサードパーティライブラリに型情報がどのように付与されているかを知らないと、このモジュールのメリットが分からないと思うが、ここでは割愛する。『りあクト!』の4章を読むと分かる。

以下のコマンドでインストールする。
(同じく開発環境でしか使用しないモジュールだと思うので、「-D = --save-dev」)をつけてインストールする。

$ npm install -D typesync

上記の2パッケージをインストールする前後でpackage.jsonの差分は以下のようになる。

$ diff package.json package.json.bk
42,45d41
<   },
<   "devDependencies": {
<     "ts-node": "^9.1.1",
<     "typesync": "^0.8.0"
Create React Appの生成物をGitにコミットする。

今回のチュートリアル用に以下のGitHubリポジトリを作成した。

github.com

ここに今回のcreate-react-appコマンドの生成物をコミットする。
以下のコマンドを順番に実行する。

$ cd react_tutorial_nihon_u_1
$ git remote add origin https://github.com/delhi09/react_tutorial_nihon_u_1.git
$ git branch -M main
$ git push -u origin main

create-react-appコマンドがgitの初回コミットログの生成まで内部で実行してくれているようなので、上記のコマンドだけでOK。

動作確認

pushしたソースコードが動く状態になっていることを確認する。
任意のディレクトリで以下のコマンドを順番に実行する。

$ git clone https://github.com/delhi09/react_tutorial_nihon_u_1
$ cd react_tutorial_nihon_u_1
$ npm install
$ npm start

成功したら、コンソール上に以下のメッセージが表示される。

Compiled successfully!

You can now view react_tutorial_nihon_u_1 in the browser.

  http://localhost:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

http://localhost:3000/ にアクセスして、以下の画面が表示されればOK。
(実際には勝手にブラウザが立ち上がる。)

f:id:kamatimaru:20210103151612p:plain

以上

【Reactの勉強】環境構築(nodeのインストールまで)

概要

Reactの勉強をしようと思ったが、まず、モダンなフロントエンド用の開発環境をちゃんと構築したことがなかったので、そこから始める。

適宜、以下の本を参考にさせて頂きながら進める。(以下、『りあクト!』)

oukayuka.booth.pm

バージョン

「Reactの勉強」で使用している主なソフトウェアのバージョンは以下の通り。

  • node: 15.5.0
  • react-create-app: 4.0.1
  • react: 17.0.1
  • react-dom: 17.0.1
  • typescript: 4.1.3
  • eslint: 7.17.0
  • prettier: 2.2.1

環境構築

Node.jsをインストールする。

方針

『りあクト!』では

  1. anyenvをインストールする。
  2. nodenvをインストールする。

という手順が紹介されていた。

github.com
github.com

今回は他のxxxenv系のツールも一緒に管理したいという要件はないので、とりあえずnodenvの方だけインストールすることにする。

nodenvのインストール

brewでインストールできる。

$ brew install nodenv

インストール完了

$ nodenv -v
nodenv 1.4.0
nodenvをshellに登録する。

以下のコマンドを実行してnodenvをshellに登録する。
zshを使用している場合なので、他のshellを使っている場合は読み替える。

$ echo 'eval "$(nodenv init -)"' >> ~/.zshrc

zshrcに以下のように記載されていればOK

$ tail -1 ~/.zshrc
eval "$(nodenv init -)"

→ 1回ターミナルを閉じる。

※ なお、これをやらないと、後続の手順でnodeをインストールした際に、nodeコマンドにPATHが通らない。

$ node
zsh: command not found: node
nodenvからnodeをインストール

ここも『りあクト!』の通りに進める。

nodenvで提供されている最新のnodeを確認する。

$ nodenv install -l | egrep "^[0-9\.]+$" | tail
14.15.2
14.15.3
15.0.0
15.0.1
15.1.0
15.2.0
15.2.1
15.3.0
15.4.0
15.5.0

※ nodenvではiojs-xxxgraal+ce-xxxという名前の系統のものも提供されているので、無印のものだけに絞るためにgrepしている。

上記の結果をみると15.5.0が最新であることが分かる。(2020年12月30日時点)

インストールする。

$ nodenv install 15.5.0
$ nodenv global 15.5.0

nodenv global 15.5.0は複数のバージョンがインストールされているときにはデフォルトでv15.5.0を使うという意味だとのことである。

nodeとnpmのPATHが通っていることを確認できればOK。

$ node -v
v15.5.0
$ npm -v
7.3.0

Pythonのエコシステムの情勢が分かる調査(Python Developers Survey)

概要

りあクト!』を読んでいたら、JSの世界には「The State of JavaScript」という、JavaScript周りの各種エコシステムの情勢を調査したものが存在することを知った。

2019.stateofjs.com

Pythonの世界には同じような調査はないのかな?と思って調べてみたところ、「Python Developers Survey」というJetBrains社によって毎年行われている調査があるのを見つけた。

www.jetbrains.com

眺めていたらとても面白かった。

感想

  • Flaskってあんまり周りでは仕事で使われてるの聞いたことがなかったけど、実はすごいシェアあるんだなと思った。
  • 逆に、最近ホットな気がするFast APIはまだ名前が出てきていなかった。調査期間の関係で、登場するなら来年以降だろうか?
  • ORMは体感に違わずDjango以外だとSQLAlchemyのシェアが圧倒的なんだなと思った。
  • RDBのシェアは、以前に人から「日本だとMySQLの方が高いけど、海外ではPostgreSQLの方が高いんだよ」って教えてもらったことがあるのだけれど、数字で見るとやはりそうなんだなと思った。
  • CI部門でJenkins/Hudsonが一位っていうのも結構驚いた。
  • etc...

VS CodeでPylanceを試してみる

概要

数ヶ月前からVS CodePythonのProjectを開くと「Pylanceという拡張機能をインストールしますか?」というポップアップが表示されるようになった。

f:id:kamatimaru:20201227081409p:plain

  • まだpreview版らしい。
  • どんな機能が追加されるのか把握できてない。
  • 今のVS CodeのPython開発環境に特に不満がない。

ので、仕事中にインストールしてかえって環境が壊れたりしたら嫌だなと思ってインストールしていなかったが、年末年始の機会に試してみた。

どんな機能が追加されるのか

以下の公式ドキュメントに主な機能の一覧が書いてある。
marketplace.visualstudio.com

字面だけ見ると、docstringとかimportとか、コード補完系の便利そうな機能が色々並んでいる気がする。

また、以下のブログに貼られている動画からいくつかの機能のデモを見ることができる。
devblogs.microsoft.com

インストール&有効化する

インストール

他のextension同様、Marketplaceからインストールできる。

f:id:kamatimaru:20201227081812p:plain

有効化

試してみたところ、インストールしただけではPylanceは有効にならないようだった。

VS Codeの設定の「Python: Language Server」という項目が「Microsoft」になっているので、これを「Pylance」に変更する必要がある。

【変更前】
f:id:kamatimaru:20201228020233p:plain

【変更後】
f:id:kamatimaru:20201228020428p:plain

→ 設定を変更すると右下に「Reload」というポップアップが表示されるので、リロードする。

検証の前提

以下の環境で検証する。

  • フォーマッターやliterが入っていると、Pylanceの機能なのか分からなくなってしまうので、blackやisortやflake8はインストールしていない環境で検証する。
  • docstringに関しては、普段は「Python Docstring Generator」というPluginを使っているが、こちらも同様の理由で無効にする。

検証する

公式ドキュメントに紹介されている機能の中から、気になるものを検証していくことにする。

1.docstring

「docstring」としか書いてなかったので、最初はdocstringの雛形を自動生成してくれる機能がPylanceには標準で搭載されたのかと思った。
しかし、以下のissueを読んだところ、そうではなく、メソッドやクラスを使用している箇所でカーソルを合わせるとdocstringが吹き出しで表示される機能のことのようである。

github.com

【検証結果】
f:id:kamatimaru:20201228022633p:plain

上記のように、確かにメソッドのdocstringが表示された。

これはこれで地味だが嬉しい機能だと思う。
ただ、「Python Docstring Generator」は依然として必要ということが分かった。

2.Auto-imports

既にimportしているモジュールに関して、同一モジュール配下の新たなクラスやメソッドを使おうとすると、自動でサジェストを表示してくれて、サジェストを承認するとさらにimport文の追加まで自動で行ってくれる。

例えば、以下のように既にos.path.existsをimport済みであるとする。

f:id:kamatimaru:20201228030637p:plain

この時に、「j」と入力すると、joinメソッドがサジェストされる。

f:id:kamatimaru:20201228030911p:plain

サジェストを承諾すると、os.path.joinがimportに自動で追加される。

f:id:kamatimaru:20201228031240p:plain

「Auto-imports 」機能に関しては以下のブログでもデモを見ることができる。
devblogs.microsoft.com

3.IntelliCode compatibility

AIがコードを自動補完してくれる機能である。

code.visualstudio.com

これがすごく便利だった。

以前から存在していたようだが、Microsoftの標準のLanguage Serverを使用していた時には有効になっていなかった気がする。
以下のような記事もあったので互換性に問題があったのかもしれない。

qiita.com

PylanceではFeaturesの一つに

IntelliCode compatibility

と謳っている通り、問題なく使うことができた。

例えば、以下のように「os.」と入力すると一般的に利用頻度が高そうな「environ」と「path」をサジェストしてくれる。

f:id:kamatimaru:20201228033250p:plain

「path」を選択すると、さらに利用頻度が高そうな「join」と「dirname」メソッドをサジェストしてくれる。

f:id:kamatimaru:20201228033535p:plain

さらにすごいのが、もし以下のようにif文の条件の中で「os.path.」と入力すると「exists」と「isfile」をサジェストしてくれる。

f:id:kamatimaru:20201228033747p:plain

このように、コードの文脈によって適切なコードをサジェストしてくれるのは本当にすごいと思った。

なお、IntelliCodeが有効になっていない場合、サジェストは表示されるが、以下のようにモジュールがアルファベット順に表示されるだけになってしまうので、便利さが大分違う。

f:id:kamatimaru:20201228034537p:plain

その他のTips

メソッドの丸括弧部分の補完を有効にする。

以下のように、デフォルトではサジェストに基づいてメソッドを選択した際に、丸括弧が補完されないのが少し不便に感じた。

f:id:kamatimaru:20201228040455p:plain

これはpython.analysis.completeFunctionParensという設定値がデフォルトでは無効になっていることが原因である。
有効にすれば、丸括弧部分まで補完されるようになるので、私は有効にした。
f:id:kamatimaru:20201228040848p:plain

なお、以下の公式ドキュメントに編集可能な設定値については一覧が記載されている。
marketplace.visualstudio.com

感想

まだ数ある機能のいくつかを試しただけだが、良さげだったこと及び、公式のブログにも以下のように書かれており、MSもじょじょにPylanceに寄せていく方針のようなので、今後は仕事でも使ってみようかなと思った。

Our long-term plan is to transition our Microsoft Python Language Server users over to Pylance and eventually deprecate and remove the old language server as a supported option.

【引用元】
devblogs.microsoft.com

【自分用メモ】テーブル設計のカタログサイト

色んな業界のシステムのテーブル設計がまとまっているカタログサイトがある。

www.databaseanswers.org

私が最初にこのサイトを知ったのは、以下のt_wadaさんのTweetだった。


実際の案件でも参考にできるだろうし(もちろんこのまま使える訳ではないが)、何よりも見てるとすごく楽しいので、時々、無性に読みたくなるのだけれど、その度にURLを探してしまうので、メモしておく。

Djangoのエラーハンドリングに関して覚えておくべきこと

はじめに

この記事はDjango Advent Calendar 2020 22日目の記事です。

22日目の記事を担当しますdelhi09と申します。
本記事では「Djangoのエラーハンドリングに関して覚えておくべきこと」というタイトルで書かせて頂きます。

概要

Djangoはエラーハンドリング周りに関しても非常に高機能です。

複雑な要件がなければ、エラーハンドリングはほぼDjangoの標準機能で事足りるので、実はロジックの中でエラーハンドリングを意識したコードを書く必要はほとんどありません。

ただ、Djangoが暗黙にやってくれていることを知らないと、不要なエラーハンドリング処理を書いたり、ログを二重に出してしまったりすることがあります。

加えて、エラーハンドリング周りの部分はオンライン上でも情報が少なめだったり、公式ドキュメントに書いてないこともあったりするなと感じました。

そこで、本記事ではDjangoが暗黙にやってくれているエラー処理の中から、Djangoを使う側でも覚えておいた方がよいと思ったことを書きました。

先にまとめ

以下が本記事のまとめです。

  • エラーハンドリングに関してDjangoを使う側で意識するべきことは以下の3つである。
    • 500.htmlを配置する。
    • LOGGINGを設定する。
    • View層以下での例外は余計なハンドリングをせずにエスカレーションする。

エラーハンドリングに関してDjangoを使う側で意識するべきこと

前提

アプリケーションでサーバーサイド起因のエラーが発生した時に、最低限行わなければならない処理は以下の3つだと私は考えています。

  1. HTTPステータスコード500系を返す。
  2. ユーザーに適切なエラー画面を表示する。
  3. 発生したエラー内容をログファイルに適切なログレベルで出力する。

この3つに関しては、Djangoではほぼコーディングなしで対応することができます。

以下、順番に説明していきます。

1.HTTPステータスコード500系を返す。

DjangoではViewで例外がraiseされた場合には、handler500というものが実行されて、HTTPステータスコード500を返してくれます。

docs.djangoproject.com

従って、View層以下で例外をraiseすれば、Djangoが内部でHTTPステータスコード500でレスポンスを返してくれるので、HTTPステータスコードに関してはDjangoを使う側では意識する必要はありません。

※ 尚、handler500で実行される処理はオーバーライドすることも可能です。その場合は以下の公式ドキュメントや去年のAdvent Calendarの記事を読んで頂くと良いと思います。

docs.djangoproject.com
qiita.com

2.ユーザーに適切なエラー画面を表示する。

1で説明したhandler500は、デフォルトの挙動では500.htmlという名前のテンプレートファイルが存在する場合には、500.htmlを汎用エラー画面として返すという仕様になっています。

従って、500.htmlという名前のTemplateファイルに汎用エラー画面用のHTMLを書いて配置するだけでOKです。

加えて、覚えておいた方がよいこととして、DEBUG = Trueの場合は、500.htmlが存在したとしても、画面には汎用エラー画面ではなくデバッグ情報を表示するというのがDjangoの仕様です。

従って、DEBUG = Trueの開発環境では、汎用エラー画面が表示されることは動作確認できません。

このことを知らないと、開発時に「あれ?500.htmlを配置したのに使われないぞ?何か間違ってるのかな?」と焦ります。

docs.djangoproject.com

3.発生したエラー内容をログファイルに適切なログレベルで出力する。

Djangoは処理の過程で例外がraiseされた場合には、実はレスポンスを返すときに内部でスタックトレースのロギングを行ってくれています。
ログレベルに関しても

  • 400系に相当する例外 → Warnigレベル
  • 500系に相当する例外 → Errorレベル

HTTPステータスコードに応じてログレベルを分けてくれています。

上記の仕様に関しては、私も最近まで知らなかったのですが、ある時、ロジックの中で例外を捕捉してloggingする処理を書いていたら、同様の内容のエラーログが二重に出力されていることに気づいて、それをきっかけに調べて知りました。

本件に関しては、公式ドキュメントに該当する記述を見つけられなかったのですが、以下のソースコードresponse_for_exceptionというメソッドを読んで、その中でさらに呼ばれているdjango.utils.log.log_responseというメソッドを辿ると分かるかと思います。

github.com

github.com

(公式ドキュメントに関しては、私が見つけられていないだけかもしれないので、ここに記述があるよってご存知の方がいたら教えて頂けるとありがたいです。)

従って、View層以下で例外をraiseすれば、Djangoが内部で適切なログレベルでロギングをしてくれるので、ロギングに関してもDjangoを使う側で意識する必要はありません。

但し、settings.pyLOGGINGの設定をしておかないと、ログがファイルに出力されないので、そこはDjangoを使う側がしっかり設定を書く必要があります。

DjangoのLOGGINGの設定に関しては、私も以前に記事を書いたので、よろしければこちらも参考にしてください。

kamatimaru.hatenablog.com

(1〜3を受けて)View層以下では余計な例外ハンドリングはせずにエスカレーションするのが良い。

1〜3で見てきたように、Viewで例外をraiseすれば、あとはDjangoが内部でエラーレスポンスの作成やロギングをやってくれるので、View層以下では例外をエスカレーションするだけ(=何もハンドリング処理を書かない)にするのが良いのではないかと考えられます。

例えば、Djangoが内部で例外のスタックトレースをロギングしてくれているということを知らずに、以下のようなコードを書いたとします。

class SampleView(View):
    def get(self, request, *args, **kwargs):
        service = SampleService()
        try:
            service.do_business_logic()
        except BusinessLogicException as e:
            logger.exception(e)
            raise
        return render(request, "index.html")

このコードは結果的に

  • 同じ内容のエラーログを二重に出力してしまっている。
  • 本来1行で済むはずのコードを5行かけて書いてしまっている。

ことになります。

この例であれば

class SampleView(View):
    def get(self, request, *args, **kwargs):
        service = SampleService()
        service.do_business_logic()
        return render(request, "index.html")

try...exceptで囲わずに書き、例外のハンドリングはDjangoに任せるのが良いと思います。

最後に

本記事に関しては、実は、私自身が、Djangoが内部で例外のスタックトレースをロギングしてくれているということを数日前まで知らなくて、「例外ハンドリング用のMiddlewareを自作する」という割とボリューミーな記事をAdvent Calendar用に書いていたのですが、検証している時にそのことに気づいて、「あれ?もしかしてMiddleware自作する必要ないかも?」となって記事を書き直したという経緯があったりします。

読んで頂きありがとうございました!

MySQLでCASE式を使うUPDATE文を使うと複合ユニーク制約を回避できるのか検証

概要

SQLでは、以下のようにCASE式を使うと1回のSQL実行で複数のUPDATEを実行することができる。

UPDATE article_image_url SET display_order = (
    CASE id
        WHEN 1 THEN 2
        WHEN 2 THEN 3
        WHEN 3 THEN 1
    END
) WHERE id in (1, 2, 3);

MySQLでこれを使うと複合ユニーク制約を回避できるのかを検証した。

MySQLのバージョン

5.7

検証したかった理由

Django ORMのbulk_updateメソッドは内部でCASE式を使うUPDATE文を生成するので、挙動を知りたかった。
(ググったら、挙動はSQL製品によるがMySQLはエラーになるという記事がいくつか見つかったが、自分でも検証してみたかった。)

bulk_updateが内部で発行するSQLに関しては、以下の記事を参照させて頂いた。
tokibito.hatenablog.com

結論

回避できない。

検証過程

無機質に検証するだけでもよいが、せっかくなので、実際にありそうなケースとして、ブログに表示する画像のURLを保存するテーブルを例とする。

「画像に紐づく記事のID」と「表示順」で複合ユニーク制約が存在するとする。

CREATE TABLE文は以下

CREATE TABLE article_image_url (
    id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
    article_id INTEGER NOT NULL,
    image_url VARCHAR(255) NOT NULL,
    display_order INTEGER NOT NULL,
    UNIQUE unique_article_id_display_order (article_id, display_order)
) ENGINE = InnoDB DEFAULT CHARACTER 
SET = utf8 DEFAULT COLLATE = utf8_general_ci;

初期データを投入する。

INSERT INTO article_image_url VALUES (null, 1, "http://example.com/images/1", 1);
INSERT INTO article_image_url VALUES (null, 1, "http://example.com/images/2", 2);
INSERT INTO article_image_url VALUES (null, 1, "http://example.com/images/3", 3);

この時点でのテーブルの中身は以下

mysql> select * from article_image_url;
+----+------------+-----------------------------+---------------+
| id | article_id | image_url                   | display_order |
+----+------------+-----------------------------+---------------+
|  1 |          1 | http://example.com/images/1 |             1 |
|  2 |          1 | http://example.com/images/2 |             2 |
|  3 |          1 | http://example.com/images/3 |             3 |
+----+------------+-----------------------------+---------------+

以下のCASE式を使うUPDATE文を実行する

UPDATE article_image_url SET display_order = (
    CASE id
        WHEN 1 THEN 2
        WHEN 2 THEN 3
        WHEN 3 THEN 1
    END
) WHERE id in (1, 2, 3);

結果

mysql> UPDATE article_image_url SET display_order = (
    ->     CASE id
    ->         WHEN 1 THEN 2
    ->         WHEN 2 THEN 3
    ->         WHEN 3 THEN 1
    ->     END
    -> ) WHERE id in (1, 2, 3);
ERROR 1062 (23000): Duplicate entry '1-2' for key 'unique_article_id_display_order'
mysql>

残念ながらDuplicate entryが発生。

TODO

他のSQL製品でも試してみたい。