flutter doctorを使って開発環境を構築する

背景

前回flutter doctorの実行で躓いてしまいました。今回はflutter doctorで状況を確認し、開発環境を構築するところまで行いたいと思います。

flutter doctorの実行

まずflutter doctorを実行して状況を確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ ./bin/flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6 20G165 darwin-x64, locale ja-JP)
[✗] Android toolchain - develop for Android devices
✗ Unable to locate Android SDK.
Install Android Studio from: https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK components.
(or visit https://flutter.dev/docs/get-started/install/macos#android-setup for
detailed instructions).
If the Android SDK has been installed to a custom location, please use
`flutter config --android-sdk` to update to that location.

[✗] Xcode - develop for iOS and macOS
✗ Xcode installation is incomplete; a full installation is necessary for iOS
development.
Download at: https://developer.apple.com/xcode/download/
Or install Xcode via the App Store.
Once installed, run:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
✗ CocoaPods not installed.
CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that
responds to your plugin usage on the Dart side.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/platform-plugins
To install see https://guides.cocoapods.org/using/getting-started.html#installation
for instructions.
[✓] Chrome - develop for the web
[!] Android Studio (not installed)
[✓] VS Code (version 1.65.0)
[!] Proxy Configuration
! NO_PROXY does not contain 127.0.0.1
! NO_PROXY does not contain ::1
[✓] Connected device (1 available)
[✓] HTTP Host Availability

! Doctor found issues in 4 categories.

バツと!になっている箇所について対応が必要そうです。

Xcodeのインストール

まずは簡単そうなXcodeのインストールから行います。

App Storeからダウンロード

まずApp StoreからXcodeをインストールします。

ライセンスに同意する

ライセンスに同意する必要があるので、以下のコマンドを実行します

1
$ sudo xcodebuild -license

するとライセンスに関する文面が表示されます。スペースキーでページ送りをして、最後に以下の表示になったらagreeとタイプしてエンターキーを押します。

1
2
3
By typing 'agree' you are agreeing to the terms of the software license agreements. Type 'print' to print them or anything else to cancel, [agree, print, cancel] agree

You can view the license agreements in Xcode's About Box, or at /Applications/Xcode.app/Contents/Resources/English.lproj/License.rtf

状況確認

Xcodeがインストールできたので、再度flutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
$ flutter doctor
...(省略)...
[!] Xcode - develop for iOS and macOS (Xcode 13.2.1)
✗ CocoaPods not installed.
CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that
responds to your plugin usage on the Dart side.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/platform-plugins
To install see https://guides.cocoapods.org/using/getting-started.html#installation
for instructions.
...(省略)...

ということで、CocoaPodsのインストールが必要でした。

CocoaPodsのインストール

先程のflutter doctorのメッセージにあるインストールのドキュメントを見てみます。

冒頭にYou can use a Ruby Version manager, however we recommend that you use the standard Ruby available on macOS unless you know what you're doing.とあるので、グローバルにインストールされているRubyを利用しようと思います。

1
2
3
4
5
6
7
8
$ which ruby
/Users/user/.anyenv/envs/rbenv/shims/ruby
$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]
$ rbenv versions
* system
2.4.10
3.1.0

グローバルにインストールされているRubyが2.6.3ということですね。こちらにインストールします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
$ sudo /usr/bin/gem install cocoapods
Password:
Fetching concurrent-ruby-1.1.9.gem
Fetching tzinfo-2.0.4.gem
Fetching zeitwerk-2.5.4.gem
Fetching activesupport-6.1.5.gem
Fetching fuzzy_match-2.0.4.gem
Fetching i18n-1.10.0.gem
Fetching nap-1.1.0.gem
Fetching httpclient-2.8.3.gem
Fetching algoliasearch-1.27.5.gem
Fetching ffi-1.15.5.gem
Fetching ethon-0.15.0.gem
Fetching typhoeus-1.4.0.gem
Fetching netrc-0.11.0.gem
Fetching public_suffix-4.0.6.gem
Fetching addressable-2.8.0.gem
Fetching cocoapods-core-1.11.3.gem
Fetching claide-1.1.0.gem
Fetching cocoapods-deintegrate-1.0.5.gem
Fetching cocoapods-downloader-1.5.1.gem
Fetching cocoapods-plugins-1.0.0.gem
Fetching cocoapods-search-1.0.1.gem
Fetching cocoapods-trunk-1.6.0.gem
Fetching cocoapods-try-1.2.0.gem
Fetching molinillo-0.8.0.gem
Fetching atomos-0.1.3.gem
Fetching colored2-3.1.2.gem
Fetching nanaimo-0.3.0.gem
Fetching rexml-3.2.5.gem
Fetching xcodeproj-1.21.0.gem
Fetching escape-0.0.4.gem
Fetching fourflusher-2.3.1.gem
Fetching gh_inspector-1.1.3.gem
Fetching ruby-macho-2.5.1.gem
Fetching cocoapods-1.11.3.gem
Successfully installed concurrent-ruby-1.1.9
Successfully installed i18n-1.10.0
Successfully installed tzinfo-2.0.4
Successfully installed zeitwerk-2.5.4
Successfully installed activesupport-6.1.5
Successfully installed nap-1.1.0
Successfully installed fuzzy_match-2.0.4
Successfully installed httpclient-2.8.3
A new major version is available for Algolia! Please now use the https://rubygems.org/gems/algolia gem to get the latest features.
Successfully installed algoliasearch-1.27.5
Building native extensions. This could take a while...
ERROR: Error installing cocoapods:
ERROR: Failed to build gem native extension.

current directory: /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/ext/ffi_c
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby -I /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0 -r ./siteconf20220317-44199-1ctmdly.rb extconf.rb
checking for ffi.h... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers. Check the mkmf.log file for more details. You may
need configuration options.

Provided configuration options:
--with-opt-dir
--without-opt-dir
--with-opt-include
--without-opt-include=${opt-dir}/include
--with-opt-lib
--without-opt-lib=${opt-dir}/lib
--with-make-prog
--without-make-prog
--srcdir=.
--curdir
--ruby=/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/$(RUBY_BASE_NAME)
--with-ffi_c-dir
--without-ffi_c-dir
--with-ffi_c-include
--without-ffi_c-include=${ffi_c-dir}/include
--with-ffi_c-lib
--without-ffi_c-lib=${ffi_c-dir}/lib
--enable-system-libffi
--disable-system-libffi
--with-libffi-config
--without-libffi-config
--with-pkg-config
--without-pkg-config
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:467:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:585:in `block in try_compile'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:534:in `with_werror'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:585:in `try_compile'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:1109:in `block in have_header'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:959:in `block in checking_for'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:361:in `block (2 levels) in postpone'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:331:in `open'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:361:in `block in postpone'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:331:in `open'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:357:in `postpone'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:958:in `checking_for'
from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:1108:in `have_header'
from extconf.rb:10:in `system_libffi_usable?'
from extconf.rb:42:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

/Library/Ruby/Gems/2.6.0/extensions/universal-darwin-20/2.6.0/ffi-1.15.5/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5 for inspection.
Results logged to /Library/Ruby/Gems/2.6.0/extensions/universal-darwin-20/2.6.0/ffi-1.15.5/gem_make.out

エラーが発生してしまいました。メッセージを確認すると、ffi.hが見つからないということのようです。

libffiがインストールされていないからかもしれないので、インストールしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ brew install libffi
Running `brew update --preinstall`...
...(省略)...
==> Downloading https://ghcr.io/v2/homebrew/core/libffi/manifests/3.4.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libffi/blobs/sha256:a461f6ad21a23a725691385dbbec3eff958cf61d5282e84dc3f0483e307e1875
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:a461f6ad21a23a725691385dbbec3eff958cf61d5282e84dc3f0483e307e1875?se=202
######################################################################## 100.0%
==> Pouring libffi--3.4.2.big_sur.bottle.tar.gz
==> Caveats
libffi is keg-only, which means it was not symlinked into /usr/local,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

For compilers to find libffi you may need to set:
export LDFLAGS="-L/usr/local/opt/libffi/lib"
export CPPFLAGS="-I/usr/local/opt/libffi/include"

==> Summary
🍺 /usr/local/Cellar/libffi/3.4.2: 17 files, 599.8KB
==> `brew cleanup` has not been run in the last 30 days, running now...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
Removing: /Users/user/Library/Caches/Homebrew/icu4c--69.1... (27.2MB)
Removing: /Users/user/Library/Caches/Homebrew/nkf--2.1.5... (167KB)
Removing: /Users/user/Library/Caches/Homebrew/nkf_bottle_manifest--2.1.5... (7.9KB)
Removing: /Users/user/Library/Logs/Homebrew/telnet... (64B)

インストールできました。しかし、再度cocoapodsをインストールしようとしても同じエラーで止まります。

エラーログを見てみると、ruby/config.hが見つからないということで、シンボリックリンクを作成してみました。

1
2
$ cd /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby
$ sudo ln -sf ../../../../Headers/ruby/config.h

すると、内容は変わったのですが、いまだにエラーは出ます。

1
2
3
4
5
6
7
8
current directory: /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/ext/ffi_c
make "DESTDIR="
make: *** No rule to make target `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/universal-darwin20/ruby/config.h', needed by `AbstractMemory.o'. Stop.

make failed, exit code 2

Gem files will remain installed in /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5 for inspection.
Results logged to /Library/Ruby/Gems/2.6.0/extensions/universal-darwin-20/2.6.0/ffi-1.15.5/gem_make.out

よくみると、universal-darwin20と書かれています。Rubyのバージョンにもそのように書いてありました。しかし、実際に存在するディレクトリはuniversal-darwin21です。この記事を参考に、シンボリックリンクを作成します。

1
2
$ cd /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/
$ sudo ln -s universal-darwin21 universal-darwin20

再度試します

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
$ sudo gem install cocoapods
Password:
Building native extensions. This could take a while...
Successfully installed ffi-1.15.5
Successfully installed ethon-0.15.0
Successfully installed typhoeus-1.4.0
Successfully installed netrc-0.11.0
Successfully installed public_suffix-4.0.6
Successfully installed addressable-2.8.0
Successfully installed cocoapods-core-1.11.3
Successfully installed claide-1.1.0
Successfully installed cocoapods-deintegrate-1.0.5
Successfully installed cocoapods-downloader-1.5.1
Successfully installed cocoapods-plugins-1.0.0
Successfully installed cocoapods-search-1.0.1
Successfully installed cocoapods-trunk-1.6.0
Successfully installed cocoapods-try-1.2.0
Successfully installed molinillo-0.8.0
Successfully installed atomos-0.1.3
Successfully installed colored2-3.1.2
Successfully installed nanaimo-0.3.0
Successfully installed rexml-3.2.5
Successfully installed xcodeproj-1.21.0
Successfully installed escape-0.0.4
Successfully installed fourflusher-2.3.1
Successfully installed gh_inspector-1.1.3
Successfully installed ruby-macho-2.5.1
Successfully installed cocoapods-1.11.3
Parsing documentation for ffi-1.15.5
Installing ri documentation for ffi-1.15.5
Parsing documentation for ethon-0.15.0
Installing ri documentation for ethon-0.15.0
Parsing documentation for typhoeus-1.4.0
Installing ri documentation for typhoeus-1.4.0
Parsing documentation for netrc-0.11.0
Installing ri documentation for netrc-0.11.0
Parsing documentation for public_suffix-4.0.6
Installing ri documentation for public_suffix-4.0.6
Parsing documentation for addressable-2.8.0
Installing ri documentation for addressable-2.8.0
Parsing documentation for cocoapods-core-1.11.3
Installing ri documentation for cocoapods-core-1.11.3
Parsing documentation for claide-1.1.0
Installing ri documentation for claide-1.1.0
Parsing documentation for cocoapods-deintegrate-1.0.5
Installing ri documentation for cocoapods-deintegrate-1.0.5
Parsing documentation for cocoapods-downloader-1.5.1
Installing ri documentation for cocoapods-downloader-1.5.1
Parsing documentation for cocoapods-plugins-1.0.0
Installing ri documentation for cocoapods-plugins-1.0.0
Parsing documentation for cocoapods-search-1.0.1
Installing ri documentation for cocoapods-search-1.0.1
Parsing documentation for cocoapods-trunk-1.6.0
Installing ri documentation for cocoapods-trunk-1.6.0
Parsing documentation for cocoapods-try-1.2.0
Installing ri documentation for cocoapods-try-1.2.0
Parsing documentation for molinillo-0.8.0
Installing ri documentation for molinillo-0.8.0
Parsing documentation for atomos-0.1.3
Installing ri documentation for atomos-0.1.3
Parsing documentation for colored2-3.1.2
Installing ri documentation for colored2-3.1.2
Parsing documentation for nanaimo-0.3.0
Installing ri documentation for nanaimo-0.3.0
Parsing documentation for rexml-3.2.5
Installing ri documentation for rexml-3.2.5
Parsing documentation for xcodeproj-1.21.0
Installing ri documentation for xcodeproj-1.21.0
Parsing documentation for escape-0.0.4
Installing ri documentation for escape-0.0.4
Parsing documentation for fourflusher-2.3.1
Installing ri documentation for fourflusher-2.3.1
Parsing documentation for gh_inspector-1.1.3
Installing ri documentation for gh_inspector-1.1.3
Parsing documentation for ruby-macho-2.5.1
Installing ri documentation for ruby-macho-2.5.1
Parsing documentation for cocoapods-1.11.3
Installing ri documentation for cocoapods-1.11.3
Done installing documentation for ffi, ethon, typhoeus, netrc, public_suffix, addressable, cocoapods-core, claide, cocoapods-deintegrate, cocoapods-downloader, cocoapods-plugins, cocoapods-search, cocoapods-trunk, cocoapods-try, molinillo, atomos, colored2, nanaimo, rexml, xcodeproj, escape, fourflusher, gh_inspector, ruby-macho, cocoapods after 26 seconds
25 gems installed

無事インストールできました。

flutter doctorで確認します。

1
2
3
4
$ ./bin/flutter doctor
...(省略)...
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
...(省略)...

Xcodeはクリアしました。

Android toolchainのインストール

次にAndroid toolchainをインストールします。flutter doctorの出力にあるサイトからダウンロードしてインストールします。

ダウンロードしたdmgをダブルクリックし、アプリケーションアイコンをアプリケーションディレクトリにドラッグ&ドロップします。

インストールが終わったら、On first launch it will assist you in installing the Android SDK components.と書かれているので、Android Studioを起動します。

利用規約に同意してその他のコンポーネントをインストールします。

ここまで終わったらflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
$ ./bin/flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
✗ cmdline-tools component is missing
Run `path/to/sdkmanager --install "cmdline-tools;latest"`
See https://developer.android.com/studio/command-line for more details.
✗ Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.
...(省略)...

まだ足りないようでした。メッセージに従ってコマンドを実行していきます。

Android SDK Command-line Toolsのインストール

sdkmanagerのパスがわからなかったので、GUIで設定しました。

Android Studioを起動し、メニューのAndroid Studio->Preferenceを選択し、左メニューのAppearance & Behavior->System Setting->Android SDKを選択します。

右のタブでSDK Toolsを選択し、下に表示される一覧の中にAndroid SDK Command-line Tools (latest)にチェックを入れ、右下のApplyボタンをクリックします。

するとダウンロードとインストールが始まります。終わったらOKをクリックします。

ライセンスの同意

次にライセンスに同意します。メッセージに記載のあるコマンドを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./bin/flutter doctor --android-licenses 
Warning: Failed to download any source lists! Fetch remote repository...
Warning: Still waiting for package manifests to be fetched remotely.
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: Still waiting for package manifests to be fetched remotely.y...
Warning: IO exception while downloading manifesttch remote repository...
Warning: IO exception while downloading manifest
Warning: Still waiting for package manifests to be fetched remotely.y...
All SDK package licenses accepted.======] 100% Computing updates...

だいぶ時間がかかりますし、うまくいってないようなメッセージが出ますが、結果的に全てのライセンスに同意できています。

それではflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ./bin/flutter doctor                   
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.65.2)
[!] Proxy Configuration
! NO_PROXY does not contain 127.0.0.1
! NO_PROXY does not contain ::1
[✓] Connected device (1 available)
[!] HTTP Host Availability
✗ HTTP host https://maven.google.com/ is not reachable. Reason: An error occurred while checking the HTTP host: Operation timed out
✗ HTTP host https://pub.dev/ is not reachable. Reason: An error occurred while checking the HTTP host: Operation timed out
✗ HTTP host https://cloud.google.com/ is not reachable. Reason: An error occurred while checking the HTTP host: Operation timed out

! Doctor found issues in 2 categories.

もう少しですね…

NO_PROXYの設定

NO_PROXYの設定はコマンドを実行するシェルで環境変数を設定するのだろうと思うので設定してみます。

~/.zshrcNO_PROXYを設定します

1
NO_PROXY=localhost,127.0.0.1,::1

シェルを再起動します

1
$ exec $SHELL -l

環境変数を確認します

1
2
$ env | grep -i no_proxy
NO_PROXY=localhost,127.0.0.1,::1

設定できました。

HTTP Host Availabilityの対応

環境変数のPROXYの設定が悪さをしていて、外部へのアクセスができてなかったので、unsetコマンドで削除しました。

最後にflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
$ ./bin/flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.65.2)
[✓] Connected device (1 available)
[✓] HTTP Host Availability

なぜかまたライセンスで!になっているので、コマンドを実行します。(やはり先程はエラーになっていたようです)

1
2
3
4
5
6
$ ./bin/flutter doctor --android-licenses
5 of 7 SDK package licenses not accepted. 100% Computing updates...
Review licenses that have not been accepted (y/N)? y

1/5: License android-googletv-license:
...(省略)...

ライセンスが表示されるのでyを押して同意します。

今度こそ最後にflutter doctorで確認します。

1
2
3
4
5
6
7
8
9
10
11
12
$ ./bin/flutter doctor                   
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6.4 20G417 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.65.2)
[✓] Connected device (1 available)
[✓] HTTP Host Availability

• No issues found!

すべてクリアできました!

まとめ

flutter doctorですべてOKになるところまで実施しました。

大きく時間がかかったのはcocoapodsのインストールだけでした。こちらもMacのバージョンを上げたりしていなければ(darwin20とdarwin21の違い)、スムーズに進んだと思います。

とりあえず環境構築はできました。次からはモバイルアプリの開発を進めていきたいと思います。

参考図書

「dartが悪質なソフトウェアかどうかをAppleでは確認できないため、このソフトウェアは開けません」の対処法

背景

会社の業務でスマートフォンアプリを開発することになったので、今のスマホアプリ開発ツールを調査してみました。すると、flutterというツールが見つかりました。

flutterはgoogleが開発しているクロスプラットフォームなスマートフォンアプリ開発ツールで、現在も活発に開発が行われているようです。同じような開発ツールでReactNativeがあるようですが、”flutter ReactNative 比較”と検索してみると、

Flutterはよりシンプルで、OS(オペレーティングシステム)の更新によって行われた変更に対して耐性があります。 React Nativeはデバイスのネイティブ要素に依存しており、システムが更新された場合に iOSアプリケーションと Androidアプリケーションに個別に追加の適応作業が必要があります。

とのことなので、flutterの利用が良さそうです。ということで早速試してみました。

flutterSDKのダウンロード

まず、flutterSDKをダウンロードします。

https://docs.flutter.dev/development/tools/sdk/releases?tab=macos

最新版が今日(2022/03/03)リリースされていました。

ダウンロードしたファイルを適当な場所に展開します。

flutter doctorの実行

Flutter実践入門のチュートリアルをみていると、flutter doctorというコマンドで状況が確認できるということです。早速実行してみます。

1
$ ./bin/flutter doctor

すると、ポップアップが表示されdartが悪質なソフトウェアかどうかをAppleでは確認できないため、このソフトウェアは開けません。と表示され、OKボタンをクリックするとコンソールに

1
/Users/user/flutter/bin/internal/shared.sh: line 223: 40699 Killed: 9               "$DART" --disable-dart-dev --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"

と表示され終了してしまいます。

「ソフトウェアは開けません」が表示された時の対応方法

Appleのサポートページに回答が書いてありました。

https://support.apple.com/ja-jp/guide/mac-help/mchleab3a043/mac

ポップアップが表示された時に、「Finderで開く」を選択して、Finderで表示し、開くことができないアプリケーションのアイコンを右クリック->開くを選択して一回開けば、次回から警告は表示されなくなります。

まとめ

スマートフォンアプリの開発は10年ぶりくらいで、10年前はUnityを使っていました。flutterはとても便利そうですが、新しい分野を学ぶときは勉強することが多いので、成長を感じつつも学ぶ量に圧倒されています。

flutterはこれからどんどん試していくのでまた気づきがあれば掲載します。

参考図書

MySQLdb._exceptions.DataError (1406, Data too long for column 'name')の原因調査

背景

前回謎のエラーが発生し、マイグレーションはできているものの、なぜなのかがわからないままでした。

テストデータをテーブルに投入する際に、既存データの破棄のためロールバックを行ったのですが、また同じエラーが発生したので、詳しく調べてみました。

エラー内容の確認

エラーが起こったのはauthアプリケーションのロールバックでした

1
2
3
4
5
6
7
8
9
10
11
12
$ docker-compose run --rm web python manage.py migrate auth zero
...
(省略)
...
Unapplying auth.0002_alter_permission_name_max_length...
DEBUG ALTER TABLE `auth_permission` MODIFY `name` varchar(50) NOT NULL; (params [])
DEBUG (0.021) None; args=[]; alias=default
Traceback (most recent call last):
...
(省略)
...
django.db.utils.DataError: (1406, "Data too long for column 'name' at row 57")

今回はDEBUGメッセージを表示しているので実行されるSQLがわかります。auth_permissionテーブルの変更でエラーが発生していることがわかります。

レコードの確認

実際にauth_permissionテーブルを確認してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ docker-compose run --rm web python manage.py dbshell
Creating example_web_run ... done
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 92
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [example]> select * from auth_permission;
+----+--------------------------------------------------------+-----------------+-----------------------------------------------+
| id | name | content_type_id | codename |
+----+--------------------------------------------------------+-----------------+-----------------------------------------------+
| 1 | Can add log entry | 1 | add_logentry |
| 2 | Can change log entry | 1 | change_logentry |
| 3 | Can delete log entry | 1 | delete_logentry |
...
(省略)
...
| 57 | 51文字の文字列 | 15 | add_長めのモデル名 |
...
(省略)

エラーメッセージは

1
django.db.utils.DataError: (1406, "Data too long for column 'name' at row 57")

なので、57行目を確認したところ、nameカラムの値が51文字ありました。

マイグレーションの内容確認

マイグレーションの内容を確認します。authアプリケーションのマイグレーションファイルはgithubで確認できました。

https://github.com/django/django/tree/main/django/contrib/auth/migrations

この中の

https://github.com/django/django/blob/main/django/contrib/auth/migrations/0002_alter_permission_name_max_length.py

でエラーが発生しています。ファイル名からしてすでに怪しいですね。

内容は以下になっています

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("auth", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="permission",
name="name",
field=models.CharField(max_length=255, verbose_name="name"),
),
]

最大長を255に変更しているのがわかります。だとすると元々の長さはどれくらいだったのでしょうか。それを知るためにはその前のマイグレーションフィアルである、0001_initial.pyを確認します。

確認すると以下のようになっていました。

1
("name", models.CharField(max_length=50, verbose_name="name")),

最大50文字でした…最大長255から最大長50に戻すには保存されているデータが大きすぎるということで、エラーになっているということですね。

ロールバックの順序で対応できるか

マイグレーションでデータをINSERTしているのであれば、ロールバックでデータをDELETEすれば、ALTER文でエラーが発生しなくなるのでは?と思い、INSERTしているマイグレーションのアプリケーションを探してみると、sessionsのマイグレーションでINSERTしていることがわかった。

しかし、sessionsのマイグレーションをロールバックしてもデータが削除されなかった…

まとめ

長い名前のモデルを作成すると、auth_permissionテーブルのnameカラムに長さが50を超えるデータが入ることがある。その状態でロールバックすると例外が発生してしまいます。

現状思いつく対応としては、auth_permissionテーブルのデータを全て削除してからロールバックを行うという方法です。スマートさはないですが、エラーの回避はできます。

しかし、テーブル作成した後にmax_length=255にするくらいなら、最初からmax_length=255で作成しておいてくれればいいのになと思いました。

参考図書

DjangoでRailsのようなログ出力設定をする

背景

Djangoでviewを実装し、ぽちぽち動かしているときに、どんなクエリが実行されているかを確認できるとうれしいと思いました。
Railsだとデフォルトでそのようになっているのですが、Djangoは設定が必要だったので設定してみました。

設定

設定内容は以下になります

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
}
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
}
}

設定内容を詳しくみていきます

設定の詳細

まず公式ドキュメントを確認します
ロギング | Django ドキュメント | Django
公式ドキュメントの内容に沿って確認していきます

LOGGING

公式ドキュメントには

n order to configure logging, you use LOGGING to define a dictionary of logging settings. These settings describe the loggers, handlers, filters and formatters that you want in your logging setup, and the log levels and other properties that you want those components to have.

と記載されており、LOGGINGというdictionaryの定数を定義することでロギングの設定ができることがわかります。

LOGGING内の要素

version

versionについて、公式ドキュメントには記載がありませんが、例に記載されているように記述しておきます。
試しにversionを削除したところ、

1
2
3
web_1  |   File "/usr/local/lib/python3.10/logging/config.py", line 498, in configure
web_1 | raise ValueError("dictionary doesn't specify a version")
web_1 | ValueError: dictionary doesn't specify a version

というエラーが出てしまいました。なので、記述しておきます。

disable_existing_loggers

disable_existing_loggersについて、公式ドキュメントには以下のように記載されています。

LOGGINGdisable_existing_loggers キーの値を True にすると、全てのデフォルトの設定が無効になります(キーが存在しない場合は dictConfig のデフォルトになります)。このため、 ’disable_existing_loggers’: True を使う場合は注意してください。 True を設定する必要は殆どないでしょう。 disable_existing_loggersFalse に設定して、デフォルトのロガーの一部、または全てを定義しなおすこともできます。あるいは、 LOGGING_CONFIGNone に設定して、 ロギングの設定を自分で行うこと も出来ます。

基本的にはFalseで良さそうですね。

handlers

[公式ドキュメント](ロギング | Django ドキュメント | Django)に記載があります。

Handlerはloggerから指定されます。今回の例で言うと、console
という名のハンドラを定義し、ログレベルと、ログを処理するハンドラクラスを指定します。

上記で指定しているlogging.StreamHandlerについてはPythonの公式ドキュメントに記載がありました
logging.handlers — ロギングハンドラ — Python 3.10.0b2 ドキュメント

StreamHandler クラスの新たなインスタンスを返します。 stream が指定された場合、インスタンスはログ出力先として指定されたストリームを使います; そうでない場合、 sys.stderr が使われます。

docker-compose upで動作させている時は、実行しているコンソールが標準出力なので、そのコンソールにログが出力されると言うことですね

loggers

[公式ドキュメント](ロギング | Django ドキュメント | Django)に記載があります

キー(今回でいうところのdjango.db.backends)は、パッケージのパスを指定します。そのパッケージ内で出力されたログは、ロガーで指定したログレベルと比較して同等以上の場合にロガーを通じてハンドラに渡されます。

以上で設定はすべて見終わりました。

詳細なログ設定

公式ドキュメントにあった詳細なログ設定も記載しておきます。パッケージごとに細分化されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'filters': {
'special': {
'()': 'project.logging.SpecialFilter',
'foo': 'bar',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'filters': ['special']
}
},
'loggers': {
'django': {
'handlers': ['console'],
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
},
'myproject.custom': {
'handlers': ['console', 'mail_admins'],
'level': 'INFO',
'filters': ['special']
}
}
}

フォーマッタは見やすくするために利用することもあるかもしれません。

動作確認

設定前と設定後でどのようにコンソールの出力が変わったか見てみます。

設定前

1
2
3
4
5
web_1  | Starting development server at http://0.0.0.0:8000/
web_1 | Quit the server with CONTROL-C.
web_1 | [08/Feb/2022 11:36:59] "GET /accounts/login/?next=/ HTTP/1.1" 200 1892
web_1 | [08/Feb/2022 11:37:05] "POST /accounts/login/?next=/ HTTP/1.1" 302 0
web_1 | [08/Feb/2022 11:37:05] "GET / HTTP/1.1" 200 1176

設定後

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
web_1  | Starting development server at http://0.0.0.0:8000/
web_1 | Quit the server with CONTROL-C.
web_1 | [08/Feb/2022 11:39:16] "GET /accounts/login/ HTTP/1.1" 200 1892
web_1 | (0.001)
web_1 | SELECT VERSION(),
web_1 | @@sql_mode,
web_1 | @@default_storage_engine,
web_1 | @@sql_auto_is_null,
web_1 | @@lower_case_table_names,
web_1 | CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
web_1 | ; args=None; alias=default
web_1 | (0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None; alias=default
web_1 | (0.001) SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`email` = 'test@example.com' LIMIT 21; args=('test@example.com',); alias=default
web_1 | (0.001) SELECT (1) AS `a` FROM `django_session` WHERE `django_session`.`session_key` = 'xxxxxxxxxxxxxxxxxxxxxxx' LIMIT 1; args=('xxxxxxxxxxxxxxxxxxxxxxx',); alias=default
web_1 | (0.000) INSERT INTO `django_session` (`session_key`, `session_data`, `expire_date`) VALUES ('xxxxxxxxxxxxxxxxxxxxxxx', 'yyyyyyyyyyyyyyyyyyyy', '2022-02-22 02:39:29.502823'); args=('xxxxxxxxxxxxxxxxxxxxxxx', 'yyyyyyyyyyyyyyyyyyyy', '2022-02-22 02:39:29.502823'); alias=default
web_1 | (0.004) UPDATE `auth_user` SET `last_login` = '2022-02-08 02:39:29.509067' WHERE `auth_user`.`id` = 1; args=('2022-02-08 02:39:29.509067', 1); alias=default
web_1 | (0.001) UPDATE `django_session` SET `session_data` = 'zzzzzzzzzzzzzzzzzzzzzzzz', `expire_date` = '2022-02-22 02:39:29.515114' WHERE `django_session`.`session_key` = 'xxxxxxxxxxxxxxxxxxxxxxx'; args=('zzzzzzzzzzzzzzzzzzzzzzzz', '2022-02-22 02:39:29.515114', 'xxxxxxxxxxxxxxxxxxxxxxx'); alias=default
web_1 | [08/Feb/2022 11:39:29] "POST /accounts/login/ HTTP/1.1" 302 0
web_1 | (0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None; alias=default
web_1 | (0.001) SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`expire_date` > '2022-02-08 02:39:29.536012' AND `django_session`.`session_key` = 'xxxxxxxxxxxxxxxxxxxxxxx') LIMIT 21; args=('2022-02-08 02:39:29.536012', 'xxxxxxxxxxxxxxxxxxxxxxx'); alias=default
web_1 | (0.001) SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21; args=(1,); alias=default
web_1 | [08/Feb/2022 11:39:29] "GET / HTTP/1.1" 200 1176

実行されているSQLがログに表示されるようになりました!

ログの種類によって色をわける

どうしてもRailsをベースに考えてしまい、ログの種類によって色が変わるのがいいと思って調べてみると、colorlogというパッケージが見つかりました。

Poetryでインストール後、LOGGINGを以下のように書き換えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'color': {
'()': 'colorlog.ColoredFormatter',
'format': '%(log_color)s%(levelname)-8s %(message)s',
'log_colors': {
'DEBUG': 'bold_black',
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'bold_red',
},
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'color',
}
},
'loggers': {
'django.db.backends': {
'level': 'DEBUG',
'handlers': ['console'],
},
}
}

formattersで指定したlog_colorsは例に載っていたものをそのまま利用しました。ここをお好みで調整すると色が変わります。

まとめ

開発デバッグ作業を効率よく行うには情報を増やすことが必要です。LOGGINGの設定であれば、一度行ってしまえばそのままずっと使えるのでしっかりと理解して設定するのがよいと思いました。

参考図書

データベース名を変更したらマイグレーションエラー MySQLdb._exceptions.DataError (1406, Data too long for column 'name') が発生したときの対処法

背景

Djangoでモデル実装をして、マイグレーションを実行しました。
その後、テータベース名が適切ではないという話になり、データベース名を変更することになりました。

其の際に思わぬエラーが発生したので記しておきます。

設定ファイルのデータベース名の変更

設定ファイル内でデータベース名の記載があったのは二つでした。

  • docker-compose.yml
  • front/settings.py

それぞれ新しいデータベース名に変更します。(frontはアプリケーション名)

新規データベースの作成

データベースのrenameができないということと、個々のテーブルを別データベースの下にrenameするのは数が多くて大変だったので、新規でデータベースを作成し、権限を付与した後にマイグレーションすることにしました。

データベースコンテナを起動した状態で、アプリケーションコンテナにログインして、mysqlコマンドでデータベースに接続しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ docker-compose run --rm web /bin/bash
Creating example_web_run ... done
root@ddad35e91809:/code# mysql -u root -p -h db
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 30
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> create database new_db;
Query OK, 1 row affected (0.002 sec)

MySQL [(none)]> grant all privileges on `new_db`.* to 'django'@'%';
Query OK, 0 rows affected (0.002 sec)

MySQL [(none)]> grant all privileges on `test_new_db`.* to 'django'@'%';
Query OK, 0 rows affected (0.002 sec)

MySQL [(none)]> exit
Bye
root@ddad35e91809:/code#
root@ddad35e91809:/code# exit
exit
$

マイグレーションでエラー発生

これでマイグレーションすれば大丈夫と思っていて、マイグレーションを実行しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
$ docker-compose run --rm web python manage.py migrate front
Creating example_web_run ... done
Operations to perform:
Apply all migrations: front
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying front.0001_initial... OK
Traceback (most recent call last):
File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/mysql/base.py", line 73, in execute
return self.cursor.execute(query, args)
File "/usr/local/lib/python3.10/site-packages/MySQLdb/cursors.py", line 206, in execute
res = self._query(query)
File "/usr/local/lib/python3.10/site-packages/MySQLdb/cursors.py", line 319, in _query
db.query(q)
File "/usr/local/lib/python3.10/site-packages/MySQLdb/connections.py", line 254, in query
_mysql.connection.query(self, query)
MySQLdb._exceptions.DataError: (1406, "Data too long for column 'name' at row 33")

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/code/manage.py", line 22, in <module>
main()
File "/code/manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/usr/local/lib/python3.10/site-packages/django/core/management/__init__.py", line 425, in execute_from_command_line
utility.execute()
File "/usr/local/lib/python3.10/site-packages/django/core/management/__init__.py", line 419, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 373, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 417, in execute
output = self.handle(*args, **options)
File "/usr/local/lib/python3.10/site-packages/django/core/management/base.py", line 90, in wrapped
res = handle_func(*args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/core/management/commands/migrate.py", line 277, in handle
emit_post_migrate_signal(
File "/usr/local/lib/python3.10/site-packages/django/core/management/sql.py", line 46, in emit_post_migrate_signal
models.signals.post_migrate.send(
File "/usr/local/lib/python3.10/site-packages/django/dispatch/dispatcher.py", line 170, in send
return [
File "/usr/local/lib/python3.10/site-packages/django/dispatch/dispatcher.py", line 171, in <listcomp>
(receiver, receiver(signal=self, sender=sender, **named))
File "/usr/local/lib/python3.10/site-packages/django/contrib/auth/management/__init__.py", line 83, in create_permissions
Permission.objects.using(using).bulk_create(perms)
File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 519, in bulk_create
returned_columns = self._batched_insert(
File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1324, in _batched_insert
self._insert(item, fields=fields, using=self.db, ignore_conflicts=ignore_conflicts)
File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1301, in _insert
return query.get_compiler(using=using).execute_sql(returning_fields)
File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1441, in execute_sql
cursor.execute(sql, params)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 99, in execute
return super().execute(sql, params)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 76, in _execute_with_wrappers
return executor(sql, params, many, context)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute
with self.db.wrap_database_errors:
File "/usr/local/lib/python3.10/site-packages/django/db/utils.py", line 90, in __exit__
raise dj_exc_value.with_traceback(traceback) from exc_value
File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
File "/usr/local/lib/python3.10/site-packages/django/db/backends/mysql/base.py", line 73, in execute
return self.cursor.execute(query, args)
File "/usr/local/lib/python3.10/site-packages/MySQLdb/cursors.py", line 206, in execute
res = self._query(query)
File "/usr/local/lib/python3.10/site-packages/MySQLdb/cursors.py", line 319, in _query
db.query(q)
File "/usr/local/lib/python3.10/site-packages/MySQLdb/connections.py", line 254, in query
_mysql.connection.query(self, query)
django.db.utils.DataError: (1406, "Data too long for column 'name' at row 33")
ERROR: 1

長々とエラーメッセージが出力されましたが、ポイントは

1
django.db.utils.DataError: (1406, "Data too long for column 'name' at row 33")

と思いました

状況確認

エラーメッセージで調べても、データが溢れてるんじゃない?とか、データ溢れた場合にエラーが発生しないようにするにはsql_mode=’’にすればいいよという内容しか出てきませんでした。

nameカラムの確認

nameというカラムを持つテーブルが複数あり、max_lengthなどを確認しましたが、だいたい128が設定されていました。そもそもマイグレーションではデータを入れることはないので、調べる箇所が検討違いでした。

マイグレーションの状況

テーブルが正常に作成されているかどうかを確認したのですが、正常に作成されていました。

テスト

変えたのはDB名のみです。なので、データベース名の長さが問題なのかと思い、長いデータベース名や短いデータベース名を試してみましたが、どの場合でもエラーが発生しました。ただ唯一、以前のデータベース名のみエラーが発生しないという状況でした。

解決方法

一旦全てのテーブルをロールバックしてから行ってみようと思い、アプリケーションすべてをロールバックしました。

その後、マイグレーションを行うとエラーは発生せず、マイグレーションも行われていました。

エラーメッセージからnameというカラムに問題があるのかと思ったのですが、一旦全てきれいにしてからマイグレーションを行えばデータベース名の変更は問題なさそうです。

ですが、もともとデータベースは新しく作成しているので、各アプリケーションのテーブルが影響しているとは考えづらいですね…

まとめ

データベース名の変更は極力行わない。行う場合は全てのアプリケーションのテーブルをロールバックしてから再度マイグレーションを行う。

参考図書

初期のモデル実装の調整方法

背景

実装初期、モデルの実装の調整は多く発生します。今行っているプロジェクトも業務内容が複雑で、なかなか細部まで理解するのが難しい状態です。

そう言った場合、テーブルの追加削除、カラムの追加削除、リレーションの設定等、たくさんの変更が入ります。

そういった作業をスムーズに行うために、モデルの修正->ロールバック->マイグレーションファイルへの適用->マイグレーション実行->データベース確認、の流れをまとめて記述しておきたいと思います。

ロールバック

すでにテーブルなど作成済の場合は、モデルの変更をマイグレーションファイルに適用する前にロールバックを行い、テーブルを削除しておきます。

1
2
3
4
5
6
7
$ docker-compose rum --rm web python manage.py migrate example_app zero
Creating example-app_web_run ... done
Operations to perform:
Unapply all migrations: example_app
Running migrations:
Rendering model states... DONE
Unapplying example_app.0001_initial... OK

これでテーブルは全て削除されました。

マイグレーションファイルへの適用

モデルを修正した後は、マイグレーションファイルへその変更を反映する必要があります。

しかし、マイグレーションファイルがある状態でマイグレーションファイルを作成しようとすると、新しいマイグレーションファイルができてしまいます。

運用フェーズに入っている場合はそれが正しいと思うのですが、初期のモデル調整で複数のマイグレーションファイルを作成したくないので、一旦マイグレーションファイルを削除します。

1
$ rm example_app/migrations/0001_initial.py

次にマイグレーション作成コマンドを実行します。

1
2
3
4
5
6
$ docker-compose run --rm web python manage.py makemigrations example_app
Creating example-app_web_run ... done
Migrations for 'example_app':
example_app/migrations/0001_initial.py
- Create model ...
(省略)

これで再度example_app/migrations/0001_initial.pyが作成されました。

マイグレーションの実行

マイグレーションファイルを作成したので、マイグレーションを実行します。

1
2
3
4
5
6
$ docker-compose run --rm web python manage.py migrate example_app
Creating example-app_web_run ... done
Operations to perform:
Apply all migrations: example_app
Running migrations:
Applying example_app.0001_initial... OK

マイグレーションが完了しました。

テーブルの確認

マイグレーションによりテーブルを作成したら、dhshellコマンドでモデルを変更した箇所が正しくテーブルに反映されているか確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker-compose run --rm web python manage.py dbshell
Creating example-app_web_run ... done
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 30
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [example_app]> show create table xxxx;

show create tableコマンドなどでテーブルを確認します。

修正したファイルのコミット(Git)

テーブルを確認し問題なければ修正したファイルをコミットします。モデルの修正点がわかりやすいようコミットコメントを記述します。

以上でモデルの修正が完了となります。

まとめ

アプリケーション開発初期はモデルの修正が頻繁に行われます。最初から完全なモデルを実装するのは難しいですからね。

修正・変更のログを取得しなくてもよい初期のモデル、テーブル構成はまっさらにしてから再作成するのがよいと思います。そうしないとマイグレーションファイルがどんどん増えて、煩雑になってしまいます。

参考図書

メールアドレスで認証するように変更する

背景

前回、Djangoが提供してくれている機能を用いてユーザー認証ページを作成しました。

ユーザー名はメールアドレスの場合が多いと思いますので、ユーザー名ではなくメールアドレスで認証できるように変更したいと思います。

認証バックエンドの追加

認証方法を変更するにはどのようにするかというと、ドキュメントに記載がありました。

このページの中の認証バックエンドを指定するをみると、認証バックエンドとして利用するクラスのリストはAUTHENTICATION_BACKENDSに定義されており、デフォルトでは

1
['django.contrib.auth.backends.ModelBackend']

となっているようです。確認してみます。

1
2
3
4
5
6
7
$ docker-compose run --rm web python manage.py shell
Creating example_web_run ... done
>>>
>>> from django.conf import settings
>>>
>>> settings.AUTHENTICATION_BACKENDS
['django.contrib.auth.backends.ModelBackend']

確かに設定されていました。

settings.AUTHENTICATION_BACKENDSはリストで、最初のクラスから順番にauthenticate()メソッドを呼び出し、最初に有効な認証と判断した段階で処理を終えるようです。

認証バックエンドの実装

では認証バックエンドを実装していきます。実装に必要な内容はドキュメントを参照します。

親クラスの指定

親クラスにはdjango.contrib.auth.backends.BaseBackendクラスを指定します。

デフォルトで指定されているクラスの親クラスも調べてみます。親クラスを参照するには__bases__メソッドを利用します。

1
2
3
4
5
6
7
$ docker-compose run --rm web python manage.py shell
Creating example_web_run ... done
>>>
>>> from django.contrib.auth.backends import ModelBackend
>>> ModelBackend.__bases__
(<class 'django.contrib.auth.backends.BaseBackend'>,)
>>>

デフォルトのModelBackendの親クラスもBaseBackendでした。

こちらのクラスを継承して実装してみようと思ったのですが、BaseBackendクラスの実装はほぼないので、自分で全て実装しないといけません。
https://github.com/django/django/blob/main/django/contrib/auth/backends.py

今回は認証部分(authenticateメソッド)のみを変更したいので、ModelBackendを継承して実装したいと思います。

空の実装で追加してみる

一旦認証バックエンドを追加しますが、試しに空の実装で追加してみます。accounts/backends.pyを作成し、以下のように実装します。

1
2
3
4
5
from django.contrib.auth.backends import ModelBackend


class EmailAuthenticationBackend(ModelBackend):
pass

そして、settings.pyにAUTHENTICATION_BACKENDSを定義します。

1
2
3
4
AUTHENTICATION_BACKENDS = [
'accounts.backends.EmailAuthenticationBackend',
'django.contrib.auth.backends.ModelBackend',
]

順番が重要なので、デフォルトの認証バックエンドよりも前にします。

動作確認

では動作確認を行います。ドキュメントによると、

ユーザーが一度認証されると、Djangoはどの認証バックエンドが認証に使用されたかを、ユーザーセッションに保持します

と記載されているので、sessionのデータを確認することでどの認証バックエンドが利用されたかがわかりそうです。

セッションはrequest.sessionでアクセスできます。ログイン後、適当なViewを作成し、request.sessionの中身を確認します。

1
2
3
4
5
from django.http import HttpResponse


def top(request):
return HttpResponse((',').join(request.session.keys()))

レスポンスを確認すると、

1
_auth_user_id,_auth_user_backend,_auth_user_hash

となりました。_auth_user_backendがいかにもそれっぽいですね。次はそのキーを指定します。

1
2
def top(request):
return HttpResponse(request.session['_auth_user_backedn'])

レスポンスは

1
accounts.backends.EmailAuthenticationBackend

となりました。新しく作成した認証バックエンドが利用されていることがわかりました。

メールアドレスで認証する

メールアドレスで認証するようauthenticate()メソッドを変更します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend

UserModel = get_user_model()

class EmailAuthenticationBackend(ModelBackend):
def authenticate(self, request, email=None, password=None, **credentials):
try:
user = UserModel.objects.get(email=email)
except UserModel.DoesNotExist:
return None
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user

こちらで実装が完了しました。

ただこれだけではメールアドレスで認証ができません。ログインフォームの改修が必要です。

ログインフォームの実装

ログインフォームについては実践Djangoのコードそのまま拝借します。以下のコードをaccounts/forms.pyとして保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from django import forms
from django.contrib.auth import authenticate, get_user_model
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _

UserModel = get_user_model()

class EmailAuthenticationForm(forms.Form):
email = forms.EmailField(max_length=254, widget=forms.TextInput(attrs={'autofocus': True}))
password = forms.CharField(label=_("Password"), strip=False, widget=forms.PasswordInput)

error_messages = {
'invalid_login': "メールアドレスまたはパスワードに誤りがあります",
'inactive': _("This account is inactive"),
}

def __init__(self, request=None, *args, **kwargs):
self.request = request
self.user_cache = None
super().__init__(*args, **kwargs)

self.email_field = UserModel._meta.get_field("email")
if self.fields["email"].label is None:
self.fields["email"].label = capfirst(self.email_field.verbose_name)

def clean(self):
email = self.cleaned_data.get("email")
password = self.cleaned_data.get("password")

if email is not None and password:
self.user_cache = authenticate(self.request, email=email, password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
params={"email": self.email_field.verbose_name}
)
else:
self.confirm_login_allowed(self.user_cache)

return self.cleaned_data

def confirm_login_allowed(self, user):
if not user.is_active:
raise forms.ValidationError(self.error_messages["inactive"], code='inactive')

def get_user_id(self):
if self.user_cache:
return self.user_cache.id
return None

def get_user(self):
return self.user_cache

URLの設定

フォームクラスを実装したら、そのフォームクラスを利用するようurls.pyを修正します。

accounts/urls.pyloginを以下のように修正します。

1
2
3
4
5
path('login/', LoginView.as_view(
form_class=EmailAuthenticationForm,
redirect_authenticated_user=True,
template_name='accounts/login.html'
), name='login'),

form_classの指定を追加しました。これで、テンプレートは変更することなく、フォームがメールアドレスをPOSTするフォームに変わります

from accounts.forms import EmailAuthenticationFormの1行も忘れずに

動作確認

/accounts/logout/にアクセスしてログアウト後、ログインフォームを表示し、メールアドレス、パスワードを入力してsubmitすると、TOPページに遷移することが確認できます。

メールアドレスで認証するよう実装することができました

まとめ

Djangoが提供してくれているユーザー認証の仕組みはそのまま使える感じだと思いますが、今回のようにメールアドレスで認証したい場合などは少し手を入れる必要があります。

ですが、それも決まったメソッドをオーバーライドするだけで非常に簡単に実装できました。

次回はユーザーモデルのカスタマイズを試してみたいと思います。

参考図書

Djangoでユーザー認証を実装する

背景

今実装しているアプリケーションでは、ユーザー認証機能が必須です。
実践Djangoのサンプルアプリケーションで、とても簡潔に実装されていたので、こちらにまとめておきます。

認証用アプリケーションの追加

まず認証用アプリケーションを追加します。

1
$ docker-compose run --rm web python manage.py startapp accounts

作成されたファイルを確認します。

1
2
3
4
5
6
7
8
$ find accounts -type f
accounts/migrations/__init__.py
accounts/models.py
accounts/__init__.py
accounts/apps.py
accounts/admin.py
accounts/tests.py
accounts/views.py

アプリケーションを追加するためにINSTALLED_APPSに追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ vi example/settings.py
...
...(省略)...

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'accounts.apps.AccountsConfig',
]
...(省略)...
...

アプリケーションの追加が完了しました。

URLの設定

Djangoが提供しているビューを利用して、認証用アプリケーションのURLを設定します。

Djangoはログイン用のビューとしてLoginView、ログアウト用のビューとしてLogoutViewというクラスを提供してくれています。

accounts/urls.pyを以下のように書き換えます。

1
2
3
4
5
6
7
8
9
10
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path

urlpatterns = [
path('login/', LoginView.as_view(
redirect_authenticated_user=True,
template_name='accounts/login.html'
), name='login'),
path('logout/', LogoutView.as_view(), name='logout')
]

pathの第3引数のname=はURLを逆引きするときの名前を指定します。

では作成したaccounts/urls.pyをプロジェクト配下のurls.pyで読み込むようにします。

1
2
3
4
5
6
7
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
]

テンプレートの作成

ログイン画面のテンプレートaccounts/login.htmlをまだ実装してないので、こちらを実装します。

bootstrapを利用して見た目を調整する

見た目を整えるためにbootstrap5を利用しようと思います。Djangoで利用するにはdjango-bootstrap5というパッケージをインストールします。

パッケージのインストール

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker-compose run --rm web poetry add django-bootstrap5
Creating example_web_run ... done
Skipping virtualenv creation, as specified in config file.
Using version ^21.3 for django-bootstrap5

Updating dependencies
Resolving dependencies... (22.2s)

Writing lock file

Package operations: 3 installs, 0 updates, 0 removals

• Installing soupsieve (2.3.1)
• Installing beautifulsoup4 (4.10.0)
• Installing django-bootstrap5 (21.3)

インストール完了しました。

INSTALLED_APPSに追加する

利用するためにはINSTALLED_APPSへの追加が必要です。

1
2
3
4
5
INSTALLED_APPS = [
...(省略)...
'accounts.apps.AccountsConfig',
'django_bootstrap5'
]

テンプレートの追加

accounts/templates/accounts/login.htmlを追加します。

レイアウトファイルの追加

ページの統一感を持たせるためレイアウトファイルを追加します。templates/base.htmlというファイルを追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% load static %}
{% load django_bootstrap5 %}
<html>
<head>
...(省略)...
</head>
<body>
...(省略)...
<main>
<div class="container">
{% block main %}{% endblock %}
</div>
</main>
</body>
</html>

accounts/login.htmlの追加

accounts/templates/accounts/login.htmlは先程作成したレイアウトファイルのmainブロックを上書きします。

1
2
3
4
5
6
7
8
9
10
11
12
{% extends "base.html" %}
{% load django_bootstrap5 %}

{% block main %}
<h2>ログイン</h2>
<form method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
{% bootstrap_form form %}
{% bootstrap_button button_type="submit" content="ログイン" %}
</form>
{% endblock %}

作成したので、ログインページにアクセスしてみます。

テンプレート検索ディレクトリの追加

アクセスしてみるとTemplateDoesNotExist at /accounts/login/というエラーが発生してしまいます。理由はレイアウトファイルを置いたディレクトリはテンプレート検索パスに含まれていないため、テンプレートが見つからないからです。

templatesディレクトリをテンプレート検索パスに追加します。example/settings.pyTEMPLATESリストのDIRSを以下のように変更します。

1
'DIRS': [BASE_DIR / 'templates'],

再度アクセスすると、無事ログイン画面が表示されました!

動作確認

以前にpython manage.py createsuperuseradminユーザーを作成していたので、adminでログインできることを確認します。

ログインフォームにユーザー名とパスワードを入力してsubmitすると、認証は通ったようですが、/accounts/profile/というページにリダイレクトされ、Page not foundエラーが表示されてしまいました。

ログイン後のリダイレクトURL設定

ログイン後のリダイレクトURLはsettings.LOGIN_REDIRECT_URLで指定します。デフォルトの値を確認してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker-compose run --rm web python manage.py shell_plus
Creating example_web_run ... done
# Shell Plus Model Imports
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
# Shell Plus Django Imports
from django.core.cache import cache
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.models import Avg, Case, Count, F, Max, Min, Prefetch, Q, Sum, When
from django.utils import timezone
from django.urls import reverse
from django.db.models import Exists, OuterRef, Subquery
>>> settings.LOGIN_REDIRECT_URL
'/accounts/profile/'

デフォルトでは/accounts/profile/になっていることがわかりました。こちらの設定を上書きします。

1
2
3
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

LOGIN_URLはデフォルトと同じですが明示的に記載しておきます。LOGOUT_REDIRECT_URLはデフォルトでは指定されていませんでしたので/に指定しました。

再度/accounts/login/にアクセスします。先程すでに認証されているので、TOPページにリダイレクトされました。

まとめ

Djangoが提供しているビューを利用してユーザー認証を実装しました。テンプレートを作成したくらいで、ほとんど実装することなく認証ページを作成することができました。とても便利だと思います。

次回はメールアドレスで認証するように変更してみたいと思います。

参考図書

MySQLコンテナの文字コードをutf8mb4に設定する

背景

実践Djangoのサンプルアプリケーションを実装していて、フォームからマルチバイト文字を保存しようとしたところ、エラーが発生しました。

MySQLのオフィシャルコンテナは、そのままだとデフォルトの文字コードがutf8になっていないようなので、その設定方法をまとめておきます。

クライアント側の設定

クライアント側はDjangoなので、Djangoの設定になります。
プロジェクト/settings.pyのデータベース設定に、OPTIONSに文字コードを指定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangosnippets',
'USER': 'django',
'PASSWORD': 'django',
'HOST': 'db',
'PORT': 3306,
'OPTIONS': {
'charset': 'utf8mb4'
}
}
}

以上でクライアント側の設定は完了です。

サーバー側の設定

MySQLのオフィシャルコンテナのページを見ると、以下のように記載があります。

1
2
3
4
5
Configuration without a cnf file

Many configuration options can be passed as flags to mysqld. This will give you the flexibility to customize the container without needing a cnf file. For example, if you want to change the default encoding and collation for all tables to use UTF-8 (utf8mb4) just run the following:

$ docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

設定ファイルをカスタマイズして読み込ませてもいいと思いますが、そうしなくていい方法を選ぼうと思います。コマンドのオプションで指定できるようなので、docker-compose.ymlにそのように記載します。

1
2
3
4
5
6
7
8
9
10
11
12
db:
image: mysql:5.7
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: 'django'
MYSQL_PASSWORD: 'django'
MYSQL_DATABASE: 'djangosnippets'
ports:
- 33060:3306
volumes:
- mysql:/var/lib/mysql

commandを記述して、起動コマンドを上書きします。

これでサーバー側の設定は完了です。

動作確認

では動作確認を行います。Djangoのコマンドでデータベースに接続します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker-compose run --rm web python manage.py dbshell
Creating djangosnippets_web_run ... done
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 24
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [djangosnippets]>

文字コードを確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MySQL [djangosnippets]> show variables like '%char%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database | utf8mb4 |
| character_set_filesystem | binary |
| character_set_results | utf8mb4 |
| character_set_server | utf8mb4 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.011 sec)

ちゃんとutf8mb4になっていました。

まとめ

MySQLコンテナの文字コードの設定方法を記述しました。

utf8mb4にしておくことで、絵文字も保存できるようになります。

開発環境を作る際に忘れてしまったらこのページを参照しようと思います。

参考図書

poetryを使ってDjango4.0にアップデートする

背景

2021年12月7日にDjango4.0がリリースされました。

リリースノート
https://docs.djangoproject.com/ja/4.0/releases/4.0/

今は3.2.9を利用していて、LTSなので使い続けることは可能なのですが、バージョンアップで後で苦労するのが大変そうだったので、今のうちにあげてみることにしました。

バージョン管理にPoertyを利用しているので、それを前提に記載していきます。

pyproject.tomlの変更

現在のバージョンの確認

現在のバージョンの確認をします

1
2
3
4
5
6
$ docker-compose run --rm web pip list
...
distlib 0.3.4
Django 3.2.9
django-extensions 3.1.5
...

現在は3.2.9です。

pyproject.tomlの確認

pyproject.tomlでの指定を確認します。

1
2
3
4
5
6
7
[tool.poetry.dependencies]
python = "^3.10"
boto3 = "^1.20.3"
Django = "^3.2.9"
mysqlclient = "^2.1.0"
requests = "^2.26.0"
platformdirs = "^2.4.0"

Djangoのバージョン指定を4.0.0にします。

poetryのバージョン指定

4.0系のバグフィックスバージョンは含めたいので、^4.0.0と指定します。
~4.0と書いてもいいのかもしれませんが、^4.0.0の方がわかりやすいかなと思います。

diffを取ると以下のようになりました

1
2
-Django = "^3.2.9"
+Django = "^4.0.0"

パッケージの更新

パッケージの更新にはpoetry updateコマンドを利用します。Djangoのみ更新したいので、パッケージも指定します。

1
2
3
4
5
6
7
8
9
10
11
$ docker-compose run --rm web poetry update Django
...
Updating dependencies
Resolving dependencies... (37.8s)

Writing lock file

Package operations: 0 installs, 1 update, 1 removal

• Removing pytz (2021.3)
• Updating django (3.2.9 -> 4.0)

Djangoが4.0にupdateされたと同時にpytzが削除されたようです。

poetry.lockの確認

poetry.lockのdiffも確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git diff poetry.lock 
...
[[package]]
name = "django"
-version = "3.2.9"
-description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
+version = "4.0"
+description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"

[package.dependencies]
-asgiref = ">=3.3.2,<4"
-pytz = "*"
+asgiref = ">=3.4.1,<4"
sqlparse = ">=0.2.2"
+tzdata = {version = "*", markers = "sys_platform == \"win32\""}
...

Djangoのバージョン指定が変わっていることがわかります。また、pytzが削除されています。

pytzが削除されて、tzdataが追加されている件については、
https://docs.djangoproject.com/ja/4.0/releases/4.0/#zoneinfo-default-timezone-implementation
が関係していると思います。

この変更の理由については、以下のスライドがわかりやすいと思います。
https://www.slideshare.net/ryu22e/python-39zoneinfo

まとめ

まだアプリケーションを作り込んでいないので、バージョンをあげたことによる変化は感じていませんが、逆に早めにバージョンを上げることで、これ以降のバージョンアップもスムーズにできるというメリットがあると思います。

新しいバージョンが発表されたら早めに対応するのが良いと思います。