TECH PLAY

株式会社モバイルファクトリー

株式会社モバイルファクトリー の技術ブログ

222

こんにちは、エンジニアの id:i1derful です。 2020年モバイルファクトリーアドベントカレンダー の21日目の記事です。 はじめに 僕は、とあるプロダクトチームのフロントエンドユニットに所属しています。 主に何してるかですと、フロントエンド改修プロジェクトに携わっています。 フロントエンド改修プロジェクトでは、レガシーなコードをリプレイスしています。 プロジェクト内での自身の主な役割は、 プロジェクトの方針および指針を決める スケジュールを切る 遅れたらテコ入れして上長に「いい感じ」に報告して交渉する メンバーの作業範囲・段取りを考えて伝える 何人のエンジニアを借りれるか相談する もちろん作業する このプロジェクトはすでに2年ほどやっています。 Q: 2年やっての感想は? A: 疲れましたね もちろん疲れただけでなく得られたものは当然ありますよ。 失敗は成功のもとと言いますので、しくじりエピソードを記事にまとめてみました。 プロジェクトのきっかけはなんですか? 我々プロダクトチーム開発者は「モダンな環境で開発したい」思いが切にありました。 このプロジェクトは、950あるUIコンポーネント等を移植していくプロジェクトです。 先人のフロントエンド職人の方が遺した2017年のドキュメントに「脱AngularJS」の文字が読み取れます。 この「脱AngularJS」を実現するべく、当時の急先鋒であった「Vue.js」が選定されました。 その結果「AngularJS 1.x」の上に「Vue.js」が乗っかってるトリッキーな構造が仕上がりました。 この仕組みを実現しているモジュールは、社内では『変態モジュール』と呼ばれているのですが、このモジュールがなかったらフロントエンド改修プロジェクトがさらなる荒波を突き進んでいたと思います。 敬意を込めて感謝します。ありがとうございました。 月日は流れ、我々プロダクトチーム開発者は「早く Vue.js 上だけで開発したい」思いにシフトしました。 プロジェクトの目的はなんですか? 「脱AngularJS」を掲げただけでは、ゴールが不明瞭でメリットが弱かったのでした。 そのため、以下の問題の解消を目的に掲げることで、プロダクトチームの合意を得てプロジェクトに昇格させました。 複雑な仕組みの単純化 先ほど言ったトリッキーな構造です 学習コストを下げる チームを移動しただけでトリッキーに出会うのはつらいですね 開発者体験(Developer eXperience: DX)向上 エンジニアはアーキテクチャがぐちゃぐちゃを嫌います ユーザー体験(User eXperience: UX)を手早く生み出す環境整備 制作範囲の外にあるトリッキーを考えず楽して作りたいものです 「脱AngularJS」は手段であって、DX向上なら生産性向上に繋がりプロダクトチームのメリットになるので目的になります。 高尚なことを言ってそうに聞こえますが、 「面倒くさいからこんな環境やめさせてくださいな」を言っているに過ぎないのです。 プロジェクトはどんな運用で動いていますか? 『 レガシーソフトウェア改善ガイド 』の帯に書いてありました。 これは延命ではない。進化なのだ! かっこいいですね。すなわち我々のプロジェクトもかっこいいはずです。 先人のフロントエンド職人のお力もあり、フロントの構造がモノリシックではなかったので「AngularJS のコンポーネントを Vue.js のコンポーネントに移植」が明白だったことが幸いでした。 『レガシーソフトウェア改善ガイド』において「リファクタリング」と「リアーキテクティング」は本質的に同じと説いていますが、コンポーネント移植が「リファクタリング」で完全なる「脱AngularJS」が「リアーキテクティング」に該当するでしょう。 Q: それはそれとしてシステムを止めているの? A: いえ、稼働中に少しずつ移植しています システムリプレイスには以下の方式がありますが、 一括移行方式 段階移行方式 並行移行方式 パイロット方式 このプロジェクトは「並行移行方式」が当てはまります。 なぜ「並行移行方式」を採用しているのかは、コンポーネントを含むモジュールの数が950もあるためですね。 「一括移行方式」を採用していたら膨大すぎて大爆発する恐れもありました。問題箇所が深くなってわかりにくくなるでしょう。 「段階移行方式」を採用していたら950もあれば都度都度システムを停止することになり、プロダクトが死んでしまうでしょう。 リスクは当然減らしたいし、サービスを止めたくありませんよね。 そこで『変態モジュール』があることで、我々プロジェクトは「並行移行方式」のコンポーネント移植ができたわけです。 再度あらためて先人に敬意を込めて感謝します。ありがとうございました。 しくじりエピソード プロジェクトを進行するにあたって、さまざまな出来事がありました。 振り返りとして反省点を3つ挙げてみます。 しくじり① 設計の正しさを体現したい欲望に取り憑かれていた 理想形も叶えたい謎のバイタリティに溢れていた時期があり、設計の正しさを体現したい欲望に取り憑かれていました。 今なら「このタイミングでやるべきことじゃない」と、優先度を考慮した上で言い切れます。 これはプロジェクト内で全会一致した、方針ではなく指針、確固たる作業指針がなかったがためと思います。 『レガシーソフトウェア改善ガイド』によれば「リライトのリスク」を軽んじていたのでしょう。 とはいえ、設計に対する考えが深まったので、やって損だったとまでは思わないですね。 しくじり② 締め切りが近くなるまで焦っていない 夏休みの宿題でしょうか。 僕は夏休みの宿題を始業式間近になって焦ってやりだすどころか、もはや提出すらしない破天荒な子どもでした。 会社員として働けていることが奇跡に近いかもしれません。 とはいえ、個人だけでなくプロジェクトメンバー全員が等しく締め切りを意識しなければならないはずです。 だからベロシティグラフを見ながらメンバーに発破をかけたりすることで進捗を安定させないといけません。 締め切りを守らないなんて組織が許しませんし、自発的に動くチームは常に改善活動でアップデートしていくはずです。 スクラムで言うところの透明性がなくては健全なチームではないですね。 しくじり③ やっぱり皮算用の見積もり精度は低い 大きなタスクは中身が見えないことから規模感の言い値で見積もりされがちなので、やっぱり精度は高くなかったですね。 そのため、プロジェクトが進行していくうちに「難易度」という物差しで工数見積もりを行い見積もり精度を高めようとしていきました。 難易度の低いタスクから中難度のタスクまではほぼほぼ見積もりどおりとは思いました。 けれど、最も難易度が高いタスクは15日かかると見積もりをしていたのですが、15日でやるにはさすがに無理をして(残業して)間に合わせる努力をしましたね。 小さく見積もりたくなる人間の弱さ(自分を優れたものとして見せたがる)が出てしまったと振り返れます。 かといって、単に見積もりを盛って、30日など大きめの数字で見積もっても、上長への報告時に「あーそんなに」という無価値なトークで時間が潰れるので難しいところですね。 大きめの数字になるなら、もっと近くのゴールがあるのでタスクを小さく分割します。 分割すると見積もり精度が上がります。 難易度の物差しを使う場合、以下のような「つらみ」が見積もり数字に反映されます。 発生条件がレアな動作確認 動作確認のために環境整備が必要 共通化がもたらした密結合なコンポーネントの関心の分離 モジュール同士の依存関係がズブズブで1つずつほどく必要がある 世の中には難易度評価の手法がすでにあるので、それを元に見積もるのが楽そうです。 難易度評価をする際は、以下が必要と言われています。 質的作業分析 量的作業分析 技術的実現性 先人は必ずいます。車輪の再発明なんて楽をしていないのでまっぴらごめんですね。 さいごに このプロジェクトに関わっていただいた、今も関わってくれているメンバーの皆さん本当にありがとうございます。 少人数、かつプロダクトが正常に稼働することが最優先なので、プロジェクトの進行がストップすることもよくありました。 「脱AngularJS」は続きますが、終わりの目処が見えてきました。 貴方が入社されるときには「刷新されたモダンなフロントエンド開発環境」が用意されている(はず)でしょう。 しゃべってても仕方ないので、ひたむきに愚直にがんばりますね。 早く終わらせて一緒に祝杯でもあげましょう。 それではまた今度。 次の記事は id:Eadaeda さんです。
アバター
この記事は、 モバイルファクトリー Advent Calendar 2020 20日目の記事です。 こんにちは、最近眠りが浅いことで悩んでいる Yunagi_N です。 一昨年に続いて 私はマイペースに、今年も趣味全開のお話をします。 はじめに 今年4月くらいから某 VR SNS にはまって Unity に興味を持ち、いろいろなことをやっているのですが、 VR 世界で VR ならではのパフォーマンスをやっている人たちを見かけて、憧れてパーティクルをいじってみました。 ここでは、一般にパーティクルライブや VRMV と呼ばれているパフォーマンスを指しますが、 それぞれの説明については実際に体験してもらうのが一番良いので、各種 VR プラットフォームにおでかけしてみてください。 今回、普通にパーティクルをいじってみるだけでは特に面白みが無いと感じたので、 再生中の音楽に合わせて動的に変化する効果をスクリプトとシェーダーで作ってみました。 なお、 会社で開発しているアプリ・プロジェクトとは一切関係ありません。 前提 以下の環境で開発、動作確認を行っています: Windows 10 Unity 2018.4.20f1 また、本記事の実装は VR SNS 内部で動くように作られており、 セキュリティ上の理由などから、独自実装の VM 上で動くため以下の制限があります: C# の一部の構文のみをサポート (C# 7 相当の機能にさらに制限をかけたもの) 本来は C# そのものではなくノード形式のプログラミング言語で行うのですが、有志が C# で作成できるアセットを公開しています Unity で使えるすべての API が許容されているわけではない 例えば、 Job System や ECS (2020.2 の時点でロードマップから消えたようですが)、また List さえ API が公開されていません なお、この記事では以下の事については解説しません: Unity の基礎 パーティクル (Particle System) の基礎 シェーダー (ShaderLab, HLSL) の基礎 また、本記事では using を省いていることがあります。ご了承ください。 また、私は音声周りのプロではないので、記事中に間違いなどがある場合があります、ごめんなさい。 実装 まずは音声情報を取得します。 パーティクルなどの動きに応用できるような情報は基本的には以下の2つだと思います。 オーディオレベル (dB 単位) 周波数スペクトル情報 それぞれの取得は、以下のコードで簡単に行えます。 まずオーディオレベル (dB) は GetOutputData から計算できます。 なお、ここでのオーディオレベル (dB) は dBFS と呼ばれるもので、以下の計算式で求められます。 // MaxValue は RMS (Root Mean Square) が取り得る値の最大値 var dbfs = 20.0f * Mathf.Log10(RMS / MaxValue); 通常、 GetOutputData で得られる値の範囲は -1 ~ 1 であるため、下記のコードにて dBFS が求められます。 private const int SampleCount = 1024 ; // 64 ~ 8192 の範囲の 2 のべき乗の値を指定する必要があります。 [SerializeField] private AudioSource audioSource; private float [] _samples = new float[SampleCount]; private void Update() { var db = CalcDecibel(); } private float CalcDecibel() { audioSource.GetOutputData(_samples, 0 ); var sum = 0.0f ; foreach ( var sample in _samples) sum += sample * sample; var rmsValue = Mathf.Sqrt(sum / SampleCount); var dbValue = 20.0f * Mathf.Log10(rmsValue); if (dbValue < - 80.0f ) dbValue = - 80.0f ; return dbValue; } 次に、周波数ごとのスペクトル情報は GetSpectrumData を使います。 特に難しいことはないですね。 // 各種インスタンス変数は上記のものを使い回します。 private void Update() { audioSource.GetSpectrumData(_samples, 0 , FFTWindow.Hanning); } このとき、第3引数に設定する FFTWindow は、求めている精度に応じて適切なものを使用します。 今回、 GetSpectrumData で取得したいデータはそこそこの精度で得られれば良いので、 FFTWindow.Hanning を設定しました。 また、配列に入れられた値は、以下のように計算することで、インデックスと Hz を変換できます。 var i = /* 配列の index */ ; var hz = AudioSettings.outputSampleRate * 0.5f * i / SampleCount; 例えば、 AudioSettings.outputSampleRate が 44100Hz である場合、配列の1番目の周波数は、 var hz = 44100 * 0.5 * 1 / 1024 ; // 21.53Hz といった具合で、以降は 21.53Hz ごとにデータがサンプルされています。 これで、再生されている音声から各種情報が取得できました。 ただし、オーディオレベルはまだしもスペクトルは生データのままでは使いづらいので、 これらのデータを加工したうえでパーティクル (Particle System) やシェーダーなどに渡しやすくします。 データの加工形式はいくつかあると思いますが、今回は最終的に以下のデータを渡してパーティクルを制御することにしました。 オーディオレベル (dBFS) ピッチ情報 音域ごとのオーディオレベル (dBFS) 個人の好みで分類 Peak Hold Fall Down VU メーターで、最大値が更新されたらそこに点が移動し、徐々に低下していくアレです ここでは、最大値 ( max ) + r Hz を範囲に取り、以下の演算の結果を渡します (Mathf.Clamp(n, max - r, max) - (max - r)) / r まずピッチ情報ですが、これはスペクトル情報から一番大きい値を取り出し、 良い感じに補正してあげれば、それらしい値が得られるようです。 (ただし、通常の音楽においては正確な値は取れないそう。) コードは以下の通り。簡単ですね。 // 各種インスタンス変数は上記のものを使い回します。 private void Update() { audioSource.GetSpectrumData(_samples, 0 , FFTWindow.Hanning); var pitch = CalcPitch(_samples); } private float CalcPitch( float [] samples) { var maxValue = 0.0f ; var maxIndex = 0 ; for ( var i = 0 ; i < SampleCount; i ++ ) { var spectrum = samples[i]; if (maxValue > spectrum) continue ; maxValue = spectrum; maxIndex = i; } var l = samples[maxIndex - 1 ] / samples[maxIndex]; var r = samples[maxIndex + 1 ] / samples[maxIndex]; var f = maxIndex + 0.5f * (r * r - l * l); return f * AudioSettings.outputSampleRate * 0.5f * maxIndex / SampleCount; } 次は、音域ごとに周波数帯を分類し、各音域のオーディオレベルを計算します。 これは、カヤックさんのオーディオスペクトルアナライザーのコードを元に、 bin を128個に分類したものから、特定周波数区域の値の平均値を取りました。 詳しくは、記事末尾に記載している参考リンクを参照ください。 最後は Peak Hold Fall Down の実装ですが、これは下のコードで実装しました。 private const float FalldownPerTick = 0.1f ; private const float LevelRange = 5.0f ; private float _peak; // 各種インスタンス変数は上記のものを使い回します。 private void Update() { var db = ...; // 初めに計算した dBFS var value = CalcPeakFallDownValue() } // 雑だけど private float CalcPeakFallDownValue( float db) { var delta = Time.deltaTime; _peak = Mathf.Max(_peak - FalldownPerTick * delta, - 80.0f ); _peak = Mathf.Clamp(db, _peak, 0.0f ); var minValue = _peak - LevelRange; return (Mathf.Clamp(db, minValue, _peak) - minValue) / LevelRange; } ここまでで、ようやく必要なデータがそろいました。長かったです。 今度は、これらのデータを Particle System に渡してあげます。 その前に、今回使うシェーダーのコードを張っておきます (ShaderLab は Transparent で良い感じに)。 ポイントはテクスチャを透明度に変換しているのと、頂点カラーを使っていることくらいです。 テクスチャを透明度に変換しているのは用意したテクスチャの都合から、 頂点カラーを使っているのは、 Material 数を減らしたいというプラットフォーム上の都合からです。 なお、 Particle System からシェーダーにデータを渡すには、 Renderer モジュールのうち、 Custom Vertex Streams を有効にした上で、何をどのセマンティクスに渡すか設定する必要があります。 // 各エントリポイントは、以下の設定 (ShaderLab) // // #pragma vertex vs // #pragma fragment fs // #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD; float4 color : COLOR; // 他はご自由に... } struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD; float4 color : COLOR0; // 他はご自由に... } v2f vs (appdata v) { v2f o = (v2f) 0 ; o.vertex = UnityObjectToClipPos (v.vertex); o.uv = TRANSFORM_TEX (v.uv, _MainTex); o.color = v.color; // 渡したいものや変換したいものをお好きに return o; } fixed4 convertMonochromeToTransparent ( const float3 color) { const float transparent = color.r + color.g + color.b; return fixed4 ( 0 , 0 , 0 , transparent / 3 ); } fixed4 fs (v2f i) : SV_TARGET { fixed4 color = convertMonochromeToTransparent ( tex2D (_MainTex, i.uv)); color.rgb = i.color.rgb; color.a *= i.color.a; // Emission color.rgb *= pow ( 2 , _Emission); // あとは付けたい効果をどしどしと return color; } 最後に、 Particle System でデータを渡してあげます。 上記で変換したデータを元に、 Particle System や Particle を操作するコンポーネントをつくります。 それぞれの操作は単純なものなので、コードは省略しますが、私は以下のようなものを作成しました: 特定のデータの値を増幅・減衰させてさらにデータを扱いやすくするコンポーネント 特定のデータが条件を満たした場合、パーティクルを Emit 特定のデータが条件を満たした場合、 Particle System のプロパティを変更 これはインスペクターを作るのが面倒なので、非 Active な Particle System から値を引っ張ってくるように実装しました 特定のデータの値や変化量を Particle そのものに渡す 元データの値を良い感じにして、 velocity や rotation に渡すと良いです ちなみに、 Particle そのものの操作は下記のようにすれば行えます。 // private ParticleSystem ps; var particles = new ParticleSystem.Particle[ps.particleCount]; ps.GetParticles(particles); for ( var i = 0 ; i < ps.particleCount; i ++ ) { var particle = particles[i]; // お好きな操作をここで paritcles[i] = particle; } ps.SetParticles(particles); と、こんな感じで、音声情報を元に Particle System を操作できるコンポーネント群が完成しました。 あとは、一緒に再生したい音楽や BGM に合わせて、ひたすら数値を調整していけば、完成です。 正直な話、コードを書くよりもひたすら数値調整するのが厳しい気がしますが、そこは根気よく頑張りましょう。 では、お疲れさまでした。また来年会いましょう。 次の記事は id:i1derful さんです。 参考: 音響とか / Sound and Acoustics How to choose FFT Window type - Unity Answers ヤマハ - 製品情報 Unity のオーディオの再生・エフェクト・解析周りについてまとめてみた - 凹みTips スマホ実機でサウンドのスペクトル解析を見たい - KAYAC engineers' blog
アバター
この記事は モバイルファクトリー Advent Calendar 2020 19日目の記事です。 こんにちは!新卒エンジニアの id:dorapon2000 です。開発する方にとってGitは必須だと思いますが、どのように操作しているでしょうか。コマンドラインから、エディタの拡張機能から、GUIクライアントからなどの方法があると思います。今回は、GitKrakenというGitのGUIクライアントについて紹介して、コマンドラインとの違いについて考えたことを書きたいと思います。クライアントだけを利用する人やコマンドラインだけを利用する人が新しい気付きを得られると嬉しいです。 GitKraken https://www.gitkraken.com/ UI/UXにとても力を入れているGitのGUIクライアントです。第一印象はGitグラフが美しいですよね。私自身はこの見た目に魅了されて、個人的な開発ではGitKrakenを利用しています。見た目がきれいだと開発のやる気も起きます。 アカウントマネジメントやself-hostedのリポジトリと連携する以外のおおよその機能は無料で利用できますが、プライベートリポジトリを利用したい場合は サブスクリプション に登録する必要があります。プライベートリポジトリを利用できるようになるIndividualであれば$29/yearです。 機能 Git操作の基本的なことはGUI上からほぼできます。 git add & commit addとcommitをします。 git add -p addする部分の選択も行単位で可能です git rebase -i HEAD~2 2つのコミットを1つにまとめます。 git commit --amend & rebase -iの[r]eword コミットの編集をします。 git checkout/push/pull ボタンを押すだけですぐ可能です。 コマンドラインと比べて楽なところ 差分をすぐ見れる ファイルや一部のコードに対するHEADに戻す操作がとても簡単 コマンドラインでは複数のコマンドを使い分ける必要がある git reset --hard HEAD git checkout -p ファイルのgit addが簡単 コマンドラインでは git add -p でハンクごとに追加するか指定する必要があるがGUIではさくっとできる コミットメッセージの編集が簡単 コマンドラインではコミットが直近であるかないかで複数のコマンドを使い分ける必要がある 1つ前であれば git commit --ammend 2つ以上前であれば git rebase -i remoteでバックログされているコミットがすぐわかる コマンドラインでは git fetch をしないとremoteの最新版とlocalでどれだけ履歴が離れているかわからない クライアントであれば裏で定期的に git fetch をやってくれる mergeコミットのrevertが簡単 コマンドラインからだと git revert $hash -m 1 のようにオプションを付ける必要ある。このオプションを覚えなくても済みます。 GitKrakenではできないこと 普段遣いで気づいたGitKrakenではできないことをあげようと思います。 少し複雑なgit差分だと、1行単位で git add ができない( git add -p の[e]dit) rebaseをundoできない ssh先のリポジトリを管理できない 少し複雑なgit差分だと、1行単位でgit addができない コミットせずにまとめて編集をして、あとから細かくコミットしていくということが私はよくあります。変更が離れた場所であればGitKraken上でもワンクリックでその部分だけaddできるのですが、同じ場所に分離したい変更が混じっているとまとめてaddされてしまい、1行ずつaddのような細かい指定ができません。つまり、 git add -p の[e]ditのような細かいaddができません。 rebaseをundoできない コマンドラインでは git reflog から git reset --hard でもとに戻せますが、GitKrakenの現在の仕組みではそこまで柔軟に対応できないようです。 ssh先のリポジトリを管理できない sshした先のgitリポジトリをGitKrakenでは管理できません。しょうがないかなとも思っていますが、将来の機能追加に期待しています。 GUIクライアントを使ってみて コマンドラインだとオプションやコマンドの使い分けを覚える必要がある部分を、GUIクライアントではワンクリックで実現できてしまいます。一部の操作において手数を大きく省ける力があります。また、gitの学習敷居を下げる役割もあると思いました。 どちらからでもよいと思った操作は、checkoutやstash、push、pullのようなシンプルなコマンド群です。個人的にはコマンドラインから操作するのと手数感は変わりませんでした。 逆にGUIクライアントでは手が届かない部分として、上述した git add -p のeや git reflog などがあります。いつも使うわけではないですが、いざというときにあると嬉しいですね。 私の場合、基本的にGitKrakenから操作して、コマンドラインをちょうど触っているときは直接コマンドを叩いたりしています。 まとめ GitKrakenの紹介を兼ねて、GUIクライアントとコマンドラインの操作感の違いについてご紹介しました。結論としてどちらにも得手不得手がありますが、個人的にはGUIクライアントのスピード感はとても好みです。 明日の記事は Yunagi_N さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 18日目の記事です。 こんにちは、エンジニアの @PikkamanV です。 先日CloudBees社が認定するCertified Jenkins Engineer 2020を取得しました。 この記事では普段の業務で得たJenkinsの知識の延長で試験に合格するまで過程を記録しています。 同じように受験される方の参考になれば幸いです。 ※本記事は2020年11月時点の試験に基づいており、現在は試験に変更点がある可能性があります。 受験の際は最新の情報をCloudBees社の公式サイトでご確認ください。 www.cloudbees.com なぜ受験したか 元々私は業務でCIツールまわりを触ることが多かったのですが、今年の春ごろからあるプロジェクトのJenkinsサーバを触ることになりました。 ちなみに 去年のアドベントカレンダーでもCircleCIについて書いています 。 件のJenkinsサーバは先人の書いた構築ログやJenkinsfileを元に構築を行い、9か月ほど自分が中心となって運用していました。 その結果チーム内からは「自称Jenkins職人」として認知されるようになり、サーバに何か不具合があると呼ばれるようになっていました。 トラブルを解決するたびにJenkinsについての知識を深めていきましたが、果たしてこれでJenkinsそのものに詳しくなったと言えるか?今のプロジェクトの事情に詳しいだけで、他のプロジェクトでは通用しない知識なのではないかと考えるようにもなりました。 そこで今回Certified Jenkins Engineerの認定試験を受け、合格して「認定Jenkins職人」となり自分のスキルアップを証明することを目指しました。 また、受験料は会社のキャリアアップ支援制度を利用して負担してもらいました。 モバファクでは社員が外部のセミナーへの参加や資格取得などにかかる費用を、予算の枠内で支援する制度があります。 昨年のアドベントカレンダーにもキャリアアップ支援制度を利用してAWSのトレーニングを受講した記事があるので参考にしてください。 tech.mobilefactory.jp 試験の概要 試験はエンタープライズ版のCloudBees Jenkinsを提供しているCloudBees社が運営しています。 日本語対応はしておらず申込から受験まですべて英語のみで行うことになりますが、日本のテストセンターでも受けることができます。 ただし、今年は世界的なCOVID-19の流行のため、自宅でオンライン受験をすることができました。 受験料もテストセンターで受ける場合(150ドル)に比べて自宅受験の方がお安くなっているので(99ドル)おすすめです。 試験範囲もテストセンターで受ける場合と同一です。 試験自体は90分で60問の択一問題を解きます。複数の選択肢を選ぶ問題では部分点もあります。 合格ラインは66%ですが、問われる内容は多岐に渡るため、後述するコースワークをやっただけではなく実際に何ヶ月か運用した経験がないとボーダーを超えるのは難しいと思います。 試験勉強から受験まで 勉強できる期間は1か月ほどだったので、できるだけ効率的に試験勉強を進めようとあらかじめ計画を立てました。 まずCloudBees社が提供する学習サイトのCloudBees Universityに試験ガイドが掲載されています。 このガイドには試験範囲やサンプル問題、参考資料が掲載されており、勉強の指針を立てるときに最初にチェックする文書になるでしょう。 https://standard.cbu.cloudbees.com/certification-guide-and-information/370303 standard.cbu.cloudbees.com また、 2018年版の試験ガイド にはさらに詳細に読むべき資料が書かれていて便利です。 しかし、1か月の勉強期間で参考資料をすべて読むことは不可能に思えました。 そこで、まずインターネット上から受験者の体験記を探し、彼らの勉強した内容をまとめました。 日本語話者の情報はあまり出てこないのですが、英語で検索すると結構出題パターンが分かってきます。 また、YouTubeにも対策講座がいくつか挙がっているので参考になりました。以下はその一例です。 実際の試験問題は試験ガイドのサンプルより難しいので、無料の教材でもやってみると練習になります。 www.youtube.com 総合して以下の方針で試験を攻略することにしました。 自宅にJenkinsサーバを立てて、インストールや管理の方法を復習する 業務で使っているJenkinsやJenkinsfileでやっていることを復習する CloudBees社が提供する無料コースワークにVMを立てて手を動かして取り組む コースワークで登場した用語や機能を Jenkins Handbook から調べていく 結果的にこの手順で試験に合格するのに必要な知識はカバーできました。 学べたこと Jenkinsのフリースタイルジョブとパイプラインについては業務経験で得た知識をもう一度実践してみる形になり、よい復習になりました。 一方で業務では使っていない機能については覚えるのはやや苦労しましたが、新鮮な気持ちで勉強できました。 いくつか例を挙げてみます。 Blue Ocean パイプラインをGUIで管理する機能です。 GitHubなどのSCMサービスと連携しGUIでJenkinsfileを編集し、パイプラインを実行後、コミットすることができます。 特に便利な点は、GUIで各stepを編集している途中でもリアルタイムにJenkinsfileをプレビューすることができることです。また、プレビュー中のJenkinsfileを直接編集しても、GUIでの設定が変更されるので、操作が分かりやすいです。ただし、一部の処理はまだBlue Oceanに対応していないのに注意が必要です。 Shared Library あるプロジェクトに複数のJenkinsfileがあるときに便利な機能です。コピペされた処理が書かれたJenkinsfileたちの一部に変更が入った場合、すべてのJenkinsfileに同じ変更を加えることになり大変な手間です。 そういう時に共通処理部分を共有ライブラリとしてまとめ、SCMから直接配布することができます。これによってJenkinsfileは小さく保守のしやすい形を保つことができます。 Docker agent 今のプロジェクトではJenkinsサーバ自体に様々なミドルウェアをインストールしているのですが、ビルド環境をDockerで用意することも可能です。Docker Pipeline プラグインを使うと公開されているイメージや自前のイメージを指定できるようになります。Dockerを使うことでCIのプロセスだけでなく、その環境構築も再現性を高くできるので、タイミングを見て移行したいと考えています。 まとめ 今回の受験を通じて自分のスキルを客観的に証明できるようになっただけでなく、業務だけでは得られない知識を体系的に学ぶことができました。いわゆるベンダー資格は受験料が高くなかなか手を出しにくかったのですが、これからは会社の支援制度も利用していくことで、他分野についてもスキルアップしていけたらと思います。 明日の記事は id:dorapon2000 さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 17日目の記事です。 こんにちは、エンジニアの shioiyan です。 モバイルファクトリーには部活動制度があり、いくつもの部活動が存在しているのですが、自分はそのうちのゲームジャム部に所属しています。 今年2月から弊社はリモートワークになりましたが、ゲームジャム部はビデオ通話を使って活動を継続しています。 近頃、外出自粛している人が増えた中でも、ビデオ通話で話しながら楽しく遊べるサービスを作ろう!ということで部活を通じて、Web上でリアルタイムにそれぞれの画面が同期するお絵かきチャットの開発をしました。 仕様 今回作るリアルタイムお絵かきチャットの仕様はざっくり以下のようになります。 ユーザは部屋を選んで入室ができる 部屋にはマウスやタップ操作で絵を描くことのできるキャンバスがある 絵を描くと同じ部屋のメンバーのキャンバスがリアルタイムで更新される 部屋を退室することができる 技術選定の背景 各クライアントのお絵かき画面をリアルタイムで同期するためには WebSocket API を用いることが思い浮かびました。 しかし、WebSocketサーバの構築・管理にはコストがかかるため、「動いて触れるものを素早く作りたい」という方針だったゲームジャム部で作るには少しネックでした。 調べていく中で API Gateway の WebSocket API を用いればサーバレスでさくっと構築できてサーバの管理要らずで良さそう、ということがわかってきたので、公式で紹介されていたチャットの実装を参考にものは試しと作ってみました。 すると思った以上にさくっと要件を叶える実装をすることができたので、その知見を共有しようと思います。 利用した技術スタック リアルタイム通信: Amazon API Gateway WebSocket API バックエンド: AWS Lambda 接続/部屋情報の保持: Amazon DynamoDB フロントエンド: Nuxt.js (v2.14.11) 構成 まずはじめに今回実装したものの全体の構成を示しておきます。 クライアントはAPI GatewayとWebSocketで通信し、通信内容に応じて3種類のLambda関数を実行します。 DynamoDBはLambda関数を通じて接続しているクライアント情報の参照や更新を行います。 実装されたもの(canvasの同期のみ) (左のブラウザのcanvasに描いた絵が右のブラウザにも同期されています) 実装 ▶︎ フォルダ構成 $ tree -I node_modules -L 3 . ├── webSocket │ ├── onConnect │ │ └── app.js │ ├── onDisconnect │ │ └── app.js │ ├── package.json │ ├── sendMessage │ │ └── app.js │ ├── serverless.yml │ └── yarn.lock └── view ├── README.md ├── assets │ └── README.md ├── commitlint.config.js ├── components │ └── README.md ├── layouts │ ├── README.md │ └── default.vue ├── middleware │ └── README.md ├── nuxt.config.js ├── package.json ├── pages │ ├── README.md │ ├── room.vue │ └── index.vue ├── plugins │ └── README.md ├── static │ ├── README.md │ └── favicon.ico ├── store │ └── README.md ├── stylelint.config.js ├── utils │ └── webSocket.js └── yarn.lock Serverless Framework API Gateway WebSocket API + Lambda + DynamoDBの構成はServerless Frameworkで作成します。 コマンド1つで各種リソースの生成・更新・削除ができるのはとても便利です。 デプロイ $ sls deploy -v --stage dev ... Service Information service: advent-calendar-2020 stage: dev region: ap-northeast-1 stack: advent-calendar-2020-dev resources: 25 api keys: None endpoints: wss://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev functions: connectHandler: advent-calendar-2020-dev-connectHandler disconnectHandler: advent-calendar-2020-dev-disconnectHandler sendMessageHandler: advent-calendar-2020-dev-sendMessageHandler layers: None Stack Outputs ConnectHandlerLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxx:function:advent-calendar-2020-dev-connectHandler:10 DisconnectHandlerLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxx:function:advent-calendar-2020-dev-disconnectHandler:10 ServiceEndpointWebsocket: wss://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev ServerlessDeploymentBucketName: advent-calendar-2020-dev-serverlessdeploymentbuck-xxxxx SendMessageHandlerLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxx:function:advent-calendar-2020-dev-sendMessageHandler:10 ✨ Done in 18 .34s. リソース一括削除 $ sls remove --stage dev ▶︎ webSocket/serverless.yml service : advent-calendar-2020 provider : name : aws stage : ${opt:stage, 'dev' } region : ${opt:region, 'ap-northeast-1' } runtime : nodejs12.x iamRoleStatements : - Effect : Allow Action : - dynamodb:Query - dynamodb:GetItem - dynamodb:PutItem - dynamodb:DeleteItem Resource : - Fn::GetAtt : [ ConnectionsTable, Arn ] - Effect : Allow Action : - dynamodb:Query Resource : "arn:aws:dynamodb:${self:provider.region}:*:table/${self:service}-connections-${self:provider.stage}/index/*" environment : TABLE_NAME : Ref : ConnectionsTable websocketsApiName : ${self:service}-${self:provider.stage} websocketsApiRouteSelectionExpression : $request.body.message functions : connectHandler : handler : onConnect/app.handler events : - websocket : $connect disconnectHandler : handler : onDisconnect/app.handler events : - websocket : $disconnect sendMessageHandler : handler : sendMessage/app.handler events : - websocket : sendMessage resources : Resources : ConnectionsTable : Type : AWS::DynamoDB::Table Properties : TableName : ${self:service}-connections-${self:provider.stage} AttributeDefinitions : - AttributeName : connectionId AttributeType : S - AttributeName : roomId AttributeType : S KeySchema : - AttributeName : connectionId KeyType : HASH ProvisionedThroughput : ReadCapacityUnits : 1 WriteCapacityUnits : 1 GlobalSecondaryIndexes : - IndexName : roomId-index KeySchema : - AttributeName : roomId KeyType : HASH ProvisionedThroughput : ReadCapacityUnits : 1 WriteCapacityUnits : 1 Projection : ProjectionType : ALL SSESpecification : SSEEnabled : False DynamoDB WebSocketのconnectionIdとその接続しているクライアントが今入室している部屋情報を保持するために、connectionIdとroomIdのカラムを作成しています。 また、同じ部屋に入っているメンバーのレコードを取得するためにroomIdを使用してクエリを投げたいのですが、パーテションキー(connectionId)の指定をせずにクエリを投げることはできません。(パーテションキーを指定せずにクエリを投げると ValidationException: Query condition missed key schema element エラーになる) そこでroomIdに グローバルセカンダリインデックス を貼って検索できるようにしています。 SSESpecification.SSEEnabled は DynamoDBに保存されたデータの暗号化を有効にするかの設定 です。 有効にすると料金がかかるので、開発時には無効にしておくと良いでしょう。 API Gateway WebSocket API API GatewayのWebSocket APIでは serverless.yml の websocketsApiRouteSelectionExpression で指定された値( routeKey )がクライアントから渡されると、それに応じたルートと呼ばれるリソースタイプで処理が実行されます。 今回の実装だと、 $request.body.message の値によってルートが決定され、 $request.body.message が sendMessage だと sendMessageHandler の関数が実行されることになります。 ただし、API Gatewayで最初からルートに使用できる3つの特別なrouteKey値が存在します。 $connect: クライアントがWebSocket APIに最初に接続するときに使用される 接続開始したときに実行したい処理にルーティングできる 今回の実装だと connectHandler が実行される $disconnect: クライアントがWebSocket APIから切断するときに使用される 接続を切断したときに実行したい処理にルーティングできる 今回の実装だと disconnectHandler が実行される $default: websocketsApiRouteSelectionExpression の値が他のrouteKeyに一致しない場合に使用される 今回は使用しない( sendMessage 以外の $request.body.message は考慮しない) Lambda関数 今回実装している3つの関数の大枠は こちらのドキュメント を参考にしています。 ▶︎ webSocket/onConnect/app.js const AWS = require( 'aws-sdk' ) AWS.config.update( { region: process.env.AWS_REGION } ) const DDB = new AWS.DynamoDB( { apiVersion: '2012-10-08' } ) exports.handler = function ( event , context, callback) { let roomId = '' if ( event .queryStringParameters && event .queryStringParameters.roomId) { roomId = event .queryStringParameters.roomId } const putParams = { TableName: process.env.TABLE_NAME, Item: { connectionId: { S: event .requestContext.connectionId } , roomId: { S: roomId } } } DDB.putItem(putParams, function (err) { callback( null , { statusCode: err ? 500 : 200, body: err ? 'Failed to connect: ' + JSON.stringify(err) : 'Connected.' } ) } ) } 接続時にQuery ParameterでroomIdを渡してconnectionIdと共にDynamoDBに保持します。 これによってWebSocketの接続状態をDynamoDBに保持しつつ、接続しているクライアントがどの部屋に入っているかも参照できるようになります。 ▶︎ webSocket/sendMessage/app.js const AWS = require( 'aws-sdk' ) const DDB = new AWS.DynamoDB.DocumentClient( { apiVersion: '2012-08-10' } ) const { TABLE_NAME } = process.env exports.handler = async ( event , context) => { const roomId = JSON.parse( event .body).roomId // 自分が参加しているルームの参加者のレコードを取得 const queryParams = { TableName: TABLE_NAME, KeyConditionExpression: "#ROOMID = :ROOMID" , ExpressionAttributeNames: { "#ROOMID" : "roomId" } , ExpressionAttributeValues: { ":ROOMID" : roomId } , IndexName: 'roomId-index' } const connectionData = await DDB.query(queryParams).promise() const apigwManagementApi = new AWS.ApiGatewayManagementApi( { apiVersion: '2018-11-29' , endpoint: event .requestContext.domainName + '/' + event .requestContext.stage } ) const postData = JSON.parse( event .body).data const myConnectionId = event .requestContext.connectionId const postCalls = connectionData.Items.map(async ( { connectionId } ) => { try { // 送信者には送らない if (myConnectionId !== connectionId) { await apigwManagementApi.postToConnection( { ConnectionId: connectionId, Data: postData } ).promise() } } catch (e) { if (e.statusCode === 410) { console.log( `Found stale connection, deleting ${connectionId} ` ) await DDB. delete ( { TableName: TABLE_NAME, Key: { connectionId } } ).promise() } else { throw e } } } ) try { await Promise.all(postCalls) } catch (e) { return { statusCode: 500, body: e.stack } } return { statusCode: 200, body: 'Data sent.' } } グローバルセカンダリインデックスを貼ったroomIdで同じ部屋のクライアントのレコードを取得して、それらのconnectionIdに対して postToConnection でdataを送信します。 クライアントから送信されたdataを同じ部屋のクライアント全員に送信しています。 ▶︎ webSocket/onDisconnect/app.js const AWS = require( 'aws-sdk' ) AWS.config.update( { region: process.env.AWS_REGION } ) const DDB = new AWS.DynamoDB( { apiVersion: '2012-10-08' } ) exports.handler = function ( event , context, callback) { const deleteParams = { TableName: process.env.TABLE_NAME, Key: { connectionId: { S: event .requestContext.connectionId } } } DDB.deleteItem(deleteParams, function (err) { callback( null , { statusCode: err ? 500 : 200, body: err ? 'Failed to disconnect: ' + JSON.stringify(err) : 'Disconnected.' } ) } ) } 切断時にはDynamoDBから切断したクライアントのレコードを削除します。 クライアント WebSocketの接続にはJavaScriptのWebSocketのwrapperライブラリの Sockette を使用しています。 Socketteを使用することで再接続処理や、WebSocketの各種EventListenerで実行される関数が簡単に指定できます。 ▶︎ view/utils/webSocket.js import Sockette from 'sockette' export function newConnection( { roomId, onReceivedMessage } ) { return new Sockette( `wss://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev?roomId= ${roomId} ` , { timeout: 5e3, maxAttempts: 3, onmessage: (e) => onReceivedMessage(e), onerror: (e) => console.error(e), } ) } vueコンポーネントの実装は、実際に絵を描く部分は割愛しますが以下のようになります。 ▶︎ view/pages/room.vue <template> <div> <canvas ref= "canvas" ></canvas> </div> </template> <script> import { newConnection } from '~/utils/webSocket' export default { data() { return { ws: null , roomId: 'roomA' , // XXX : 実際は動的にする } } , mounted() { this .connectWs() } , beforeDestroy() { this .disconnectWs() } , methods: { connectWs() { this .ws = newConnection( { roomId: this .roomId, onReceivedMessage: this .onReceivedMessage, } ) } , disconnectWs() { if ( this .ws !== null ) { this .ws.close() } } , onReceivedMessage( event ) { // WebSocketでメッセージを受信したときに実行される const data = JSON.parse( event .data) switch (data.actionType) { // actionTypeによって処理を分岐。画面クリア、1つ戻る/進むといったeventを増やしたりする case 'DRAW' : this .draw(data.positions) break default : break } } , draw( { fromX, fromY, toX, toY } ) { // canvasにfromの座標からtoの座標に線を引く ... } , sendDrawMessage(positions) { this .sendMessage( { data: JSON.stringify( { actionType: 'DRAW' , positions, } ), } ) } , sendMessage( { data } ) { if ( this .ws !== null && this .roomId) { this .ws.json( { // WebSocketを介してobjを送信 message: 'sendMessage' , data, roomId: this .roomId, } ) } } , // タップ&ドラッグ時にthis.sendDrawMessage(positions)や自分のcanvasに対してthis.draw(positions)を行う処理 ... } , } </script> ... ページ表示後にWebSocketの接続を行い、接続した状態でcanvas上でタップ&ドラッグ操作をするとその座標をWebSocketを介して同じ部屋のクライアントに操作した内容を送信してcanvasの同期を行います。 送信するobjに actionType という値を持たせていますが、これは受信したメッセージの内容を識別するためのものです。 この値を変えることで、別のデータのやりとりとそれに応じた処理の分岐も簡単に行えます。 この記事では実装していませんが、例えばキャンバスクリアや描いた絵を1つ戻す(undo)/進める(redo)といったイベントの同期をできるようにすると、よりお絵かきチャットっぽくなるでしょう。 まとめ API Gateway + WebSocket APIでお絵かきチャットを作ることができました。 今回はcanvasの同期に利用しましたが、様々なリアルタイム通信が必要な場面で便利に使っていけそうな機能だと感じました。 明日の記事は id:pikkaman さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 16日目の記事です。 はじめに 動作環境 逆引き 条件を満たす最初の要素をとる 条件を満たす最後の要素をとる 条件を満たす要素より後ろの要素たちを抽出する 条件を満たす要素以降の要素たちを抽出する 条件を満たす要素より前の要素たちを抽出する 条件を満たす要素以前の要素たちを抽出する 先頭からいくつかの要素を抽出する 末尾からいくつかの要素を抽出する 1つしかない要素のみを抽出する 条件を満たす要素を検索する 条件を満たす要素数を求める 条件を満たさない要素数を求める 条件を満たす要素のインデックスを求める 条件を満たす最初の要素のインデックスを求める 条件を満たす最後の要素のインデックスを求める 条件を満たすただ一つの要素のインデックスを求める 最初にCODE BLOCKが正常終了する要素の結果を求める 最後にCODE BLOCKが正常終了する要素の結果を求める 一つだけCODE BLOCKが正常終了する要素の結果を求める 要素の最大値を求める 文字列の最大を求める 要素の最小値を求める 文字列の最小を求める 最小値と最大値を同時に求める リストを文字列順で並び替える リストを数値順で並び替える 文字列を結合する 要素の合計を求める 要素の積を求める 各要素のうちどれかが条件を満たすならtrue 各要素のうちどれかが条件を満たさないならtrue 全要素が条件を満たすならtrue 全要素が条件を満たさないならtrue 要素がただ1つだけ条件を満たすならtrue それぞれの要素に処理を行いたい 要素をランダムに並び替える 最頻値を求める 重複を弾く 特定の要素の後ろに要素を追加する 2つのリストを同時に操作する 複数のリストを1つのリストにする 1つのリストを複数のリストに仕分ける 各要素を複数のリストに仕分ける 条件ごとの要素数を求める リストからイテレータを作成する リストから複数をまとめて返すイテレータを作成する key-valueリスト操作 key-valueリストをまとめて1要素にする key, valueのまとまりを展開する key-valueリストのkeyのみを抽出する key-valueリストのvalueのみを抽出する key-valueリストから条件にあう全要素を抽出する 条件にあったペア数を出す 条件にあう最初の要素を抽出する 条件にあう要素が見つかったかを調べる key-valueリストにmapと同じことをしたい まとめ はじめに こんにちは、エンジニアの id:Dozi0116 です。 自分は4月に入社してから現在まででいろいろなPerlのコードに触れられたのですが、その中で List::AllUtils というモジュールが印象に残っています。 List::AllUtilsとは、「リスト操作のいろいろが詰まったモジュール」を集めて使えるようにしたモジュールで、 List::Util List::SomeUtils List::UtilsBy の3つのモジュールが一体化したモジュールです。 実際の開発でも使っていますが、たくさんの関数が詰まっているため、どんなことができるのか?を全く理解しきれていません。また、既に書かれているソースコードをすぐにList::AllUtilsのものと理解ができず、新しい関数を見かけるたびにこんなことまでできるのか!と驚いていました。 せっかくこんな便利モジュールを使っているのに使いこなせないのはもったいない… そこで、List::AllUtilsができることを理解するため、やりたいことから使うべきコードを見つけやすくなるように、そして他に同じように困っている人のハードルを下げるため、この逆引きを作りました。 動作環境 Perl 5.30.2 List::AllUtils 0.18 List::SomeUtils 0.56 List::Util 1.45 List::UtilsBy 0.11 逆引き 今回載せている例は このテストコード で検証しています。 今回の逆引きでは、 reduce などの発想次第でなんでもできそうな関数は、既にある関数と差別化ができる場合にのみ載せています。ご了承ください。 また、List::AllUtilsの依存バージョン関係上、使えない関数がいくつかあります。 条件を満たす最初の要素をとる @list = ( 4 , 7 , 1 ); $result = first { $_ > 5 } @list ; # 7 $result = first_value { $_ > 5 } @list ; # 7 $result = firstval { $_ > 5 } @list ; # first_valueのエイリアス extract_first_by は条件を満たした要素をオリジナルのリストから消す @list = ( 4 , 7 , 1 ); $result = extract_first_by { $_ > 5 } @list ; # 7 print ( @list ); # (4, 1); reduce で書くこともできる @list = ( 4 , 7 , 1 ); $result = reduce { defined ( $a ) ? $a : $b > 5 ? $b : undef } undef , @list ; 条件を満たす最後の要素をとる @list = ( 4 , 7 , 1 ); $result = last_value { $_ < 5 } @list ; # 1 $result = lastval { $_ < 5 } @list ; # 同じ(last_valueのエイリアス) 条件を満たす要素より後ろの要素たちを抽出する @list = ( 2 , 4 , 6 , 8 , 10 ); @result = after { $_ > 5 } @list ; # (8, 10) 条件を満たす要素以降の要素たちを抽出する @list = ( 2 , 4 , 6 , 8 , 10 ); @result = after_incl { $_ > 5 } @list ; # (6, 8, 10) 条件を満たす要素より前の要素たちを抽出する @list = ( 2 , 4 , 6 , 8 , 10 ); @result = before { $_ > 5 } @list ; # (2, 4) 条件を満たす要素以前の要素たちを抽出する @list = ( 2 , 4 , 6 , 8 , 10 ); @result = before_incl { $_ > 5 } @list ; # (2, 4, 6) 先頭からいくつかの要素を抽出する @list = 1..10 ; @result = head 3 , @list ; # (1, 2, 3) @result = head - 2 , @list ; # (1, 2, 3, 4, 5, 6, 7, 8) 末尾からいくつかの要素を抽出する @list = 1..10 ; @result = tail 3 , @list ; # (8, 9, 10) @result = tail - 2 , @list ; # (3, 4, 5, 6, 7, 8, 9, 10) 1つしかない要素のみを抽出する @list = ( 1 , 1 , 1 , 2 , 3 , 3 , 4 , 5 ); @result = singleton @list ; # (2, 4, 5) 条件を満たす要素を検索する @list = 1. . .10 ; @result = grep { $_ == 4 } @list ; # (4) 二分探査をするため、 CODE BLOCKは 比較した要素が小さいなら-1を、大きいなら1を、ちょうどなら0を返す必要が、また @list はソートされている必要がある @sorted_list = 1. . .10 ; @result = bsearch { $_ <=> 4 } @sorted_list ; # (4) extract_by は見つけた要素を result に抜き出して、オリジナルから消える @list = 1. . .10 ; @result = extract_by { $_ == 4 } @list ; # (4) print ( @list ); # (1, 2, 3, 5, 6, 7, 8, 9, 10) 条件を満たす要素数を求める @list = 1. . .10 ; $result = true { $_ < 4 } @list ; # 3 条件を満たさない要素数を求める @list = 1. . .10 ; $result = false { $_ < 4 } @list ; # 6 条件を満たす要素のインデックスを求める 二分探査をするため、 CODE BLOCKは 比較した要素が小さいなら-1を、大きいなら1を、ちょうどなら0を返す必要が、また @list はソートされている必要がある @sorted_list = 1. . .10 ; $result = bsearch_index { $_ <=> 4 } @sorted_list ; # 3 $result_b = bsearchidx { $_ <=> 4 } @sorted_list ; # 同じ(bsearch_indexのエイリアス) 複数のインデックスをまとめて求めるなら indexes を使う @list = ( 1 , 1 , 1 , 2 , 4 , 1 ); @result = indexes { $_ == 1 } @list ; # (0, 1, 2, 5) 条件を満たす最初の要素のインデックスを求める @list = ( 1 , 1 , 1 , 2 , 4 , 1 ); $result = first_index { $_ == 1 } @list ; # 0 $result = firstidx { $_ == 1 } @list ; # 同じ(first_indexのエイリアス) 条件を満たす最後の要素のインデックスを求める @list = ( 1 , 1 , 1 , 2 , 4 , 1 ); $result = last_index { $_ == 1 } @list ; # 5 $result = lastidx { $_ == 1 } @list ; # 同じ(last_indexのエイリアス) 条件を満たすただ一つの要素のインデックスを求める 要素が複数あった場合、 -1 が返る @list = ( 1 , 1 , 1 , 2 , 4 , 1 ); $result = only_index { $_ == 2 } @list ; # 3 $result = onlyidx { $_ == 2 } @list ; # 同じ(only_indexのエイリアス) $result = only_index { $_ == 1 } @list ; # -1 最初にCODE BLOCKが正常終了する要素の結果を求める @list = ( 4 , 7 , 1 ); $result = first_result { $_ ** 2 if $_ > 3 } @list ; # 16 $result = firstres { $_ ** 2 if $_ > 3 } @list ; # 同じ(first_indexのエイリアス) 最後にCODE BLOCKが正常終了する要素の結果を求める @list = ( 4 , 7 , 1 ); $result = last_result { $_ ** 2 if $_ > 3 } @list ; # 49 $result = lastres { $_ ** 2 if $_ > 3 } @list ; # 同じ(last_indexのエイリアス) 一つだけCODE BLOCKが正常終了する要素の結果を求める @list = ( 4 , 7 , 1 ); $result = only_result { $_ ** 2 if $_ > 5 } @list ; # 49 $result = onlyres { $_ ** 2 if $_ > 5 } @list ; # 同じ(last_indexのエイリアス) # 正常終了する要素が複数ある場合、undefを返す $result = only_result { $_ ** 2 if $_ > 3 } @list ; # undef 要素の最大値を求める @list = ( 1 , 4 , 3 ); $result = max @list ; # 4 max_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる @list = ({ value => 2 , id => 1 }, { value => 5 , id => 2 }, { value => 5 , id => 3 }); $result = max_by { $_->{ value } } @list ; # { value => 5, id => 2 } リストコンテキストを返り値に期待すれば、全部の要素を取得できる @list = ({ value => 2 , id => 1 }, { value => 5 , id => 2 }, { value => 5 , id => 3 }); @result = max_by { $_->{ value } } @list ; # ({ value => 5, id => 2 }, { value => 5, id => 3 }) 文字列の最大を求める ここでいう文字列の最大とは、文字コード比較での最大を指す @list = qw/a b c/ ; $result = maxstr @list ; # c @list = ({ name => 'a' }, { name => 'b' }, { name => 'c' }); $result = reduce { $a->{ name } gt $b->{ name } ? $a : $b } @list ; # { name => 'c' } 要素の最小値を求める @list = ( 2 , 3 , 1 ); $result = min @list ; # 1 min_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる @list = ({ value => 2 , id => 1 }, { value => 1 , id => 2 }, { value => 1 , id => 3 }); $result = min_by { $_->{ value } } @list ; # { value => 1, id => 2 } リストコンテキストを返り値に期待すれば、全部の要素を取得できる @list = ({ value => 2 , id => 1 }, { value => 1 , id => 2 }, { value => 1 , id => 3 }); @result = min_by { $_->{ value } } @list ; # ({ value => 1, id => 2 }, { value => 1, id => 3 }) 文字列の最小を求める ここでいう文字列の最小とは、文字コード比較での最小を指す @list = qw/b c a/ ; $result = minstr @list ; # a @list = ({ name => 'b' }, { name => 'a' }, { name => 'c' }); $result = reduce { $a->{ name } lt $b->{ name } ? $a : $b } @list ; # { name => 'a' } 最小値と最大値を同時に求める @list = ( 2 , 3 , 1 ); ( $min , $max ) = minmax @list ; # (1, 3) minmax_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる @list = ({ value => 2 , id => 1 }, { value => 1 , id => 2 }, { value => 1 , id => 3 }); ( $min , $max ) = minmax_by { $_->{ value } } @list ; # ({ value => 1, id => 2 }, { value => 2, id => 1 }) リストを文字列順で並び替える @list = ( 'banana' , 'melon' , 'apple' ); @result = sort @list ; # ('apple', 'banana', 'melon') 比較するものが組み込み関数の sort と違って省略できるため、 sort_by を使えば比較的簡潔に書くことができる @list = ({ name => 'banana' }, { name => 'melon' }, { name => 'apple' }); @result = sort_by { $_->{ name } } @list ; # ({ name => 'apple' }, { name => 'banana' }, { name => 'melon' }) sort_by を降順で使いたい時は rev_sort_by が使える @list = ({ name => 'banana' }, { name => 'melon' }, { name => 'apple' }); @result = rev_sort_by { $_->{ name } } @list ; # ({ name => 'melon' }, { name => 'banana' }, { name => 'apple' }) リストを数値順で並び替える @list = ( 23 , 1 , 12 ); @result = sort { $a <=> $b } @list ; # (1, 12, 23) 比較するものが組み込み関数の sort と違って省略できるため、 nsort_by を使えば比較的簡潔に書くことができる @list = ({ value => 23 }, { value => 1 }, { value => 12 }); @result = nsort_by { $_->{ value } } @list ; # ({ value => 1 }, { value => 12 }, { value => 23 }) nsort_by を降順で使いたい時は rev_nsort_by が使える @list = ({ value => 23 }, { value => 1 }, { value => 12 }); @result = rev_nsort_by { $_->{ value } } @list ; # ({ value => 23 }, { value => 12 }, { value => 1 }) 文字列を結合する @list = qw/a b c/ ; $result = join '' , @list ; # "abc" reduce で書けば、CODE BLOCKは自由に記述できるので器用なことができる @list = qw/a b c/ ; $result = reduce { uc ( $a ) . uc ( $b ) } @list ; # "ABC" 要素の合計を求める @list = 1..10 ; $result = sum @list ; # 55 # 要素がない時はundefを返す @list = (); $result = sum @list ; # undef sum0 を用いると要素0のリストの場合は0を返す @list = (); $result = sum0 @list ; # 0 @list = ({ value => 2 }, { value => 4 }, { value => 1 }); $result = reduce { $a + $b->{ value } } 0 , @list ; # 7 要素の積を求める @list = 1..10 ; $result = product @list ; # 3628800 # 要素がない時は1を返す @list = (); $result = product @list ; # 1 @list = ({ value => 2 }, { value => 4 }, { value => 1 }); $result = reduce { $a * $b->{ value } } 1 , @list ; # 8 各要素のうちどれかが条件を満たすならtrue @list = ({ flag => 1 }, { flag => 0 }, { flag => 1 }); $result = any { $_->{ flag } } @list ; # 1 # 空リストの場合、falseを返す @list = (); $result = any { $_->{ flag } } @list ; # "" any_u は空リストの場合に undef を返す @list = (); $result = any { $_->{ flag } } @list ; # "" $result = any_u { $_->{ flag } } @list ; # undef 各要素のうちどれかが条件を満たさないならtrue @list = ({ flag => 1 }, { flag => 0 }, { flag => 1 }); $result = notall { $_->{ flag } } @list ; # 1 # 空リストの場合、falseを返す @list = (); $result = notall { $_->{ flag } } @list ; # "" notall_u は空リストの場合に undef を返す @list = (); $result = notall { $_->{ flag } } @list ; # "" $result = notall_u { $_->{ flag } } @list ; # undef 全要素が条件を満たすならtrue @list = ({ flag => 1 }, { flag => 1 }, { flag => 1 }); $result = all { $_->{ flag } } @list ; # 1 # 空リストの場合、trueを返す @list = (); $result = all { $_->{ flag } } @list ; # 1 all_u は空リストの場合に undef を返す @list = (); $result = all { $_->{ flag } } @list ; # 1 $result = all_u { $_->{ flag } } @list ; # undef 全要素が条件を満たさないならtrue @list = ({ flag => 0 }, { flag => 0 }, { flag => 0 }); $result = none { $_->{ flag } } @list ; # 1 # 空リストの場合、trueを返す @list = (); $result = none { $_->{ flag } } @list ; # 1 none_u は空リストの場合に undef を返す @list = (); $result = none { $_->{ flag } } @list ; # 1 $result = none_u { $_->{ flag } } @list ; # undef 要素がただ1つだけ条件を満たすならtrue @list = ({ flag => 0 }, { flag => 1 }, { flag => 0 }); $result = one { $_->{ flag } } @list ; # 1 # 空リストの場合、falseを返す @list = (); $result = one { $_->{ flag } } @list ; # "" one_u は空リストの場合に undef を返す @list = (); $result = one { $_->{ flag } } @list ; # "" $result = one_u { $_->{ flag } } @list ; # undef それぞれの要素に処理を行いたい @list = ( 4 , 7 , 1 ); @result = map { $_ *= 2 } @list ; # (8, 14, 2) print ( @list ); # (8, 14, 2); apply で書けば、元のリストは変更されない @list = ( 4 , 7 , 1 ); @result = apply { $_ *= 2 } @list ; # (8, 14, 2) print ( @result ); # (4, 7, 1) bundle_by で書けば、複数の要素をまとめて処理できる @list = ( 1. . .8 ); @result = bundle_by { [ $_[ 0 ] , $_[ 1 ] , $_[ 2 ] ] } 3 , @list ; # ([1, 2, 3], [4, 5, 6], [7, 8, undef]) 要素をランダムに並び替える @list = ( 'a' , 'b' , 'c' , 'd' ); @result = shuffle @list ; # 何が出るかは神のみぞ知る weighted_shuffle_by で書けば、重みをつけたランダムになる @list = ( 'a' , 'b' , 'c' ); @result = weighted_shuffle_by { { a => 1 , b => 0 , c => 99 }->{ $_ } } @list ; # ほぼほぼ ('c', 'a', 'b') 最頻値を求める @list = ( 'apple' , 'pineapple' , 'apple' , 'banana' , 'apple' , 'apple' , 'apple' ); @result = mode @list ; # ('apple') 重複を弾く 後に出てきた重複要素が消される @list = ( 'hoge' , 'hoge' , 22 , 35 , 10 , 22 ); @result = uniq @list ; # ('hoge', 22, 35, 10) @result = distinct @list ; # 同じ(uniqのエイリアス) 要素が数値or文字列で一定なら以下の関数も使える @list = ( 1 , 2 , 3 , 1 , 5 ); @result = uniqnum @list ; # (1, 2, 3, 5) @list = ( 'a' , 'A' , 'aa' , 'a' ); @result = uniqstr @list ; # ('a', 'A', 'aa') uniq_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる @list = ({ value => 2 , id => 1 }, { value => 1 , id => 2 }, { value => 1 , id => 3 }); @result = uniq_by { $_->{ value } } @list ; # ({ value => 2, id => 1 }, { value => 1, id => 2 }) 特定の要素の後ろに要素を追加する @list = ( 1 , 2 , 3 , 5 ); my $result = insert_after { $_ == 3 } 4 , @list ; @list ; # (1,2,3,4,5) stringの等価比較をするなら、 insert_after_string が使える @list = ( 'first' , 'second' , 'third' , 'fifth' ); my $result = insert_after_string 'third' , 'fourth' , @list ; @list ; # ('first', 'second', 'third', 'fourth', 'fifth') 2つのリストを同時に操作する @list_a = ( 'a' , 'b' , 'c' ); @list_b = ( 1 , 2 , 3 ); @result = pairwise { { str => $a , num => $b } } @list_a , @list_b ; # ( { str => 'a', num => 1 }, { str => 'b', num => 2 }, { str => 'c', num => 3 }, ) 複数のリストを1つのリストにする @list_a = ( 'a' , 'b' , 'c' ); @list_b = ( 1 , 2 ); @result = mesh @list_a , @list_b ; # ('a', 1, 'b', 2, 'c', undef) @result = zip @list_a , @list_b ; # 同じ(meshのエイリアス) zip_by で書けば、CODE BLOCKは自由に記述できるので器用なことができる @list_a = ( 'a' , 'b' , 'c' ); @list_b = ( 1 , 2 ); @result = zip_by { $_[ 0 ] , $_[ 1 ] } \ @list_a , \ @list_b ; # ('a', 1, 'b', 2, 'c', undef) 1つのリストを複数のリストに仕分ける # CODE BLOCK は 仕分け先のインデックスを期待している @list = ( 1 , 2 , 1 , 1 , 2 ); @result = part { $_ } @list ; # ( undef, [1, 1, 1], [2, 2] ) partition_by を使うと、仕分ける時の値をkeyとしたハッシュで返してくれる @list = ( 1 , 2 , 1 , 1 , 2 ); %result = partition_by { $_ } @list ; # { 1 => [ 1, 1, 1 ], 2 => [ 2, 2] } 各要素を複数のリストに仕分ける @list = ({ id => 1 , name => 'hoge' , }, { id => 2 , name => 'fuga' }, { id => 3 , name => 'piyo' }); ( $ids , $names ) = unzip_by { $_->{ id } , $_->{ name } } @list ; # ids: (1, 2, 3), names: ('hoge', 'fuga', 'piyo') 条件ごとの要素数を求める @list = ( 1 , 2 , 1 , 1 , 2 ); %result = count_by { $_ } @list ; # { 1 => 3, 2 => 2 } リストからイテレータを作成する @list = ( 1 , 2 , 3 ); $it = each_array( @list ); $it ->(); # 1 $it ->(); # 2 $it ->(); # 3 $it ->(); # undef リファレンスから作成するなら each_arrayref が使える $list = [ 1 , 2 , 3 ]; $it = each_array( $list ); $it ->(); # 1 $it ->(); # 2 $it ->(); # 3 $it ->(); # undef リストから複数をまとめて返すイテレータを作成する @list = ( 1. . .8 ); $it = natatime 3 , @list ; $it ->(); # (1, 2, 3) $it ->(); # (4, 5, 6) $it ->(); # (7, 8) $it ->(); # undef key-valueリスト操作 ここでの key-valueリスト とはリストの要素が (key1, value1, key2, value2, ...) となっているもの。 @kvlist = ( 'jp' , 'こんにちは' , 'en' , 'hello' ); %hash = @kvlist ; $hash{ jp } ; # こんにちは key-valueリストをまとめて1要素にする @list = ( 'k1' , 'v1' , 'k2' , 'v2' ); @result = pairs @list ; # ( ['k1', 'v1'], ['k2', 'v2'] ) key, valueのまとまりを展開する @list = ( [ 'k1' , 'v1' ], [ 'k2' , 'v2' ] ); @result = unpairs @list ; # ('k1', 'v1', 'k2', 'v2') key-valueリストのkeyのみを抽出する @list = ( 'k1' , 'v1' , 'k2' , 'v2' ); @result = pairkeys @list ; # ( 'k1', 'k2' ) key-valueリストのvalueのみを抽出する @list = ( 'k1' , 'v1' , 'k2' , 'v2' ); @result = pairvalues @list ; # ( 'v1', 'v2' ) key-valueリストから条件にあう全要素を抽出する @list = ( 'k1' , 'v1' , 'k2' , 'v2' , 'k3' , 'v1' ); @result = pairgrep { $b eq 'v1' } @list ; # ('k1', 'v1', 'k3', 'v1') 条件にあったペア数を出す 2つ1セットで見るため、最大値は要素の半分になることに注意 @list = ( 'k1' , 'v1' , 'k2' , 'v2' , 'k3' , 'v1' ); $result = pairgrep { $a eq 'k1' && $b eq 'v1' } @list ; # 1 条件にあう最初の要素を抽出する @list = ( 'k1' , 'v1' , 'k2' , 'v2' , 'k3' , 'v1' ); ( $key , $value ) = pairfirst { $b eq 'v1' } @list ; # $key = 'k1', $value = 'v1' 条件にあう要素が見つかったかを調べる @list = ( 'k1' , 'v1' , 'k2' , 'v2' , 'k3' , 'v1' ); $result = pairfirst { $b eq 'v1' } @list ; # 1 key-valueリストにmapと同じことをしたい @list = ( 'k1' , 'v1' , 'k2' , 'v2' ); @result = pairmap { " $a - $b " } @list ; # ( 'k1-v1', 'k2-v2' ) まとめ PerlのモジュールであるList::AllUtilsの逆引きを作りました。 もちろんこれが正解というわけではなく、いろいろな書き方があるので、この記事を読んだ方も書き方や活用例があれば教えてください。 自分みたいに全容を理解できていない人の助けになったら嬉しいです。 最後になりますが、この記事に書くにあたって協力してくれた社員のみなさん、ありがとうございました! 明日の記事は id:summer_gift さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 15日目の記事です。 エンジニアの yokoi0803 です。DB設計をしていて多対一のリレーションを見たり、設計したりする機会が何度かあって、その度にどう設計するかで悩んでます。 多対一のリレーションはいくつかの設計で実現できますが、その選定の際の指標を得るため、今回はパフォーマンスの観点から設計の比較をしてみたいと思います。 多対一のリレーションを表現する3つの設計 あるテーブルが複数のテーブルに対して多対一で紐付くケースについて、ここではくじ引きとその景品を表現するためのDB設計を想定します。 箱の中のくじそれぞれに景品が設定されており、景品には旅行券や果物など、様々な種類のものがあります。 こういった仕様の設計手法にはいくつか種類がありますが、今回は「ポリモーフィック関連」、「交差テーブルを用いた設計」、「親テーブルを用いた設計」の3種類について取り上げることにします。 まずはそれぞれどのような設計なのか、簡単に紹介していきます。 ポリモーフィック関連 どのテーブルのどのレコードに紐付くのか、という情報をテーブルに持たせる設計です。 prizes テーブルの target_type が 「どのテーブルに紐付くか」、 target_id が「どのレコードに紐付くか」の情報を示します。 ポリモーフィック関連はSQLアンチパターンでも取り上げられているように、外部キー制約をつけることができないために紐付く対象のテーブルが保証されず、理由がない限り推奨される設計ではありません。 交差テーブルを用いた設計 紐付き先の種類ごとに関連情報だけを持たせたテーブル(交差テーブル)を用意する設計です。 prizes とその紐付き先である tickets 、 fruits との間に、関連を示す交差テーブルがそれぞれ存在します。 親テーブルを用いた設計 紐付き元と紐付き先の全てのテーブルに共通の親テーブルを用意する設計です。 今回のくじ引きのケースでは、くじ引きのくじそのものを表す balls というテーブルを親として、全てのテーブルが紐付くように設計してみました。 パフォーマンスの比較 同じ仕様を実現する3種類の設計がありますが、どれを選択すれば良いでしょうか。 「良い設計」についてはよく言及されていると思いますが、今回はそれについては考えず、パフォーマンスの観点から3種類の設計を比較してみたいと思います。 準備 今回は上の説明で取り上げた、くじ引きとその景品についてのDB設計をそのまま題材とします。 「ポリモーフィック関連」、「交差テーブルを用いた設計」、「親テーブルを用いた設計」の3種類で設計し、「紐付き先のデータ量の変化」および「紐付き先の種類数の変化」に対して検索パフォーマンスがどのように変化するかを確認します。 具体的には紐付き元である prizes テーブルから1,000件と、その紐付き先のレコードから情報を取得するまでの実行時間を計測しました。計測は Benchmark で10,000回試行し、結果としています。 マシンスペックについて 項目 値 OS Ubuntu 16.04.7 LTS CPU Intel(R) Xeon(R) CPU E3-1220L V2 @ 2.30GHz ×4 メモリ 16GB DB管理システムについて MySQL5.6を利用している 計測時に発行されるクエリの全てにINDEXが使用されるように設定 クエリの実行計画を表示する ポリモーフィック関連 EXPLAIN SELECT * FROM prizes LEFT JOIN fruits ON prizes.target_type = ' fruits ' AND prizes.target_id = fruits.id LEFT JOIN tickets ON prizes.target_type = ' tickets ' AND prizes.target_id = tickets.id WHERE prizes.id IN (:prize_ids); + ----+-------------+---------+--------+---------------+---------+---------+----------------------------------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + ----+-------------+---------+--------+---------------+---------+---------+----------------------------------------------+------+-------------+ | 1 | SIMPLE | prizes | range | PRIMARY | PRIMARY | 4 | NULL | 9 | Using where | | 1 | SIMPLE | fruits | eq_ref | PRIMARY | PRIMARY | 4 | polymorphic_relation.prizes.target_id | 1 | Using where | | 1 | SIMPLE | tickets | eq_ref | PRIMARY | PRIMARY | 4 | polymorphic_relation.prizes.target_id | 1 | Using where | + ----+-------------+---------+--------+---------------+---------+---------+----------------------------------------------+------+-------------+ 交差テーブルを用いた設計 EXPLAIN SELECT * FROM prizes LEFT JOIN prizes_fruits ON prizes.id = prizes_fruits.prize_id LEFT JOIN fruits ON prizes_fruits.fruit_id = fruits.id LEFT JOIN prizes_tickets ON prizes.id = prizes_tickets.prize_id LEFT JOIN tickets ON prizes_tickets.ticket_id = tickets.id WHERE prizes.id IN (:prizes_ids); + ----+-------------+----------------+--------+---------------+---------+---------+-----------------------------------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + ----+-------------+----------------+--------+---------------+---------+---------+-----------------------------------------------+------+-------------+ | 1 | SIMPLE | prizes | range | PRIMARY | PRIMARY | 4 | NULL | 9 | Using where | | 1 | SIMPLE | prizes_fruits | eq_ref | PRIMARY | PRIMARY | 4 | cross_table_relation.prizes.id | 1 | NULL | | 1 | SIMPLE | fruits | eq_ref | PRIMARY | PRIMARY | 4 | cross_table_relation.prizes_fruits.fruit_id | 1 | NULL | | 1 | SIMPLE | prizes_tickets | eq_ref | PRIMARY | PRIMARY | 4 | cross_table_relation.prizes.id | 1 | NULL | | 1 | SIMPLE | tickets | eq_ref | PRIMARY | PRIMARY | 4 | cross_table_relation.prizes_tickets.ticket_id | 1 | NULL | + ----+-------------+----------------+--------+---------------+---------+---------+-----------------------------------------------+------+-------------+ 親テーブルを用いた設計 EXPLAIN SELECT * FROM prizes LEFT JOIN tickets ON prizes.ball_id = tickets.ball_id LEFT JOIN fruits ON prizes.ball_id = fruits.ball_id WHERE prizes.id IN (:prize_ids); + ----+-------------+---------+-------+---------------+----------+---------+--------------------------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + ----+-------------+---------+-------+---------------+----------+---------+--------------------------------------+------+-------------+ | 1 | SIMPLE | prizes | range | PRIMARY | PRIMARY | 4 | NULL | 9 | Using where | | 1 | SIMPLE | tickets | ref | ball_idx | ball_idx | 4 | parent_table_relation.prizes.ball_id | 1 | NULL | | 1 | SIMPLE | fruits | ref | ball_idx | ball_idx | 4 | parent_table_relation.prizes.ball_id | 1 | NULL | + ----+-------------+---------+-------+---------------+----------+---------+--------------------------------------+------+-------------+ 設定ファイル (my.cnf) を表示する [ mysqld ] character-set-server = utf8 expire_logs_days = 1 max_binlog_size =300M skip-name-resolve wait_timeout = 10 log_error = /var/log/mysql/error.log slow_query_log slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 0 . 1 enforce_gtid_consistency sql_mode = TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY binlog_format = ROW binlog_row_image =minimal query_cache_size = 0 query_cache_type = 0 #------------------------------------------------ ## InnoDB #------------------------------------------------ innodb_file_per_table innodb_log_file_size = 2G innodb_flush_method = O_DIRECT innodb_open_files = 2000 #メモリ使用抑制 table_definition_cache = 400 [ mysqld_safe ] open_files_limit = 65535 スキーマ定義について それぞれの設計でのスキーマ定義を掲載します。 tickets 関係のテーブルは fruits 関係のテーブルと同様になるので省略しています。 ポリモーフィック関連 [root@localhost] polymorphic_relation> show create table prizes\G *************************** 1. row *************************** Table: prizes Create Table: CREATE TABLE `prizes` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `rank` int(10) unsigned NOT NULL, `target_id` int(10) unsigned NOT NULL, `target_type` varchar(32) NOT NULL, PRIMARY KEY (`id`), KEY `target_type_target_id_idx` (`target_type`,`target_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) [root@localhost] polymorphic_relation> show create table fruits\G *************************** 1. row *************************** Table: fruits Create Table: CREATE TABLE `fruits` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) 交差テーブルを用いた設計 [root@localhost] cross_table_relation> show create table prizes\G *************************** 1. row *************************** Table: prizes Create Table: CREATE TABLE `prizes` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `rank` int(10) unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 1 row in set (0.00 sec) [root@localhost] cross_table_relation> show create table prizes_fruits\G *************************** 1. row *************************** Table: prizes_fruits Create Table: CREATE TABLE `prizes_fruits` ( `prize_id` int(10) unsigned NOT NULL, `fruit_id` int(10) unsigned NOT NULL, PRIMARY KEY (`prize_id`), KEY `fruit_idx` (`fruit_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 1 row in set (0.00 sec) [root@localhost] cross_table_relation> show create table fruits\G *************************** 1. row *************************** Table: fruits Create Table: CREATE TABLE `fruits` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 1 row in set (0.00 sec) 親テーブルを用いた設計 [root@localhost] parent_table_relation> show create table prizes\G *************************** 1. row *************************** Table: prizes Create Table: CREATE TABLE `prizes` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `ball_id` int(10) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `ball_idx` (`ball_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 1 row in set (0.00 sec) [root@localhost] parent_table_relation> show create table balls\G *************************** 1. row *************************** Table: balls Create Table: CREATE TABLE `balls` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `color` varchar(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 1 row in set (0.00 sec) [root@localhost] parent_table_relation> show create table fruits\G *************************** 1. row *************************** Table: fruits Create Table: CREATE TABLE `fruits` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `ball_id` int(10) unsigned NOT NULL, `name` varchar(32) NOT NULL, PRIMARY KEY (`id`), KEY `ball_idx` (`ball_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 1 row in set (0.00 sec) 計測方法について 計測で用いたコードを掲載します。 use strict ; use warnings ; use utf8 ; use DBI; use Benchmark qw/timethese cmpthese/ ; use constant +{ PRIZE_COUNT => 1000 , ITERATION => 10000 , }; my $user = "root" ; my $pass = "" ; my $result = timethese(ITERATION, +{ polymorphic => sub { my $dbh = DBI-> connect ( "dbi:mysql:database=polymorphic_relation;host=Localhost;port=3306" , $user , $pass , +{ RootClass => 'DBIx::Sunny' }, ) || die $ DBI:: errstr ; my $sql = <<EOT ; SELECT * FROM prizes LEFT JOIN fruits ON prizes.target_type = 'fruits' AND prizes.target_id = fruits.id LEFT JOIN tickets ON prizes.target_type = 'tickets' AND prizes.target_id = tickets.id WHERE prizes.id IN (:prize_ids); EOT $dbh->select_all ( $sql , +{ prize_ids => [ 1. .PRIZE_COUNT] }); }, cross_table => sub { my $dbh = DBI-> connect ( "dbi:mysql:database=cross_table_relation;host=Localhost;port=3306" , $user , $pass , +{ RootClass => 'DBIx::Sunny' }, ) || die $ DBI:: errstr ; my $sql = <<EOT ; SELECT * FROM prizes LEFT JOIN prizes_fruits ON prizes.id = prizes_fruits.prize_id LEFT JOIN fruits ON prizes_fruits.fruit_id = fruits.id LEFT JOIN prizes_tickets ON prizes.id = prizes_tickets.prize_id LEFT JOIN tickets ON prizes_tickets.ticket_id = tickets.id WHERE prizes.id IN (:prize_ids); EOT $dbh->select_all ( $sql , +{ prize_ids => [ 1. .PRIZE_COUNT] }); }, parent_table => sub { my $dbh = DBI-> connect ( "dbi:mysql:database=parent_table_relation;host=Localhost;port=3306" , $user , $pass , +{ RootClass => 'DBIx::Sunny' }, ) || die $ DBI:: errstr ; my $sql = <<EOT ; SELECT * FROM prizes LEFT JOIN tickets ON prizes.ball_id = tickets.ball_id LEFT JOIN fruits ON prizes.ball_id = fruits.ball_id WHERE prizes.id IN (:prize_ids); EOT $dbh->select_all ( $sql , +{ prize_ids => [ 1. .PRIZE_COUNT] }); }, }); cmpthese $result ; 計測 紐付き先のデータ量によるパフォーマンス変化の比較 紐付き先のテーブルのレコード数が変化することで、パフォーマンスがどのように変化するか計測します。 紐付き先の種類は全て2つとし、レコード数はそれぞれ10件、1,000件、100,000件と変化させました。 結果を以下に示します。 レコード数10件 Benchmark: timing 10000 iterations of cross_table, parent_table, polymorphic... cross_table: 87 wallclock secs (36.01 usr + 2.82 sys = 38.83 CPU) @ 257.53/s (n=10000) parent_table: 81 wallclock secs (31.21 usr + 1.76 sys = 32.97 CPU) @ 303.31/s (n=10000) polymorphic: 73 wallclock secs (35.58 usr + 2.55 sys = 38.13 CPU) @ 262.26/s (n=10000) Rate cross_table polymorphic parent_table cross_table 258/s -- -2% -15% polymorphic 262/s 2% -- -14% parent_table 303/s 18% 16% -- レコード数1,000件 Benchmark: timing 10000 iterations of cross_table, parent_table, polymorphic... cross_table: 90 wallclock secs (36.54 usr + 2.41 sys = 38.95 CPU) @ 256.74/s (n=10000) parent_table: 88 wallclock secs (30.96 usr + 2.88 sys = 33.84 CPU) @ 295.51/s (n=10000) polymorphic: 79 wallclock secs (36.63 usr + 1.97 sys = 38.60 CPU) @ 259.07/s (n=10000) Rate cross_table polymorphic parent_table cross_table 257/s -- -1% -13% polymorphic 259/s 1% -- -12% parent_table 296/s 15% 14% -- レコード数100,000件 Benchmark: timing 10000 iterations of cross_table, parent_table, polymorphic... cross_table: 91 wallclock secs (36.68 usr + 2.29 sys = 38.97 CPU) @ 256.61/s (n=10000) parent_table: 101 wallclock secs (31.46 usr + 2.33 sys = 33.79 CPU) @ 295.95/s (n=10000) polymorphic: 79 wallclock secs (36.45 usr + 1.52 sys = 37.97 CPU) @ 263.37/s (n=10000) Rate cross_table polymorphic parent_table cross_table 257/s -- -3% -13% polymorphic 263/s 3% -- -11% parent_table 296/s 15% 12% -- 考察 レコード数の変化に関して見ていくと、どの設計でもレコード数が少なくとも100,000件程度までであれば、パフォーマンスの劣化は見られませんでした。 各設計手法をパフォーマンスで比較すると、親テーブル > 交差テーブル ≒ ポリモーフィック関連 となっています。 親テーブルに対して交差テーブルのパフォーマンスが低いのは、JOINするテーブルの数が関係していそうです。 親テーブルに対してポリモーフィック関連のパフォーマンスが低いのは、JOINする際の処理の差が関係しているのではないかと考えています。 紐付き先の種類数によるパフォーマンス変化の比較 紐付き先の種類数が変化することで、パフォーマンスがどのように変化するか計測します。 紐付き先の種類は2個と10個のケースで比較し、レコード数は全て1,000件として固定しました。 紐付き先の種類数を10個に増やして計測した結果を以下に示します。 Benchmark: timing 10000 iterations of cross_table, parent_table, polymorphic... cross_table: 188 wallclock secs (57.09 usr + 3.08 sys = 60.17 CPU) @ 166.20/s (n=10000) parent_table: 163 wallclock secs (35.00 usr + 2.79 sys = 37.79 CPU) @ 264.62/s (n=10000) polymorphic: 104 wallclock secs (38.80 usr + 2.74 sys = 41.54 CPU) @ 240.73/s (n=10000) Rate cross_table polymorphic parent_table cross_table 166/s -- -31% -37% polymorphic 241/s 45% -- -9% parent_table 265/s 59% 10% -- 考察 紐付き先が増えることでJOINするテーブルも増えるため、どの設計でもパフォーマンスの劣化を起こすようです。 紐付き先が増えるほどJOINするテーブルの数の差が交差テーブルと他2つの設計との間で開いていくため、パフォーマンスの差も顕著になっているのだと考えられます。 まとめ 多対一のリレーションを表現する設計として「ポリモーフィック関連」、「交差テーブルを用いた設計」、「親テーブルを用いた設計」をパフォーマンスの観点から比較した。 どの設計でも、紐付き先の種類が増えた場合、テーブル結合処理もその分増えていくためにパフォーマンスの劣化を起こす。 交差テーブルを用いた設計は、他2つの設計と比較して、紐付き先の種類が増えることに対するパフォーマンスの劣化度合いが大きい。 明日の記事は id:Dozi0116 さんです。
アバター
この記事は モバイルファクトリー Advent Calendar 2020 14日目の記事です。 はじめまして、20卒エンジニアのthe96です。 今回は業務中に使っているPerlのVSCodeの拡張機能のメソッド呼び出しの際の定義元ジャンプが正しく動作するように修正した話をします。 やったこと 従来の VSCode Perl ではメソッド呼び出し( Hoge::Fuga->func() )のときに定義元ジャンプを行った場合、別パッケージの同名関数の定義元に移動してしまうことがあります。 ソースコードを読む際に正しい定義元に移動しないと不便なので、この拡張機能に手を入れて改善しました。 この不具合について説明するために、同名の関数 hello が定義された二つのパッケージ A と B を用意しました。 修正前の定義元ジャンプ機能では、関数呼び出し( A::hello )のときは A#hello の定義に移動できていますが、メソッド呼び出し( A->hello )のときに定義元ジャンプをすると B#hello の定義に移動してしまっています。 修正後の定義元ジャンプ機能では、メソッド呼び出しでも期待通り A#hello へ移動できています。 原因 メソッド呼び出しされている関数で定義元ジャンプをする際、その関数の前の文字列を参照し、パッケージ名であればそれを含めて検索してくれます。 しかし、 -> がパッケージ名をつなぐ区切り文字として認識されていなかったのが原因でした。 https://github.com/vscode-perl/vscode-perl/blob/master/src/utils.ts#L15 export function getPackageBefore(document: vscode.TextDocument, range: vscode.Range): string { let separatorRange = getRangeBefore(range, 2); let separator = document.getText(separatorRange); let pkg = ""; while (separator === "::") { const newRange = document.getWordRangeAtPosition(getPointBefore(separatorRange, 1)); if (newRange) { range = newRange; pkg = document.getText(range) + separator + pkg; separatorRange = getRangeBefore(range, 2); separator = document.getText(separatorRange); } else { // break loop separator = ""; } } return pkg.replace(/::$/, ""); } 区切り文字に -> を加えた結果、予想通りパッケージ名を考慮して正しい定義元へとジャンプしてくれるようになりました! これで、業務中のコードリーディングが捗りそうです:tada: 改良後の拡張機能 初めてのOSSへのP-Rです https://github.com/vscode-perl/vscode-perl/pull/41 しばらく更新されていなかったので、VSCodeのマーケットプレイスにも公開しておきました。 上記が元リポジトリに反映されるまで、よろしければご利用ください。 https://marketplace.visualstudio.com/items?itemName=the96.vscode-perl 明日の記事は yokoi0803 さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 13日目の記事です。 はじめまして! とあるチームでUX・UI周りを担当しているデザイナーの id:yux_0_0 です。 今日の記事では、職種に限らず誰でもUXを意識できるようにするための「自主トレーニングのススメ」と、それを後押しする「UX探検隊」についてご紹介します。 はじめに 本題に入る前に、私がUXの世界とどう向き合っているかを軽く書かせていただきます。 私のデザイナーとしてのキャリアは、紙媒体のデザインが主な仕事の小さなデザイン事務所から始まりました。 数年かけてデザインの基礎を身につけた後にweb系企業に転職し、FlashでActionScriptを書いたり、グラフィックデザインやwebデザイン等を担当してきました。 モバイルファクトリーに入社してからもしばらくはグラフィックデザインとUIデザインをメインで担当していましたが、数年前にUXの世界に触れて「難しいけど面白い!」と感じ、それからは現場で試行錯誤しながらUXに関わる仕事をやり続けて今に至ります。 解決すべき問題に対してたくさん考えて答えを見つける事が、難しいけれど面白くて好きです。 そして作ったものがリリースされたあとのユーザーさんたちの様子を見たり、自分でイベントをやりに現地にいったときの楽しさと嬉しさといったら! やるべき事もやりたい事も多くて大変ですが、日々頭を抱えながらもワクワクしています。 「いいものを作れるようになりたい」というシンプルかつ壮大な気持ちを原動力にして、「デザイナー」という枠にとらわれず、興味があることに手を伸ばして日々現場で走り回っています。 自主トレーニングのススメ そんな感じで日々テンション高く仕事をしているのですが、UXというものはどうしても「何をやっているのか分かりにくいし専門的で難しそう」と思われがちです。 「UX」という名称がついた書籍やwebの記事などでは専門的な用語や様々な手法が多く紹介されているのですが、実はUXの基本は「深く考えること」です。 もう少し詳しくいうと「色々なケースを想定して色々な方向から物事を見ること」なのですが、これは職種や経験に限らず誰でも意識できることです。 なのでいつも「職種限らずUXを意識できる人が増えたら、チームでも会社でももっといいものが作れるようになるはず」と思っていました。 「もっとみんなで深く考え、自社サービスを良くするための意見を活発に交わしたい」と。 ですが今まで意識していなかった人からすると「何から始めたら良いの?」と思うはず。 そこで今年の夏、社内向けドキュメント 『UXを意識した現場のお仕事紹介と、自主トレーニングのススメ』 を公開しました。 「現場のお仕事紹介」では自分がやっていることについて、専門用語を使わずどの職種でも理解できるように説明しました。 「自主トレーニング(以下、自主トレと表記)」は普段仕事でやっている内容を応用したものです。 考える時の基礎体力 をつけ、 問題解決のため自分で深く考えられるようにする のを主な目的としています。 自主トレのやり方 自主トレは次の3つのステップで行います。 STEP1. 見るものを決めます 自社サービスだと答えを探す方に意識がいってしまうので、自社サービス以外を推奨します STEP2. ターゲットと目的を考えて箇条書きにします 「ターゲットはどの層なのか」と「この施策の目的は何なのか」を考え、思いついたものを書きだしてみましょう すぐに思いついたものを書くだけでOKです STEP3. 箇条書きにしたものに対して自問自答します STEP2で書いたものに対して「本当にそうなのか?」「なぜそう思うのか?」と自問自答し、その理由と考えつく限りの可能性を書き出します 書き出すものが無くなったら終了! このドキュメントは公開直後からたくさんの反響をいただきました。以前 モバファクブログ でも紹介されたので、タイトルに見覚えがある方もいるかもしれないですね。 UX探検隊、はじめました ですがその後も「自主トレをやってみようかな」「自主トレを始めたよ」という声は聞こえてきませんでした。あんなにたくさんの反響をいただいたのにどうしてだろう…と考え、たどり着いたのは「おそらく始めるきっかけがないから」。 じゃあきっかけになるような会をやろう!と考え、10月に誕生したのが『UX探検隊』です。 UX探検隊とは 少人数(最大6人)で自主トレの内容を見ながらワイワイと話す社内勉強会 参加条件は、 自主トレの内容を持ってきて当日共有する ということだけ Google Meetを使ったビデオ会議。2週に1回、1時間開催 私が隊長で、毎回募集する参加者はゲスト隊員と呼んでいます。 ゲスト隊員が 探検の下準備 (自主トレ)をして、UX探検隊の時間にみんなでその内容を 探検する (深堀りする)というものです。 自主トレのテーマや形式は自由です。できるだけ決まりごとを無くして参加しやすくしています。 「UX探検隊という勉強会を始めます!」と発表した日、「自主トレ始めようかな」という声が聞こえてきました。「さっそく効果が出たかも」と嬉しくなりました。その後、数回開催しています。 探検のときにやっていること UX探検隊では、議論をしたり何かの結論を出すことなどはしません。専門用語の解説などもしません。ただただ「なぜだろう?」と想像して意見を交わし合うだけの時間です。 現状、探検のテーマに選ばれるものはソーシャルゲームが多いですが、気になるテーマとして以下を挙げているゲスト隊員もいました。どれもとても面白そうです! ・ 日頃めちゃめちゃ使っているコマンドラインツールのUI/UXとは? ・ ブラウザのタブの位置 ・ 自転車のトップチューブの形 ある日の探検の様子をご紹介 基本的には上で紹介した「自主トレのやり方」に沿ってドキュメントを準備してくれる隊員が多いです。当日はそのドキュメントを画面共有してもらって、探検スタート! ここからご紹介する内容は隊員の自問自答の様子なので、こういう考えもあるよね、という認識で読んでいただければと思います。 詳しい内容を書くととても長くなってしまうのでほんの一部だけですが、雰囲気が伝われば! STEP1 見るものを決めます とあるゲスト隊員が選んだテーマは「ゲームの武器・キャラクターなどの編成画面」。 自身で遊んでいるゲームの中からバトルシステムが異なる2つのゲームをチョイスしたとのこと。知らないゲームもあったので画像付きで軽く説明してもらいました。 STEP2 ターゲットと目的を考えて箇条書きにします 3つの視点から考えた内容を話してくれました。 編成画面があるゲームってどんなことを楽しんでほしいのだろう? ・ ターゲットは編成が億劫ではない人? ・ 編成を楽しんでもらいたいと思っている? 編成は課金へ直結している? ・ このゲームは強い編成が組みたいならガチャを引かないといけない ・ もう片方のゲームはこのキャラクターで戦いたいから編成に入れるという感じ 編成でコミュニケーションが生まれる? ・ 編成の構成などを共有することが多い ・ そういうコミュニティの場ができるのを運営は狙っているかも 遊んだ感想も交えつつ、掘り下げた様子をたくさん話してくれました。初の自主トレ挑戦にもかかわらずSTEP2からものすごく深堀りしてくれています! 隊長の私も、自分が遊んだゲームの例を話したり、自分のスマホに入っているゲーム画面も確認しながら話を進めていきます。 STEP3 箇条書きにしたものに対して自問自答します ここからが自主トレの大事なところです。「なんでそう思ったか?編」と「本当にそうなのか?編」というふうに分けて話してくれました。 自問自答に慣れていないと考え中に迷子になりがちなので、こうやって項目を分けて考えるのは良い案ですね。 自問自答の様子を1項目だけ抜粋します。まずは「なんでそう思ったか?編」から。 お題:編成画面のあるゲームのターゲットが「編成が好きな人」とか「そういうのが億劫ではない人」ってホントなの? このゲームは編成が重要だから編成が好きとかじゃないとついていけないかもしれない ↓ ランキングの上位とかに行きたいなら、対戦相手に合わせて編成を変えるとかを毎日しないとダメだから ↓ でも「編成無理!わからない!」って言ってた人が必ず離脱するかというとそうではなさそう ↓ それでも続けているって言うことは、編成に対して自分なりの答えを出せるってこと。得手不得手あれど、そういうことができる人だと思う 次は「本当にそうなのか?編」 お題:編成画面のあるゲームのターゲットが「編成が好きな人」とか「そういうのが億劫ではない人」ってホントなの? このゲームはこういうキャラクターが好きな人がターゲットでは? ↓ 編成が嫌いな人は出来ないかって言うとそんなことはない ↓ どちらかというと編成要素は薄いのでは? ↓ 正直ステージをクリアしたいだけなら必須じゃないし、編成するにしてもやはり考えることは多くない ↓ 気軽に付け替えて自分なりの遊び方を見つけるのがこのゲームのように感じる これを聞きながら「ちゃんと自問自答できている!」ってすごくワクワクしました。 そしてゲスト隊員がたどり着いたのは 編成要素っていうのは一番強い引きの部分ではないけど、ゲームを盛り上げる要素の割と重要な部分っぽい という考え。最後に、自主トレに対してこんな感想をいただきました。 自問自答の部分では、頭の中で会話しているうちに、結局最初の意見が勝ってしまうので、反対意見をだすのがなかなか大変でした。 最初の意見で終わらせず、深く考えるための自主トレです。なので最後までちゃんと自問自答してくれたことに「パーフェクト!」という印象を抱きました。 参加した他の隊員からも「初回でこれはすごい!」という声があがっていたのが印象的でした。 考えることを楽しんでもらえたら嬉しい 探検中は思ったことをどんどん発言するように心がけています。そしてゲスト隊員にも発言することを推奨しています。 ビデオ会議だと「誰かの話を黙って聞くだけ」になりがちです。 それも悪いことではないのですが、UXの現場では複数メンバーで話しながら何かをスピーディーに作り上げていくこともよくあります。なので思ったことをその場で伝えるのはそのトレーニングにもなります。 人数が多いと発言するタイミングも難しくなるので、それも考え少人数にしています。 また、それに加えて私は純粋に考えることが楽しいと思っているのでそれも伝えたくて。 「面白い!」「すごい」「へえ」「なるほど」「どうしてだろ」「これも深堀りしたら楽しそう」などなど、シンプルな言葉ですが合いの手を入れるように発言しています。 UX探検隊に参加した隊員の声を一部ご紹介します。 ・ 楽しかったです! ・ 一個書き始めたらあっという間に時間が経った ・ 考えがどんどん深堀りして止まらない ・ まとまりなく喋ったのですが、うんうんと聴いていただけて話しやすかった UX探検隊をやるときはいつも、「考えることって楽しいな」「こんな視点から見ることもできるのか」という気づきを持ち帰ってもらえたらいいなと思っています。 なのでこういう声を聞けただけでもUX探検隊を始めた甲斐があったな、と嬉しくなりました。 12/4のアドベントカレンダー で紹介された「UX定例会」は実務的な話をする場所ですが、UX探検隊は「UXってなに?」という人でも身構えず参加できる場所として、皆様の参加を楽しみに待っています。 そして最終的には「UXってよくわからないし難しそう」という気持ちをUX探検隊で解消した人が、UX定例会にも気軽に自主的に参加するようになったら理想だなと思っています。 さいごに UX探検隊は始めたばかりの試みでまだまだこれからです。課題も多いですが試行錯誤しながら今後も継続していくつもりです。 実はUX探検隊以外にも自主トレを始めやすくする企画を準備中ですので、社内の方はどうぞお楽しみに。 社外の方へは、また機会があれば何かの形でお伝えできたらと思っています。 それでは! 明日の記事は the96 さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 12日目の記事です。 こんにちは!新卒エンジニアの id:dorapon2000 です。弊チームではDuplicate entryエラーの解消のためにMySQLのINSERT ON DUPLICATE KEY UPDATE構文を一部で用いています。しかし、使う際にいくつかハマりポイントがあったため、どのようにして回避したかについてお話しようと思います。 INSERT ... ON DUPLICATE KEY UPDATE構文 MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.5.3 INSERT ... ON DUPLICATE KEY UPDATE 構文 -- a=1,b=2,c=3のレコードがなければ作成する。あればa=1,b=2,c=c+1で更新する INSERT INTO table (a, b, c) VALUES ( 1 , 2 , 3 ) ON DUPLICATE KEY UPDATE c = c + 1 ; 「該当レコードがなければINSERT、あればUPDATE」を1クエリで実現できる構文です。 例えば、日次スコアテーブルにあるユーザーのスコアを記録したい場合、まだ1回も記録していなければレコードを作成して記録し、すでに記録してあれば追加分のスコアを増やして更新することができます。 もし、INESRT ON DUPLICATE構文を利用しない場合、アプリケーション側のコードは次のようになります。 ユニークキーAでレコードを取得 if (レコードが存在する) { AのレコードをUPDATE } else { AのレコードをINSERT } こちらのコードは特定の条件下でDuplicate entryエラー(ユニークキー制約違反によるエラー)が発生する可能性があります。特定の条件というのは、ごく短い間に、異なるスレッドが同時にこの条件分岐に差し掛かり、どちらもレコードが存在しないelse節へ到達してしまうことです。その場合、どちらでも同じユニークキーでINSERTを実行しようとしてDuplicate entryエラーが発生します。 INESRT ON DUPLICATE構文を使えば、1クエリで完結するためDuplicate entryも回避でき、コードもすっきりします。実際には、弊チームでは SQL::Maker を利用しているため、そのプラグインの SQL::Maker::Plugin::InsertOnDuplicate を使っています。 AUTO_INCREMENT問題 MySQL 5.6で確認された問題として、INSERT ON DUPLICATE構文を利用した際に、更新時にもAUTO_INCREMENTカラムがAUTO_INCREMENTされるという問題がありました。 INSERTでidが10のレコードが作成される 既存のレコードのいずれかをINSERT ON DUPLICATEによって更新する INSERTでidが12のレコードが作成される ←11ではない!? 上の例では、idの11が歯抜けになっています。歯抜け自体は問題にはなりませんが、頻繁に更新されるテーブルでINSERT ON DUPLICATEを利用しており、idカラムがUNSIGNED INTの場合、最大値である42億を超過する可能性があります。上限に達するとそれ以上カラムをINSERTできなくなるため、アプリケーションは正常に動作しなくなるでしょう。 解決策① BIGINT化 この問題に対処する方法として、まずAUTO_INCREMENTカラムをINTからBIGINT(2 64 − 1)にすることが考えられます。しかし、気になる点として以下のような点があります。 テーブルが歯抜けだらけになり少し気持ち悪い 上限はあるため根本的な解決策とは言えない すでにテーブルが運用中であれば、本番DBにINTからBIGINTにするためのALTERを打つ必要がある 解決策② INSERT ON DUPLICATEの利用回数を抑える 実際の運用では、以下のように実装することでINSERT ON DUPLICATEでDuplicate entryを解消しつつ、AUTO_INCREMENTの副作用を最小限に抑えるようにしています。 ユニークキーAのレコードを取得 if (存在している) { UPDATE } else { INSERTのトリガーを発動 INSERT ... ON DUPLICATE KEY UPDATE } 普通の更新はAUTO_INCREMENTされない通常のUPDATEで更新します。しかし、Duplicate entryが発生する可能性があるときだけ(elseの部分)、 INSERT ON DUPLICATEを利用します。こうすることによって、更新時にはAUTO_INCREMENTされてしまいますが、必要最低限に抑えるようにしています。 少し美しくないですが、メソッド化して呼び出せるようにすればそこまで気になりませんでした。 なお、INSERT ON DUPLICATEによる挿入ではINSERTトリガーが働きません。そのため、else節でINSERTのトリガーも強制的に発動させています。こちらもちょっとしたハマリポイントです。 まとめ 弊チームではINSERT ... ON DUPLICATE KEY UPDATEを利用するようになってから、Duplicate entryを随分抑制できるようになりました。皆さんもハマリポイントと和解しながらよきMySQLライフを! 明日の記事は yux_0_0さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 11日目の記事です。 エンジニアの id:toricor です。巨大なリポジトリを操作していると git gc で待たされることがたまにありますが、一体どんな処理をしているんでしょうか。 git gcとは git gc --help または man git-gc でどんなコマンドか見てみましょう Cleanup unnecessary files and optimize the local repository git gcはリポジトリ内を掃除してくれるコマンドで、pull操作などのタイミングで実行されます。 日々の開発で蓄積したコミットなどを表すオブジェクト(ルースオブジェクト) のファイルを、変更の差分のみを保存した1つのバイナリファイル(packファイル)に詰め込んだり、不要になったオブジェクトのファイルを削除したりします。 git内部で起きることを知るにはどうすればいいか gitでは 環境変数を指定する ことにより挙動を変えたりパフォーマンス情報が得られたりします。 今回は GIT_TRACE を有効にするとよさそうです。 GIT_TRACE は、どの特定のカテゴリにも当てはまらない、一般的なトレースを制御します。 これには、エイリアスの展開や、他のサブプログラムへの処理の引き渡しなどが含まれます ( https://git-scm.com/book/ja/v2 第10章より引用) https://github.com/git/git/blob/e1cfff676549cdcd702cbac105468723ef2722f4/Documentation/git.txt#L670-L672 GIT_TRACE=trueでgit gcを実行してみる 数年開発している、とあるプロジェクトでの実行結果は以下のようになりました % git --version git version 2.29.0 % GIT_TRACE=true git gc 17:34:30.544544 git.c:444 trace: built-in: git gc 17:34:30.545152 run-command.c:663 trace: run_command: git pack-refs --all --prune 17:34:30.546356 git.c:444 trace: built-in: git pack-refs --all --prune 17:34:30.548071 run-command.c:663 trace: run_command: git reflog expire --all 17:34:30.549320 git.c:444 trace: built-in: git reflog expire --all 17:34:30.867648 run-command.c:663 trace: run_command: git repack -d -l -A --unpack-unreachable=2.weeks.ago 17:34:30.868943 git.c:444 trace: built-in: git repack -d -l -A --unpack-unreachable=2.weeks.ago 17:34:30.869516 run-command.c:663 trace: run_command: GIT_REF_PARANOIA=1 git pack-objects --local --delta-base-offset .git/objects/pack/.tmp-21955-pack --keep-true-parents --honor-pack-keep --non-empty --all --reflog --indexed-objects --unpack-unreachable=2.weeks.ago 17:34:30.870871 git.c:444 trace: built-in: git pack-objects --local --delta-base-offset .git/objects/pack/.tmp-21955-pack --keep-true-parents --honor-pack-keep --non-empty --all --reflog --indexed-objects --unpack-unreachable=2.weeks.ago Enumerating objects: 1167941, done. Counting objects: 100% (1167941/1167941), done. Delta compression using up to 4 threads Compressing objects: 100% (221298/221298), done. Writing objects: 100% (1167941/1167941), done. Total 1167941 (delta 930254), reused 1167941 (delta 930254), pack-reused 0 17:35:07.547672 run-command.c:663 trace: run_command: git prune --expire 2.weeks.ago 17:35:07.549012 git.c:444 trace: built-in: git prune --expire 2.weeks.ago Checking connectivity: 1168688, done. 17:35:11.150454 run-command.c:663 trace: run_command: git worktree prune --expire 3.months.ago 17:35:11.151686 git.c:444 trace: built-in: git worktree prune --expire 3.months.ago 17:35:11.152271 run-command.c:663 trace: run_command: git rerere gc 17:35:11.153440 git.c:444 trace: built-in: git rerere gc trace: run_command: として表示されているのがgitのサブコマンドのようですね。 いくつかのサブコマンドを組み合わせて git gc が成り立っているようです。 『UNIXという考え方』 に通じるものを感じます。 見慣れないサブコマンドもあったのでそれぞれ簡単にどういったものかを見てみましょう。 サブコマンド gitにはユーザーが使う前提のaddやcommitのようなサブコマンド(磁器コマンド)と、内部で使われることを前提としたサブコマンド(配管コマンド)があります。 配管と磁器 サブコマンドの細かいオプションの詳細にはあまり立ち入らず簡単に紹介していきます。 git pack-refs git pack-refs --all --prune git-pack-refs - Pack heads and tags for efficient repository access たとえば .git/refs/heads/ にローカルブランチの参照先のコミットハッシュが格納されているファイルが多数ありますが、このコマンドを使うとそれらを削除して .git/packed-refs にまとめます。 git reflog git reflog expire --all git-reflog - Manage reflog information git reflog 自体は日々のgit操作でも過去の自分のブランチ操作を調べるときなどに使いますが、 git reflog expire でこれらの操作歴を消すことができます。 ここでは --expire=<time> が指定されていないのでデフォルトの90日分のみを残すような設定になっています。 git repack git repack -d -l -A --unpack-unreachable=2.weeks.ago git-repack - Pack unpacked objects in a repository packされていなかったオブジェクトはpackされ、すでにあるpackファイルも再編成して1つのファイルに組み直します。 git pack-objects GIT_REF_PARANOIA=1 git pack-objects --local --delta-base-offset .git/objects/pack/.tmp-21955-pack --keep-true-parents --honor-pack-keep --non-empty --all --reflog --indexed-objects --unpack-unreachable=2.weeks.ago git-pack-objects - Create a packed archive of objects git pack-objects がpackファイルを書き出し(そしてpackファイルに高速にランダムアクセスするためのpack indexファイルも書き出し)してくれるサブコマンドです。 git prune git prune --expire 2.weeks.ago git-prune - Prune all unreachable objects from the object database --expire オプションをつけることで2週間と指定してそれより古い、どこからも到達できないルースオブジェクトを削除してくれます git worktree git worktree prune --expire 3.months.ago git-worktree - Manage multiple working trees git worktree を使うと複数の作業ツリーを持てます。複数のブランチの内容を同時に複数の場所に展開できます。 prune で $GIT_DIR/worktrees にある情報を消してくれます。 https://git-scm.com/docs/git-worktree git worktree は普段の開発でも便利に使えそうです (git gcからではなく自分でgit-worktreeを使うときは git worktree add と git worktree remove のペアで使うのが基本的な使い方になるでしょう)。 git rerere git rerere gc git-rerere - Reuse recorded resolution of conflicted merges git rerere は開発者が作業したconflict解消を覚えてauto merging時に支援してくれるそうです。 git rerere gc で古いmergeのデータをunresolved conflictsで15日より古いもの、resolved conflictsで60日より古いものをデフォルトで削除してくれるようです。 git-scm.com まとめ GIT_TRACE=trueを指定することでgitの各種コマンドの内部処理を垣間見ることができる git gcは様々なサブコマンドの組み合わせで成り立っている ルースオブジェクトをpackファイルに編成するだけではなかった refsも再編成したり rerereなどのデータの削除をしたりしている 参考文献 https://github.com/git/git https://git-scm.com/book/ja/v2 明日の記事は id:dorapon2000 さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 10日目の記事です。 こんにちは、エンジニアの id:tsukumaru です。 最近はチームのエンジニアのまとめ役を任され、メンバーの評価にも一部関わるようになりました。 評価を考える際、具体的にいつどのようなことがあったのかを把握していないと、「なんとなく頑張っていそうだから」や「とりあえず真ん中の評価にしておこう」といったような評価エラー(評価者が陥りがちな過ち)を起こすことにつながってしまいます。 今回は、評価エラーを防ぐために自分が行っている「行動メモ」について紹介したいと思います。 ※ 成果目標と行動目標に分けて目標を立てているなど、「行動」を評価するための評価制度がある前提で書いています。 また、この記事では評価エラーについての詳細な説明は割愛します。 行動メモについて 普段の各メンバーの様子(Slackでのやりとりや会議、1on1での話など)の中で、「おっ 👀 」と思ったところを都度ドキュメントなどにメモしていきます。 ドキュメントは半期ごとに各メンバーごとで作っています。 (ドキュメントの公開範囲は必要に応じて調整してください) あくまでメモをする目的は「評価エラーを防ぐため」なので、メンバーの良い行動も気になった行動も両方メモします。 メモをする基準 「おっ 👀 」と思う基準として、例えば「その人が新しくチャレンジしている様子」があります。 新しくチャレンジしている(以前とは違う行動をしている)ということは、目標達成に向けて得意を伸ばしたり苦手を克服しようとしているということなので、プラスに評価するためにメモするようにしています。 また、目標に掲げていないことであっても追加で取り組んでいる様子があれば、それもメモするようにします。 逆に、例えばもし「チームの和を乱すような様子」があった場合には、マイナスな行動としてメモしておきます。 評価エラーの中の、「中心化傾向(当り障りのない無難な評価)」や「寛大化傾向(全体的に甘い評価をしてしまう)」への対策としても、マイナスな行動のメモも大事になってきます。 ただ、基準を意識しすぎてしまうと、せっかくの行動を書きそびれることもあったりするので、しっかり基準を決めるというよりは気になったら書いていく方がいいかもしれないです。 テンプレート 自分が使っているテンプレートは以下の通りです。 日付 起きたこと 自分が思ったこと 目標のカテゴリの中のどこにあてはまりそうか 基本的には上3つをメモしていく形でいいと思っています。 弊社では行動目標がさらにいくつかのカテゴリに分かれているので、その中のどこに当てはまりそうかということも追加で書くようにしています。 例えば、ドキュメントに以下のような表を作り都度追記しています。 日付 起きたこと 自分が思ったこと 分類 2020/04/01 POに立候補していた (Slackのリンク) 自分の得意を活かしながら影響範囲を広げていてGood チャレンジ 続けていくコツ 今回紹介した行動メモですが、実際にやろうとするとチームメンバーの様子を常に把握している必要があり、続けていくのはなかなか大変だと思います。 そんな中で自分が考えた続けていくコツは以下の3つです。 無理をしない 行動メモに書く内容は、一言レベルに留めておきます。 しっかり書くことよりも、いつどんなことがあったのかを記録することが大事なので、継続しやすい形式を意識します 複数人で共有する 行動メモは自分だけで書くのでも良いですが、全員の行動を把握するのは難しいこともあると思うので、同じチームのマネージャーや各メンバーのメンターなどと共有して一緒に書くのもおすすめです 定期的に確認する機会を作る 普段の業務が忙しいと、どうしても行動メモは後回しになることがあります。 1on1のタイミングで確認したり、Slackのリマインドを設定するなど、定期的に確認する機会を用意するのもいいかもしれません 気を付けるポイント この記事では、評価面談に向けて評価を考える場面のみに注目して「行動メモ」を紹介しています。 実際はメモを溜めることとは別に、普段から定期的な1on1などでメモの内容を都度フィードバックしていくことが大事です。 まとめ チームのまとめ役として評価面談に関わった経験から、評価エラーを減らすための取り組みとして「行動メモ」を紹介しました。 お互いに納得感のある評価を行うための一つの方法として、参考になれば幸いです 👀
アバター
この記事は モバイルファクトリー Advent Calendar 2020 9日目の記事です。 こんにちは、ブロックチェーンチームの新卒エンジニア id:charines です。 Nuxt.jsにおけるasyncDataの役割 ブロックチェーンチームでは、Nuxt.jsのサーバーサイドレンダリング機能を用いた開発を行っています。 asyncData はページの読み込み時に、返されたPromiseの値をコンポーネントの data にマージするためのフックで、ページの移動やエラーページの表示はPromiseの解決を待って行われます。 問題 asyncData はページコンポーネント毎に定義されるため、読み込み時のエラーハンドリングなどの処理が全ページで共通であったとしても、各ページコンポーネントにその処理を記述しなければなりません。 具体例として、サーバーサイドで実行された asyncData 内で例外が発生した場合にエラーページを表示するには、 asyncData の第一引数のオブジェクトに定義された error 関数を呼び出す必要があります。次に示すのは asyncData 内でAPIからエラーレスポンスが返された際にエラーページを表示する処理です。 Vue.extend( { async asyncData( { app, error } ) { try { // app.$api.getUser() が返すプロミスは // statusとmessageをプロパティとして持つ例外でリジェクトされることがある const user = await app.$api.getUser(); return { user } ; } catch (err) { if (process.server) { // サーバーサイドで実行されている場合はerrorを呼び出してエラーページを表示する error( { statusCode: err.response ? err.response. status : 500, message: err.message, } ); return ; } throw err; } } , data: () => ( { user: undefined , } ), } ); この例では app.$api.getUser という非同期関数の解決した値を user として data にマージします。 ここで、このコードを修正して「クライアントサイドでステータスコード401のHTTPレスポンスを受け取った例外が発生した場合はリロードする」という処理を入れることにしました。しかしこのような変更を行う場合、先述の通り asyncData はページコンポーネント毎に定義されているため、全てのページコンポーネントに修正を行う必要があります。 *1   やったこと 全てのページで共通する asyncData のエラーハンドリングを一箇所のコードにまとめるために、 asyncData を生成する関数を書きました。以下が実際のコードです。 export function createAsyncData(asyncData) { return async (context) => { try { // asyncDataはページ固有の処理を行う関数 const data = await asyncData(context); return data; } catch (err) { const statusCode = err.response ? err.response. status : 500; if (process.server) { context.error( { statusCode, message: err.message, } ); return ; } if (statusCode === 401) { location .href = context.route.path; // リダイレクトが完了するまでにエラーページが描画されないようプロミスを待機させる await Promise.race( [] ); } throw err; } } ; } この関数はページ固有の処理を行う関数を引数として受け取り、受け取った関数の実行とエラーハンドリングを行う新たな関数を返します。各ページコンポーネントではこの関数を以下のように使用します。 Vue.extend( { asyncData: createAsyncData(async ( { app } ) => { const user = await app.$api.getUser(); return { user } ; } ), data: () => ( { user: undefined , } ), } ); これでエラーハンドリングなどの全ページで共通の処理をページコンポーネント毎に書く必要がなくなり、変更が容易なコードになりました。 まとめ asyncData 関数を生成する関数を書いて全ページで共通の処理をページコンポーネントの実装から分離することで、この関数を修正するだけで全ページのエラーハンドリングを修正することができるようになりました。 明日の記事は id:tsukumaru さんです! *1 : Nuxt 2.12以降は fetch を利用することができます。fetchを使う場合もエラーハンドリングなどの処理はasyncDataと同様です。
アバター
この記事は モバイルファクトリー Advent Calendar 2020 8日目の記事です。 はじめに こんにちは、エンジニアの id:mp0liiu です。 MySQLでは基本的にクエリを実行する際インデックスは1つしか効きませんが、インデックスマージという仕組みによって複数のインデックスを使った検索結果をマージし、その和集合や共通集合を効率よく取得できる場合があります。 とはいっても具体的にどのようなケースでインデックスマージが利用されるのかわかっていなかったので、 MySQLの公式ドキュメント を見つつ実際にテーブルを作って検証してみました。 本記事では検証した結果を基にインデックスマージが利用される具体的なケースをいくつか紹介します。 検証に使った環境は以下の通りです。 Ubuntu 18.04 MySQL 5.7.32 事前準備 まず検索対象のテーブルを作ります。 CREATE TABLE user_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL , item_id INT UNSIGNED NOT NULL , count INT UNSIGNED NOT NULL DEFAULT 0 , created_at DATETIME NOT NULL , INDEX user_id (user_id), INDEX item_id (item_id), INDEX created_at (created_at) ); user_id, item_id, created_at にインデックスを貼っています。 次に以下のスクリプトでデータを挿入します。 インデックスは検索対象のテーブルの行が十分に大きく、かつカーディナリティが高い列でないと利用されないです。 今回はランダムな値のレコードを1万件挿入し、カーディナリティ100程度でインデックスマージが利用されているケースを確かめられました。 use strict ; use warnings ; use utf8 ; use DBI; use Time::Moment; my $dbh = DBI-> connect ( 'dbi:mysql:database=sandbox' , 'root' , '' , +{ AutoCommit => 1 , PrintError => 0 , RaiseError => 1 , ShowErrorStatement => 1 , AutoInactiveDestroy => 1 } ) or die $ DBI:: errstr ; my $time = Time::Moment->from_string( '2020-12-01T00:00:00Z' ); for my $n ( 1 .. 10000 ) { $dbh->do ( q{ INSERT INTO user_item (user_id, item_id, created_at) VALUES (?, ?, ?)} , undef , ( int ( rand ( 100 ) ), int ( rand ( 100 ) ), $time->plus_seconds ( int ( rand ( 100 ) ) )->strftime( '%Y-%m-%d %H:%M:%S' ), ) ); } 検証 スクリプトで挿入したデータをインデックスが効きそうな条件で検索し、EXPLAIN でクエリ実行計画を見てみます。 インデックスを貼ったそれぞれのカラムをANDで繋げて条件指定する mysql> EXPLAIN SELECT * FROM user_item WHERE item_id = 1 AND user_id = 51 AND created_at = '2020-12-01 00:00:25'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: user_item partitions: NULL type: index_merge possible_keys: user_id,item_id,created_at key: user_id,item_id key_len: 4,4 ref: NULL rows: 1 filtered: 5.00 Extra: Using intersect(user_id,item_id); Using where 1 row in set, 1 warning (0.00 sec) type列をみていると、 index_merge となっており、インデックスマージが効いていることがわかります。 key列からは user_id, item_id のインデックスが利用されたことがわかります。 Extra列は Using intersect(user_id,item_id) となっており、公式ドキュメントに書いてある インデックスマージ共通集合アクセスアルゴリズム でインデックスマージが行われることがわかります。 つまり user_id で絞り込んだ結果と item_id で絞り込んだ結果の共通集合が返ってくる、ということでしょう。 created_at のインデックスも利用可能になっていますが、rows が1になっていることを考えると恐らく user_id, item_id だけで十分結果を絞りこめるので利用されていないということでしょう。 試しにデータ量を増やしてみるとすべてのキーが使われる場合もありました。 条件の値によっても使われるキーが変化していて、より結果を絞り込みやすいインデックスから優先的に利用されていました。 動き的に複合インデックスを貼った場合と似ていますが、複合インデックスの場合は検索に利用するカラムの順番が決まっているのに対して、インデックスマージは効率に応じて利用されるインデックスが変化する点が違っていそうです。 インデックスを貼ったそれぞれのカラムをORで繋げて条件指定する mysql> EXPLAIN SELECT * FROM user_item WHERE item_id = 1 OR user_id = 10 OR created_at = '2020-12-01 00:00:01'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: user_item partitions: NULL type: index_merge possible_keys: user_id,item_id,created_at key: item_id,user_id,created_at key_len: 4,4,5 ref: NULL rows: 259 filtered: 100.00 Extra: Using union(item_id,user_id,created_at); Using where 1 row in set, 1 warning (0.00 sec) これもtype列をみていると、 index_merge となっており、インデックスマージが効いていることがわかります。 key列からは user_id, item_id, created_at のインデックスが利用されたことがわかります。 Extra列は Using union(item_id,user_id,created_at) となっており、公式ドキュメントに書いてある インデックスマージ和集合アクセスアルゴリズム でインデックスマージが行われることがわかります。 つまり user_id, item_id, created_at の各インデックスで絞り込んだ結果の和集合が返ってくる、ということでしょう。 インデックスを貼ったカラムをORで繋げて範囲条件を指定する mysql> EXPLAIN SELECT * FROM user_item WHERE item_id IN (1, 3, 5) OR created_at < '2020-12-01 00:00:05'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: user_item partitions: NULL type: index_merge possible_keys: item_id,created_at key: item_id,created_at key_len: 4,5 ref: NULL rows: 779 filtered: 100.00 Extra: Using sort_union(item_id,created_at); Using where 1 row in set, 1 warning (0.00 sec) こちらは Extra列が Using sort_union(item_id,user_id) となっており、公式ドキュメントに書いてある インデックスマージソート和集合アクセスアルゴリズム でインデックスマージが行われることがわかります。 インデックスマージソート和集合アクセスアルゴリズムがどのような場合に使われるのかよくわからなくてこの状況を作り出すのが難しかったのですが、このケースのように範囲条件で絞り込まれる結果が比較的少ない場合(item_id は 1, 3, 5 のいずれか、 created_at は 2020-12-01 00:00:00' 〜 2020-12-01 00:00:05' のいずれか)はインデックスマージが行われるようでした。 インデックスを貼った片方のカラムを条件指定し、もう片方のカラムでソートする ORDER BY でもインデックスマージが効くのか気になったので調べてみました。 mysql> EXPLAIN SELECT * FROM user_item WHERE user_id = 20 ORDER BY created_at\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: user_item partitions: NULL type: ref possible_keys: user_id key: user_id key_len: 4 ref: const rows: 89 filtered: 100.00 Extra: Using index condition; Using filesort 1 row in set, 1 warning (0.00 sec) インデックスマージは効かず、通常通り1つのインデックスだけが利用されています。 このような場合両方の列に対してインデックスを効かせるには複合インデックスを貼るしかなさそうです。 おわりに 実際に検証してみて具体的にどのような場合にインデックスマージが行われるのかがかなり理解できました。 まとめると別々のインデックスが効く複数の問い合わせ結果を集合演算したものが得られるような仕組みだと言えるかなと思いました。 明日は id:charines さんです。
アバター
この記事は モバイルファクトリー Advent Calendar 2020 7日目の記事です。 こんにちは、ブロックチェーンチームのソフトウェアエンジニア id:odan3240 です。湯船に浸かるのが楽しい季節になってきました。 以前テストに関するこの記事が話題になっていて、読んだときに最後の部分が目に留まりました。 blog.sushi.money テストを先に書いてから実装を書くか、先に書いた実装のテストをあとから書いているか、という場合でも違いが出てきそう。 以前までの自分は先に実装を書いてからテストを書くことがほとんどでした。理由としては、性格的にコードを書くのが好きで、頭の中にあるコードを急いで書き出したくなるため、作業に入ると先に実装を書いていました。 しかし、開発時に実装より先にテストケースから書き始めるとうまく実装が進むことに気付いたので、共有します。 割り算を行う関数 div を例にすると次のような感じです。 export function div ( a: number , b: number ) { if ( b === 0 ) throw new Error ( "divide zero" ); return a / b ; } これに対して実装後に正常系と異常系で二分するようなテストを書いていました。 describe ( "div について" , () => { describe ( "正常系" , () => { it ( "動作が正しいこと" , () => { expect.assertions ( 1 ); expect ( div ( 11 , 2 )) .toBe ( 5.5 ); } ); } ); describe ( "異常系" , () => { it ( "例外を投げること" , () => { expect.assertions ( 1 ); expect (() => div ( 11 , 0 )) .toThrow ( Error ); } ); } ); } ); 今の見るとこのテストには次の問題があると考えています。 「正常系」「異常系」では、どういう条件で正常系/異常系の挙動になるのかがわからない 「動作が正しいこと」では、具体的にどういう挙動を求めているのかがわからない どちらもテストコードを見なければその関数の仕様となる求めている条件や挙動がわかりません。 これの原因を考えてみると、先に書いた実装に合わせてテストケースを書いているからだと思いました。 この問題の解消のために、テスト対象の関数の仕様の列挙を目的にまずテストケースを書き始めるようになりました。先ほどの div 関数の例だと次のようなテストケースになります 1 。 describe ( "div について" , () => { describe ( "割る数が0以外の場合" , () => { it.todo ( "割り算が計算される" ); } ); describe ( "割る数が0の場合" , () => { it.todo ( "例外を throw する" ); } ); } ); ポイントは、挙動を確かめるために実装に沿ったテストケースを書くのではなく、テストの対象となる関数がどういう場合にどういう挙動になってほしいのかを表すテストケースを書くことです 2 。 仕様として先にテストケースから書くことによって次の利点があると考えています。 テストケースの数によってそのメソッドの責務が過剰になっていないか事前に気付ける テストケースを書いた時点で実装する仕様に対して抜け漏れがないかチームメンバーにレビューを依頼できる テストケースが仕様として機能するため後からその関数がどういう仕様なのか楽に追える まとめ 最近実践している、実装の前にテストケースを書くことで、仕様を整理してから実装に取り掛かることについて紹介しました。 明日の記事は id:mp0liiu さんです! jest では it.todo を使うことで、テストケースだけを記述することができる ↩ 同時に関数のインタフェースについてもこのタイミングで考えることが多い ↩
アバター
この記事は モバイルファクトリー Advent Calendar 2020 6日目の記事です。 はじめましての方ははじめまして、エンジニアの id:Nanamachi です。今回の記事ではテストに用いているJenkinsサーバーを勤務時間外に停止させる設定を行ったときに用いた AWS Instance Scheduler について解説します。 TL; DR # 課題 AWS上で稼働しているJenkinsサーバーが勤務時間外も動作しており、必要のない費用がかかっていた # 手法 AWS Instance Schedulerを用いてインスタンスの起動停止設定をスケジュールした # 結果 勤務時間外はインスタンスが停止するようになった - 突発対応などで設定を変更することも簡単にできる - 停止させたいサーバーが増えたときもそのインスタンスにタグを付けるだけでよい 背景 現在所属しているチームではテスト基盤としてJenkinsを用いています。ここではテスト時間の高速化のためマシンスペックの高いインスタンスを使っており、勤務時間外も動きっぱなしにしてしまうとそこそこの費用が発生してしまいます。そのため、夜間・休日にインスタンスを停止させて費用削減することとなりました。 手法調査 調査開始当初は、すでに時間帯ごとのスケールアウト用途として使用実績のあったEC2 AutoScalingを使う想定でした。しかし、AutoScalingではインスタンスの停止ではなく終了処理がなされてインスタンスの状態が保存されないためNGと判断しました。 そこで代わりとなる手法を調査したところ AWSが公式で情報提供 しているInstance Schedulerを発見しました。これはCloudFormationのテンプレートとして提供されている機能で、数分程度で設定が完了するほか複数のインスタンスを柔軟に設定できる点が特徴です。他の手法としてはCloudWatchやLambdaを検討したのですが、最も手間がかからず適用できる方法としてこちらを採用します。 Instance Scheduler の解説 Instance Schedulerのテンプレートでは多くの要素が定義されており初めて見ると面食らってしまいますが、実際に行われていることは単純です。 Amazon EventBridge (旧: CloudWatch Event) が定期的にトリガーする トリガーされたAWS LambdaをDynamoDBの設定を元に実行する LambdaがEC2インスタンスやRDSにつけられたタグを見て起動・停止する Instance Scheduler の構成 (画像出典: https://aws.amazon.com/jp/solutions/implementations/instance-scheduler/ ) これらの設定と必要な権限をまとめてCloud Formationで作成することが可能です。 Instance Schedulerの設定手順 AWSソリューション の ドキュメント を見ながら設定していきます。ドキュメントが詳細に書かれているためこの記事ではかいつまんで必要そうな点を解説します。 1. テンプレートから作成する 上記「AWSソリューション」のページにある「AWSコンソールで起動する」をクリックするとテンプレートを選択した状態となります。非常に便利なのですが、このとき リージョンがバージニア北部 に変更されているので画面の右上から対象のリージョンに忘れずに変更しましょう。 2. スタックの詳細を指定 「スタックの名前」は任意の名前を指定します。一度デプロイしてしまえば複数のリージョン・アカウントのインスタンス全てに対して設定を行うことが可能になるので、対象となるサービス名をつけずに「InstanceScheduler」とするのがおすすめです。 その下にはテンプレートのパラメータが定義されています。よく使いそうなオプションを抽出すると下記のとおりです。 パラメータ名 デフォルト 解説 Instance Scheduler tag name Schedule InstanceScheduler がインスタンスのスケジュールを区別する際に見るタグ Service(s) to schedule EC2 EC2だけでなくRDS、またその両方のスケジュールを管理することが可能です Regions (空白) Instance Scheduler をデプロイしたリージョン以外を管理したい場合はこちら Default time zone UTC タイムゾーンです。日本時間ならば Asia/Tokyo Cross-account roles (空白) 複数のAWSアカウントについて管理する場合はこちら Frequency 5 InstanceSchedulerのイベントが実行される間隔です(単位: 分) 3. スタックオプションの設定 / レビュー 「スタックオプションの設定」は作成されるFormation自体の設定です。デフォルトのまま次へ進み、内容が良ければ作成します。 4. スケジュールの設定 CloudFormationによってスケジュールなどを定義するための ConfigTable がDynamoDBに作成されます。 ConfigTableには次の3種類のtypeが定義されています。 - config - Instance Scheduler全体の設定 - 必要がある場合はCloudFormationから設定し、このレコードは直接編集しない - period - 曜日や日付、起動時間、停止時間といったインスタンスが起動する時間を定義するレコード - schedule - periodやTZ、起動設定などを束ねたレコード - 複数のperiodをまとめることやインスタンス種別ごとに異なるperiodを指定することもできる - 管理対象インスタンスのタグ値にはこの名前を指定する 新しくperiodやscheduleを作成する場合、各項目の型を間違えないようにするためデフォルトで用意されているレコードをコピーして作成するのがおすすめです。 5. インスタンスにタグを付ける 上記で作成したscheduleを対象となるインスタンスに設定します。EC2コンソールに移動し、 Schedule: (スケジュール名) のタグを設定するだけでそのインスタンスがSchedulerの管理下に入ります。 結果 テンプレートやドキュメントが詳細に整備されているため、1時間もかからずに設定することが可能です。また、設定もDynamoDBコンソールから管理できるため、手軽に変更することができます。さらに、Instance Schedulerを1回デプロイしてしまえば他のサーバーもタグを付けるだけで同様のスケジュール設定を適用することができます。 明日の記事は id:odan3240 さんです!
アバター
この記事はモバイルファクトリー Advent Calendar 2020 5日目の記事です。 はじめに 新卒1年目のエンジニアをしている id:dorapon2000 です。これまで1年で数冊のペースでしか本を読んできませんでしたが、入社してから8ヶ月経ち22+α冊の本を読むことができました。その際に、何を意識して読んでいたのか、モチベーションを継続させるためにした工夫などをお話しようと思います。ここでの本というのは小説ではなく主に技術書・実用書を指しています。 読んだ本 技術関係 Perlベストプラクティス ドメイン駆動設計入門 カイゼン・ジャーニー Effective Python第2版 達人に学ぶDB設計徹底指南書 達人に学ぶSQL徹底指南書第2版 ソフトウェアテスト技法練習帳 最強のCSS設計 チーム開発を成功に導くケーススタディ アカマイ 知られざるインターネットの巨人 詳解HTTP/2 WEB+DB PRESS Vol.119 Software Design 2018年8月号 個人開発をはじめよう!クリエイター25人の実践エピソード Modern Perl 4th edition におうコードの問題集 ソフトウェア設計に立ち向かう編 Web API: The Good Parts プロフェッショナルSSL/TLS その他 予想どおりに不合理 「ついやってしまう」体験のつくりかた 確率思考の戦略論 本はどう読むか お金2.0 新しい経済のルールと生き方 完全に趣味の本や今も読んでいる本 いくつか きっかけ note.com こちらの記事に衝撃を受けました。記事を書かれたリリアンさんという方は短期間に大量のインプットをされています。いろいろされていましたが読書に絞ると1週間に2冊のペースで本を読まれていました。1年で数冊しか読まない自分にも真似できるだろうかと奮い立ったのが、本を読み始めるようになったきっかけです。ただ、やる気がでたのは、リリアンさんをこの記事以前から知っていて(一方的に)身近に感じていたからだと思います。全く知らない人の記事であれば世界は広いなぁで終わっていたでしょう。 そこから本を読む上で意識したことはこのモチベーションの維持でした。 モチベーション やる気が出ても次の日の朝には雲散霧消するなんてことは日常茶飯事なので、読書を長く続けられる方法を模索する必要がありました。そのためにはまず今の自分の読書に対する姿勢から振り返りました。 読みたい本は常々ある しかし、読み始めるまでの腰が非常に重い 一旦読み始めるとさくさく最後までいく 読み終わると満足して次の1冊を読み始めない そしてタイトル以外ほぼ覚えていない。 読んだ本を再読するモチベーションがない 本を暗記するのは無理ですが、こんなことが本に載っているから、詳しく知りたいときはこの本を見返せばいい、という状況のために本を読みたかったです。その上で、タイトル以外覚えていないことが多々あるのは改善すべきだと思っていました。 どうしたら忘れずにいられるか いくつか仮定を置きました。 一瞬で読み終えるより時間を掛けるほど記憶に定着しやすい 時間をあけて復習することで記憶に定着しやすい アウトプットすることで記憶に定着しやすい それらしいことを並べていますが、どこかで見聞きしたような知識だったりカンです。この仮定を頼りに自分がやるべきことを考えました。 実践したこと ここまでのことを踏まえて私が実践したことを紹介します。 並行して読む 連続して同じジャンルの本を読まない 覚えたいと思ったことの写経をやめる 読み終えたら感想を書く 業務で活かす・妄想する 並行して読む 1冊読んでいる状態で次の本を読み始めるのに、自分はあまり労力がかからないことに気づきました。そのため、並列して本を読み、1冊読み終えてもまだ他の本を読んでいる状態にして、さらに別の本を読み始める。このローテーションで本を読むというモチベーションの線を切らずに済みます。また、1冊に掛ける期間も長くなるため、覚えやすくなると思っています。実際には4冊ほどを並行して、日替わりで読んだり、1日数十ページ×3冊ずつ読んだりしています。 連続して同じジャンルの本を読まない 同じジャンルの本は内容が一部被っているため復習に使えます。連続して短期間で読むよりも、間を開けて復習を兼ねて読むほうが覚えられると思いました。いろいろなジャンルの本に手を出すきっかけにもなります。 これは並列して読む際にも意識しています。例えば、DBとテストと行動経済学というふうに他ジャンルを並列して読みました。 覚えたいと思ったことの写経をやめる 今まで、読んで気になったところ(数ページに1箇所くらい)をメモに写経して、あとから見返して本の内容を思い出すことに使っていました。本当は要約がいいのですが、時間と労力がいるため、途中で挫折するなんてことが多々あります。問題は写経にも時間がかかってしまう点です。そこで、マーカーを引くようにしました。本をパラパラめくってマーカーの部分を拾うだけでも写経の目的が達成できます。とても簡単に。 実を言うと本をきれいに保ちたかったためマーカーを引くことを非常に渋っていました。ただ、もし新しい状態にしたかったらもう1冊買えばいいかという気持ちでマーカーを引くことにしました。この気持ちがちょっとした出費で解決できるなら安い買い物です(気持ちだけで実際にもう1冊買ったことはないです)。最近は電子書籍を購入するようになりました。一番の理由はマーカーを引くことに引け目を感じず、精神衛生上よいからです(笑)。マーカーを引けない電子書籍はいまだどちらで買えばいいか悩みますね。 読み終えたら感想を書く アウトプットすると覚えやすいと仮定したため、読み終えたらすぐ感想を書きました。人に見せるものでもないため、乱雑に思ったことと要約を500字程度で箇条書しています。ただ、定期的にブログや 読書メーター というサービスで人目に晒すことにしています。一目に晒すにはメモとマーカーを見ながら推敲する必要があり、その際に本の復習になります。 最近読んだ本の感想 - dorapon2000’s diary 最近読んだ本の感想2 - dorapon2000’s diary 業務で活かす・妄想する エンジニア業務は学んだ知識が活きるとてもよい機会です。しかし、知識をそのまま活用できるかはわかりませんし、活かすことがベストなのかは別問題なため、できるところから始めます。活かせない場合でも、このコードにはこういう考え方があってこういう形になったのだろうと妄想することで、知識から血肉骨にできます。 まとめ ここにあげたことは今でも実践して、私の読書のモチベーションを維持してくれています。あのリリアンさんの記事を読んだときの自分からは想像もつかなかったことでしょう。これからも自分を見つめ直して、自分にあった読書のノウハウを蓄積していきたいと思います。 明日の記事は id:Nanamachi さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020 4日目の記事です。 はじめまして!駅メモ!チームでUX周りを見ているUXエンジニアのMです。 今回は、会社全体のプロダクトのUX品質底上げのため、日頃から行っている「UX定例会」についてご紹介したいと思います。 モバファクのUXデザイナー/UXエンジニアとは? まずは、弊社のUX周りについての説明が必要かと思います。現在、私が所属しているチームでUXチェックを行っているメンバーは、最初からUXを担当する役割でチームに入ったのではなく、元々はエンジニアだったり、デザイナーだったメンバーが、専門性を高めてUX専任になった経緯があり、まだまだ発展途上の役割です。 UXデザイナー/UXエンジニア(以下、UX担当者)の役割は、ビジュアルのデザインに留まりません。プロダクトに触れるユーザーの体験を、たくさんの制約がある中、様々な手段で最大化することが責務です。そのため、あらゆる場面でプロダクトに関わっています。 具体的にその例を上げると、 ディレクターが考えた施策のUX面からの評価、改善案の提案 その施策を元に、Sketchなどを用いて具体的なIA(情報設計)/UIを設計、必要な画像素材等の仕様も決める デザイナーが制作した画像素材が意図したUXを実現できているかをレビュー 実装されたものにUX面で新たな問題が生まれていないかをレビュー、必要があれば対応策を提案 など、広範囲に渡ります。企画に関しては目標数値などへの理解、画像周りに関してはデザイナー業務への理解、実装面では開発への理解が求められ、広い知識と経験、コミュニケーション力が必要になります。 しかし、弊社ではまだまだUX人材が足りておらず、各チームそれぞれにUX担当者がいるという状況にはできておりません。現状UX専任のメンバーがいるのは限られた一部のチームのみとなっています。UXに明るいデザイナーやエンジニアがチームにいれば、そのメンバーがフォローしていますが、会社として世に出すプロダクトの、UX品質のムラはできるだけ減らす必要があります。 そこで、このような状況を少しでも改善するため行っているのが「UX定例会」です。 UX定例会とは 開催日/開催方法 毎週月曜の1時間 リモートワークのため現在はGoogle Meetを使ったビデオ会議 参加者 各チーム最低一人、それ以上は自由に参加OK 参加職種に制限はなく、寧ろさまざまな職種の人に参加してもらうことを推奨 やること 一週間の間にチームでやったことを、定例会の共有ドキュメントに記入し、その内容についてみんなでUXの視点で深掘り 目的 UX担当者の居ないチームのサポート それぞれのプロジェクトで使用している技術や知見の共有 具体的な事例を通してUXについて各自の理解を高めること 会社全体の状況を俯瞰できる機会を設けること UX定例会の目指すこと UX定例会の目的についてもう少し解説していきます。 UX担当者の居ないチームのサポート 例えば、ユーザーの分析のやり方を相談したいと思っても、チームメンバーが少なく、他に相談できる人がいないこともあります。そんなときのために、UX関連の課題を、 チームを横断して定期的に相談できる場 を作ることが一つの目的です。 相談事ができる度に、別のチームに声をかけてやりとりするのは、どうしても心理的ハードルが上がりがちです。そこで、定期的にこのような場が設けられていれば、 気軽に 相談できるようになります。また、定例会には様々なチームのUXに明るいメンバーが集まっているので、より的確なアドバイスやフィードバックを得ることができます。 それぞれのプロジェクトで使用している技術や知見の共有 例えば、サービス分析の手法は数多くあります。定例会で、とあるチームが「狩野モデルを使ってみて、こんなメリットがあった。」と他のチームに向けて共有したとします。すると、「そのメリットは自分のチームにも当てはまりそう。」と、それまでは一つのチームだけに収まっていた知見が、たくさんのチームに伝搬していきます。このように、 新たな知識を得られる機会 を作ることも一つの目的です。 チーム内だけの視点では、どうしても仕事の手法が凝り固まってしまいがちですが、他のチームでやっていることを実際の言葉で見聞きすることで、新しいやり方をより身近に知ることができます。このようにチーム同士でお互いに刺激し合うことで、会社全体の仕事の質を上がることを期待しています。 具体的な事例を通してUXについて各自の理解を高めること 「UX」というワードは非常に曖昧で、人によって想像しているものが違います。(長くなるため、この記事でもUXの意味を具体的に解説していません。)しかし、 UXは本来、あらゆる立場の人に持っていてほしい視点 です。UXの問題や改善すべき点は、ディレクターが気づきやすいところ、デザイナーが気づきやすいところ、エンジニアが気づきやすいところがあります。この定例会で、どのようなUXに関する問題を見つけたか、どのように改善をしたのかを、具体的に見聞きすることで、UX対しての理解を深め、参加しているメンバーそれぞれに、 UXの問題に気付ける目を養ってもらう ことも、この定例会の目的の一つです。それぞれの立場の人が少しでもUXに関して意識を持ってもらうことの積み重ねで、プロダクト全体の品質が上がることを期待しています。 会社全体の状況を俯瞰できる機会を設けること 前述の内容と少し内容がかぶりますが、日々の業務がチーム内だけに留まっていると、どうしても仕事に対する視点が凝り固まりがちです。この定例会で、他のチームに対して、自分が作っているプロダクトについて説明すると、そのプロダクトをより客観的に見やすくなります。他のチームが作るものと比較して、「ここは考えが不足しているかもしれないな」ど新しい気づきを得られることがあります。さらには、自分が関わっているプロダクトが、会社においてどのような位置づけなのかを意識するきっかけにもなります。 そうして、自分が普段作っているプロダクトをどうしていけば良いか、 より俯瞰して考えられるようになる ことも、この定例会が期待していることの一つです。 終わりに 週1回1時間という限られた時間のため、この定例会でできることは多くはありません。そのため、各チームの参加者に具体的なアクションは任せることとなり、直接的に成果に繋がっているかどうかは、まだまだ目に見えてわかる状態にはなっていません。その点は今後の課題の一つです。 とはいえ、この定例会で出た意見をもとに、それぞれのチームで改めて改善を進めたということも、日々報告いただいていますし、定例会によく参加しているメンバーが表彰されたりということもあり、微力ながら助けになれているのではないかと思います。 先日、この定例会に参加してるメンバーに、この定例会に参加していて感じたことを聞いてみました。 その声を以下に抜粋してみます。 (エンジニア) チーム内だけだと思考が凝り固まってしまってる感があるので、聞いてもらえるとうれしい気持ちになる (ディレクター) 小さなチームで似たようなサービスをずっとやっていると、改善の方向が同じになりがち。相談するといろいろな視点で見れるようになる。 (エンジニア) 自分たちがやっていることを別チームに話す機会はなかなか無い。UX定例会でどのように説明したら良いのか考えて話していると、それまで気づかなかった新しい視点に気づくことがある。 (エンジニア) デザインの意図とかを気にせず実装していたけど、UX定例会で施策の意図を突っ込まれたときに何も答えられなかった。今は、答えられるよう意識する機会が増えた。 この定例会で目指していた効果が垣間見え、やっていてよかったと感じました。 今回は、会社全体のプロダクトのUX品質底上げのための活動として「UX定例会」をご紹介しました。他にもそのために行われている活動がありますが、それはまた別の機会でご紹介できると思いますので、お楽しみに! 明日の記事は id:dorapon2000 さんです!
アバター
この記事は モバイルファクトリー Advent Calendar 2020  3日目の記事です。 デザイナーの id:momoyagi です。UI/UXについて考えたり、グラフィックデザイン作ったりしてます。前述の通りの役職なのでデザイン系の記事をすすめられたんですけど、コミュニケーションも技術じゃん(屁理屈)と思っているので雑談の話します。 弊チームのゆるく無駄な雑談   ようしゃべる 私の所属するチームでは、夕方頃、Google Meetを利用して進捗報告のための夕会後5〜10分の短い雑談タイムを設けている。 話題はTwitterのトレンドから休日の話等、他愛もない事柄ばかりで聴いても聴かなくてもなんの支障もない話題が中心。 度々、事業やタスクについての相談も行われるが普段は無駄そうなことばかり、というかほとんど無駄である。 チーム構成や世界情勢 弊チームでは、中心となる社内のメンバー数人と業務委託の社外メンバーとの合同でプロジェクトをすすめている。 合同のチームが出来て数ヶ月後、世界中で課題となっている感染症が流行り始め、20年2月にはリモートワークへ移行した。こんな事もあろうかと、と準備を進めていたバックオフィスの皆さんにはひたすらに感謝である。 これまではオフィスや休憩室で顔を合わせ、すれ違えば挨拶をするような環境だったにも関わらず、一切の交流がない孤独な業務環境に身を置くことになってしまった。 また、リモートワークに入ってから入社してきた新卒のメンバーは、(ほとんど)会ったことすらなかった。 きっかけ なぞの人物たち 相手は何を考えているのか、コンディションや人間性を知る手段がなくなってしまった私達。 リモートワーク移行直後、話し相手が居ないストレスで個人的には少し疲れていたのだが、PMとの1on1の中で雑談タイムを設けて見ようという案が出る。それだよそれそれ! その日の夕会に早速雑談タイムが採用される。いざ話すぞ、というタイミングでチームの中では心理戦が始まった。「何を話せばいいんだ?」 雑談のお作法 さあ話すぞ!と意気込むと意外と話題はなく沈黙が続きそうだったので、陽キャコミュ強になることを決意した。 具体的には、雑談タイムのはじめに合図を送り「話したい事ある人〜」と声をかけるだけで、特に何もなければTwitterのトレンドを見たり自分語りをするだけである。話題があれば、追って話題は出てくるもので、聴いていれば丁度10分たつかな…という感じだった。 毎日他愛ない話をし続ければ、メンバーの人間性がわかったり新情報が発覚して、愛着や安心感が得られた(と思う、所感です)。 雑談が得るもの 個性豊か 雑談で得られるものは業務の進捗じゃなくて「チームメンバーの印象」で、これがあるだけでも信頼感を持てたり、安心してコミュニケーションが取れるようになるだろうと思う。 ビジネスとはいえ、結局は人と人の関係で成り立つ小さな社会で、スムーズなホウレンソウやいつでも相談できる環境を用意するのも、大事な努力の一つではないかと考えている。   ポエムは以上です、話していこうね😊
アバター
この記事は Perl Advent Calendar 2020 と モバイルファクトリー Advent Calendar 2020 2日目の記事です。 こんにちは。 id:kfly8 です。 今年、会社の勉強会の時間を使ってちょこちょこと、 perl-users.jp を静的サイト化しました。 せっかくなので、2008年から2010年の記事を読み返したのですが、勉強になりました。 温故知新というと大仰ですが、昔から今にかけての変化も触れながら、 個人的に面白かった記事をいくつか紹介してみたいと思います。 1. 元々の文字列を破壊しないように置換する方法 最も素朴な方法は、別の文字列に退避する方法だと思いますが、2行になって面倒です。 my $old = "dog" ; my $new = $old ; $new =~ s/ dog / cat / ; 2008/12/12 で紹介されていたのが、 List::MoreUtils#apply を使う方法です。 my $new = apply { s/ dog / cat / } $old ; 確かに、ワンステートメントで意図がはっきりして読みやすいです。 さらに、 2008/12/12 で、代入文をカッコにくくる方法が紹介されています。シンプル。 ( my $new = $old ) =~ s/ dog / cat / ; 今だと、perlのバージョンが5.14 以降であれば、replace optionを使って、よりスッキリ書けますね。 my $new = $old =~ s/ dog / cat /r ; 2. SQLiteでdbをオンメモリにする SQLiteを利用してテストを書く時、dbのファイルを管理するのが面倒、オンメモリで処理してしまえば、ファイルが意図せず消えなかった時のことを考えなくて便利という話です。 2008/12/10 時点では、 DBI->connect('dbi:SQLite:','',''); のように、dbnameを指定しない方法が紹介されていましたが、 今だと、 DBI->connect('dbi:SQLite:dbname=:memory:','','') といった具合に :memory: を指定して明示的に書けます。 3. Errnoでエラー処理する 「ディレクトリが存在しなければ作成」という処理を素朴に書けば、ディレクトリの有無確認( -d )して作成する( mkpath )すると思います。 ただ、この書き方だと、 -d のあと mkpath を呼び出すまでの束の間に、別の処理がディレクトリを作成してしまう可能性はあります。 if (! -d $dir ) { mkdir $dir or die "failed to create dir: $dir : $! " ; } この可能性を考慮するなら、まずディレクトリの作成を試みて、返されたエラー変数( $! )とErrnoの定数と数値比較するという話が、 2009/12/05 の記事で紹介されています。 unless ( mkdir $dir or $! == Errno::EEXIST) { die "failed to create dir: $dir : $! " ; } こういった低レベルな内容だと、10年経っても原理は変わらないですね。 4. DBIx::TransactionManagerとSQL::Maker 2010/12/10 にDBIx::TransactionManager、 2010/12/13 にSQL::Makerの紹介がされています。この2つのモジュールは、今でも使われているORMの Aniki や DBIx::Sunny の内部で利用されています。 この2つのモジュールに関して、DBIx::Skinnyからパーツを分解して出来ていることが、 2010/12/05 の記事でわかります。歴史的経緯がわかって面白いです。 5. 記号プログラミングアドベントカレンダー Acme::EyeDrops便利 記号プログラミングのネタだけで、25日走りきっている アドベントカレンダー がありました。「どこへ向かっているの?」というはてぶコメントがありましたが、10年経っても色褪せないコメントだと思いました。 記事は、1日目BrainF*ck、2日目PHP、3日目JavaScript、4日目Ruby、5日目Perl・・・と続きます。 おわりに 紹介できた内容はほんの一部ですが、先人の記事は面白かったです。 過去の記事を読み返してみるのもたまには良いかもしれないと思いました。 明日の記事は Perl Advent Calendarは、hkobaさん、モバイルファクトリーAdvent Calendarは、 id:momoyagi さんです!
アバター