TECH PLAY

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

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

226

この記事は モバイルファクトリー Advent Calendar 2020 25日目の記事です。長かったアドベントカレンダーもこれがラストです。今年も25日まで毎日技術記事を楽しみに過ごせました。 こんにちは、ブロックチェーンチームのソフトウェアエンジニア id:odan3240 です。 ERC721 の extension ERC721 は Ethereum における Non-Fungible Token (以下 NFT) の規格です。ERC721 には様々な extension が存在しており、 OpenZeppelin では次の種類の extension が実装されています。 Mintable NFT を mint できる Burnable NFT を burn できる Enumerable NFT を数えられる Metadata NFT とオフチェーンのメタデータを繋げられる Pausable NFT の転送を停止できる この中でも Enumerable は NFT を数えられるようになる一方で、gas used (以下 コスト) 増加することが知られています。 speakerdeck.com 今回 Enumerable 以外の extension に対してコストの増加を調べたので、これを共有します。 実験の設定 実験の各パターンは次の通りです。Mintable をベースに他の extension を追加しています。 Basic (Mintable) CaseBurnable (Mintable + Burnable) CaseEnumerable (Mintable + Enumerable) CaseMetadata (Mintable + Metadata) CasePausable (Mintable + Pausable) バージョン solidity: 0.5.17 Ethereum の hardfork: Muir Glacier @openzeppelin/contracts: 2.5.1 1 ソースコード github.com 実験結果 deploy/mint/transferFrom の各コストは次の通りです。 deploy mint transferFrom Basic 2,113,681 67,978 61,602 CaseBurnable 2,259,122 67,978 61,602 CaseEnumerable 2,455,003 153,561 92,696 CaseMetadata 2,538,433 67,978 61,624 CasePausable 2,541,757 67,978 62,497 わかりやすく gas price が 70Gwei、円と ETH のレートが 62608円/ETH としてコストを日本円に換算すると次のようになります。 deploy mint transferFrom Basic ¥9,263 ¥298 ¥270 CaseBurnable ¥9,901 ¥298 ¥270 CaseEnumerable ¥10,759 ¥673 ¥406 CaseMetadata ¥11,125 ¥298 ¥270 CasePausable ¥11,139 ¥298 ¥274 すでに知られているように Enumerable を実装するとトークンの mint/transferFrom のコストが約2倍に増加することがわかりました。 また deploy のコストについては、各 extension を実装すると増加し、Pausable を実装するのが約1.2倍と一番大きな増加率になることがわかりました。 まとめ ERC721 の extension の違いによるコストの増加について調べました。 deploy はどの extension でもコストが増加しました。トークンの mint/transferFrom については Enumerable を実装するとコストが増加しました。 どの extension を ERC721 に実装するかは作りたいトークンの要件にもよりますが、実装すると何かしらのコストが増加する可能性を考慮しておくと良さそうです。 無事にモバイルファクトリー Advent Calendar 2020は25日完走できました。それでは皆さん良いお年を! @openzeppelin/contracts の最新版は v3 系ですが、v3 系は Enumerable がデフォルトで組み込まれていて実験に適さないので v2 系 ↩
この記事は CTOA Advent Calendar 2020 と モバイルファクトリー Advent Calendar 2020 の24日目の記事です。また先日の Gaiax Technical Meetups の登壇内容を元にした内容になります。 こんにちは。エンジニア組織開発責任者のkobaken( @kfly8 )です。 明日はクリスマスですね。娘4歳はサンタさんにレゴをリクエストしていました。届くといいですね😊 今年の2月から、モバファクは新型コロナの影響でフルリモートの働き方に変わりました。 その影響もあり、組織開発観点では社内のコミュニケーションに課題を感じた1年となりました。 エンジニア組織に限らず、組織全体において、 「他のチームがどんなことをしているのかわからない」 「どんな人かわからなくて、話かけづらい」 「さみしい..!」 なんて声を聞きました。特に、今年入ったメンバーからはよく聞きました。 新人の1人に聞くと、定期的に雑談する工夫をしたそうです。 tech.mobilefactory.jp そんな話を聞くと、個人やチームの工夫があって組織はうまくいっていることを改めて感じます。一方、組織開発の担い手として、前提が変わってしまったことによる組織課題を丁寧に解決することが求められました。例えば、オンボーディング、リモートワークでのコミュニケーション、1on1、エンゲージメント改善のためのガイドや新人研修といった研修プログラムのオンライン化、新しい働き方に合わせた人事制度・福利厚生の改変などが挙げられます。 前置きが長くなりましたが、こういった組織開発の一環として、社内勉強会の改善事情について書きたいと思います。社内勉強会はスキルアップに注目されがちだと思いますが、コミュニケーションを促進し、シナジーを生み出す実感があり、フルリモートならではの課題をいくらか解決する施策だと思います。シナジー効果により、1人で達成できないことを組織で協力して達成しやすくなっていると嬉しいですよね。 他方で、社内勉強会の運用は良いことだけでなく問題もたくさんありました。運用に悩んでいる方もいらっしゃると思います。どう改善したのか一つの事例として読んでもらえればと思います。 モバファクの社内勉強会の概要 モバファクの社内勉強会は、1日1時間、コアタイム外はいつ誰でも勉強会して良い制度です。名前は「シェアナレ!」です。 最近だとこんな勉強会がありました。 最強の〇〇環境プレゼン大会 エラー設計ワーキンググループ スクラムガイド2020読書会 TCPにダイブ! Certified Jenkins Engineer 2020になるまで UX探検隊 先週金曜日に開催された勉強会 先週金曜日に開催されていた"最強の〇〇環境"と題したLT会では、最強のノマド環境、最強のインターネット環境、最強の育児環境といった話をしていました。最近だと、アドベントカレンダー執筆のためのもくもく会が多く開催されていたりします。おとといは今年1年をふりかえるエンジニアのLT会がありました。 現状のモバファクの社内勉強会の状況を簡単にまとめると、次のような具合で組織にいくらか浸透していると感じます。 盛んに開催されている 10年以上続いている 知識を得るだけでなく、お互いを知る場にもなっている 問題はたくさん 現状、ほぼ毎日開催され、いくらか組織に浸透しているとは思いますが、問題はたくさんありました。中には現在も進行形の問題もあります。勉強会を運営している人にとって、身に覚えのある問題もあると思います。 問題は複雑に絡み合っている こういった組織の問題を分析する時、面白い所が、一つの原因があるわけではなく、互いに繋がっていて原因をたどろうとしてもうまくいかないところです。典型的な悪手は、誰かのせいにすることです。全員にとっての100点はないですが、どうなったら嬉しいか、少しずつ改善していくことが解決の糸口だと思います。 丁寧に解決し続ける 行ったことのポイントは次の4つです 地続きのコミュニケーション 仲間を巻き込む サーベイ。そして対話 草の根活動 1. 地続きのコミュニケーション 勉強会で話を聞いたら「ハイ!おしまい!」でなく、勉強会の始まる前から終わった後まで地続きでコミュニケーションを設計すると良いと思います。 例えば、誰かがいいアウトプットをしたなら、イイネと素直な気持ちを表明する。めんどうなことは続かないですが、イイネといったちょっとしたリアクションが話した人のやる気や、社内勉強会の活性化に貢献すると思います。 社内バズ?みたいなこともある 最近だと「スクラムガイド2020でましたね」と誰かがチャットで話せば「一緒に読みますか」といった話に繋がっていました。 勉強会の場だけでなく、会社の中に会話が溶け込むのが理想だと思います。 2. 仲間を巻き込む あーしようこうしようと一人相撲をしても、当たり前ですが文化はできないです。社内勉強会の文化を改善していくにあたり、公募で運営メンバーを募りました。どの施策よりも効いたと思います。運営メンバーの皆には感謝です。 運営の仲間を巻き込む狙いは2つありました。 1つ目の狙いは、自分ごとにする現場目線での発信です。それまでは管掌部署のヒューマンリレーションズ部が発信していましたが、メンバーの話を聞くと発信を自分ごとにしにくかった面が正直ありました。会社にはいろんな人がいるので、誰が伝えるかで伝わり方も変わると改めて感じました。同じ現場の人が、会社を良くするために前向きに取り組みをしていたら、良い刺激を受けるんじゃないかと思います。 2つ目の狙いは、文化の担い手づくりが狙いです。組織を変えられる実感を持つ人が増えた方が、自分たちの会社をハンドメイドする感覚が持てて楽しいんじゃないかと思います。そんな実感なく、将来、組織を良くしてくださいと言われても、どこからどうすればいいか困ると思います。組織開発をじっくり実践する場になればと思っています。 こうやって、少しずつ仲間を巻き込んでいきたいです。 3. サーベイ。そして対話 これも当たり前ですが、当てずっぽうで施策を打つわけにはいきません。どれだけ社内勉強会を薦めたいかNPSとフリーコメントを集めて、運営チームで”診断型組織開発”、つまりデータを観察し、対話しながら解釈をして、仮説をたて、改善のアクションにに繋げるといったことをしていきました。 徐々に良くなってきている サーベイの結果を見て、杓子定規に受け取るのではなく、どういう意味があるのか対話をしていきます。例えば、5点が多いのはなぜか?8点は多いけれど、9点が少ないのはなぜか?推奨となるとためらう気持ちが生まれやすいのか?解釈を話します。 同時に、どんな勉強会でありたいかといった話も混じえます。何というか眉間にシワを寄せて話し合っても、勉強会の楽しい雰囲気を作れないと思います。何が好きか、やりたいか、こういった価値観の要素は制度・仕組みの設計以上に育てにくいところなので、大切に拾っていきたいと思っています。 4. 草の根活動 ここまでおおよそ制度・仕組みの改善ですが、やはり、草の根活動はあります。例えば、発表できそうな人を探す、定期イベントを開催する、新人研修に組み込むといったことをしています。 例えば、参加しやすく、発表しやすくを狙いに、社内カンファレンスを開催しています。5月に開催された「新人研修では聞けない〇〇な話」では、キャッチーさ・お祭り感を演出しています。 ちょっとしたお祭り感のある勉強会も開催 全部が全部こんな感じで頑張ると大変ですが、フルリモートの世界観になって、お互いの気配を感じにくくなっているので、お祭り感の演出も大切かなと思っています。今は部屋の移動もなくサクッと勉強会に参加できて、それこそ、ながらで参加することもできるので社員の半数が参加することもあります。皆の様子が見えるのは安心感が生まれると思います。 運営として、意識していること 施策の例を挙げてきましたが、当然、状況次第で施策も異なると思います。また、制度・仕組みといったハード面での改善実例を中心に挙げていますが、そこからきちんと運用し続け、文化・価値観といったソフト面で根付くことが本質的に大事だと思います。そのためにも、どういったことを意識しているのかを書いて終わりにしたいと思います。 元々の文化を活かす 余裕を持つ 丁寧に。丁寧に。 1. 元々の文化を活かす モバファクの社内勉強会の場合「話したいから話す」といった自主性に根ざした色があります。そういった色は簡単には出来上がらず、貴重な価値だと思います。運営が良かれと思った改善としても、文化を殺していないか、様子の観察は忘れないようにしたいです。 2. 余裕を持つ 会社でやっていることなので、効果、成果を求めるところはあります。個人的にも事業インパクトを生み出す事例が生まれないかと楽しみではあります。ですが、求めすぎ余裕がなくなると、発表のハードルが上がり、参加へのプレッシャーが大きくなり、制度の存在意義が不明瞭になります。フルリモートの今なら、チームを超えたコミュニケーションを促せるなら良しと捉え、いい意味で無駄を楽しむ方が良いのかなと思っています。 3. 丁寧に。丁寧に。 組織を変化させるには、丁寧さが必要だと思います。丁寧というのは、実態を見て解決の手立てを考えることや、繰り返し伝えていくことです。組織には多くの人がいて感じ方も人それぞれなので、全員が満点になることはないです。例えば、今年フルリモートに舵を切り、覚悟を持って断行した企業もあると思います。会社のために良かれとやっていることだと思います。だとしても、ぞんざいにして良い理由はなく、変化に適応することは負担になるので、丁寧に、丁寧に行うことを意識しています。 まとめ 組織開発の一環として、社内勉強会の改善事例をお伝えしました。社内勉強会は、スキルアップといった側面だけでなく、お互いを知りシナジーを生み出すコミュニケーション効果もあります。また、仕組みがあればうまくいくものではなく、その運用改善のため、丁寧に自分たちの文化となるよう解決を進めています。 関連 一つ一つの具体的な施策を広報がインタビューしてくれています。よければこちらもご笑覧いただければと。 corpcomn.mobilefactory.jp corpcomn.mobilefactory.jp corpcomn.mobilefactory.jp corpcomn.mobilefactory.jp 明日は最終日ですね!明日の記事は、CTOAアドベントカレンダーはCTOA代表理事の松岡剛志さん、 モバファクのアドベントカレンダーは、 id:odan3240 です。お楽しみに!
この記事は モバイルファクトリー Advent Calendar 2020 23日目の記事です。 こんにちは、 id:nesh です。 はじめに 今回の記事は2年前の記事と関連して、モバイルアプリのテストを自動化する話です。過去の記事 AppiumでAndroidアプリの自動テストをPerlで書いてみた - Mobile Factory Tech Blog では、Perl + Appium を使ったAndroidアプリのテストについて書きました。 今回の記事には、Appium + AWS Device Farm + Jenkins を使い、Androidのモバイルアプリの動作確認を複数端末における自動化について書きます。 背景 運用中サービスのアプリに変更を入れる場合、当サービスがサポートする端末でアプリの動作確認をするのが理想的だと思います。 コロナ禍前は、会社に検証用の端末があるため、サポートする端末や問題がありそうな端末を複数台確保して、動作確認を行いやすかったです。 しかし、2月からフルリモートで働くようになってから、手元に検証用の端末は(複数台)ない状態になっています。 手軽に複数端末で、モバイルアプリのテストをしたいので、今回目をつけたのは AWS Device Farm です。 AWS Device Farm Device Farm は、実際に Amazon Web Services (AWS) によりホストされている電話やタブレットで、Android や iOS、およびウェブアプリを物理的にテストしてやり取りできるアプリテストサービスです。( AWS公式サイト より引用) モバイルアプリの開発における様々な端末での動作確認を楽にしてくれるAWSのサービスです。 このサービスの使い方は2つあります。 自動アプリテスト リモートアクセスの操作 今回は様々の端末での動作確認を自動化したいので、自動アプリテストを使います。 自動アプリテスト 事前準備 Testing mobile apps across hundreds of real devices with Appium, Node.js, and AWS Device Farm | Front-End Web & Mobile まずやっておくことは、AWS Device Farm上の準備ですが、上記のブログ記事を参考に作業します。 テストするアプリを用意 動作確認をAppium (Node.js) のテストで実装 AWS Device Farm上で設定し、自動テストアプリを実行 テストするアプリを用意 テストするアプリはAWS Device FarmやAppiumが用意してくれたサンプルアプリを使うのもできますが、今回は自分で作ったHelloWorldを表示するアプリを使います。 GitHub - fadlil/HelloWorld app/outputs └── app-debug.apk 動作確認をAppium (Node.js) のテストで実装 テストしたいことは、アプリを起動できるかどうかだけにします。 // テストフレームワーク var expect = require( 'chai' ).expect; // node.js でappiumを使う var wd = require( 'wd' ); var driver = wd.promiseChainRemote( { host: 'localhost' , port: 4723 } ); var assert = require( 'assert' ); describe( 'AWSDeviceFarmReferenceAppTest' , function () { before( function () { this .timeout(300 * 1000); return driver.init(); } ); after( function () { console.log( "quitting" ); } ); // アプリが起動できて、'Hello World!!' が表示されるテスト it( 'test_app_is_loaded' , async function () { const element = await driver.elementById( "com.example.nesh.helloworld:id/change" ); expect(element).to.exist; } ); } ); このテストをそのままローカルで実行すると失敗します。 driver.init() に必要なデバイスの情報が足りないからです。 ただ、AWS Device Farm上で実行される時、これらの情報がよしなに補完されます。 このテストファイルをAWS Device Farmで使うために、 npm-bundle と zip化する必要があります。 AWS Device Farm上で設定し、自動テストアプリを実行 新しくプロジェクトを作成 新しいrunを作成して、必要な項目を設定 アプリの *.apk ファイルをアップロード zip化されたテストファイルをアップロード デバイスを選択 必要設定を埋めたら、自動アプリテストを実行 必要な作業は大体上記の通りです。 ここまでの作業で、モバイルアプリを複数端末で手軽に自動テストできるようになりました。 しかし、AWS Device Farm上の操作自体が手間になると思います。 この手間を無くし、継続的にテストを回せたいと思っているので、Jenkins で自動化することにしました。 Jenkinsで自動化 自動化するのは、 AWS Device Farm上で設定し、自動テストアプリを実行 の操作です。 操作自体は単純で手間ではないのですが、自動にできる部分は自動化したい気持ちです。 自動化するといっても、JenkinsにAWS Device Farm用のプラグインが用意されてるので、簡単に自動化できます。 AWS Device Farm の Jenkins CI プラグイン - AWS Device Farm Jenkinsで使うプラグインは aws-device-farm | Jenkins plugin です。 この記事は上記に用意したサンプルリポジトリを使った場合、設定のスクリーンショットをいくつか貼ります。 Jenkinsのプロジェクトの設定1 Project と Device Pool はAWS Device Farm上に設定されてるものを参照します。 Application はサンプルリポジトリ上のアプリファイルのパス Jenkinsのプロジェクトの設定2 今回のテストは Appium (Node.js) を使うので、該当テストファイルのパスを入力します。 Jenkinsのプロジェクトの設定3 テスト環境の設定は、AWS Device Farm上に手動で自動アプリテストを実行した時のものをそのまま使います。 Jenkinsでの自動化が成功 これで、Jenkinsでの自動化のための設定ができて、実行して成功できました。 試してみた所感 AWS Device Farm の自動アプリテストはアプリ開発時の動作確認に便利 最初の設定も簡単で、テストするアプリさえあればすぐにできる テストデバイスの起動時間が合計1,000分まで無料なので、気楽に試せる アプリ開発時のサポート端末での起動確認などで使えそう 自動化に関しては、アプリのビルドやテストファイルの npm-bundle + zip などの作業も自動化すれば理想的 日々の面倒な作業を自動化して、快適な開発ライブを充実しましょう。 明日の記事は id:kfly8 さんです!
この記事は モバイルファクトリー Advent Calendar 2020 22日目の記事です。 こんにちは。エンジニアの id:Eadaeda です。普段はサーバーサイドの面倒を見ています。 DocBase 弊社ではドキュメント共有ツールとしてDocBaseを利用しています。Markdown形式で書いた文章を投稿することが出来、記事の埋め込みや検索なども便利です。私もチケットのワークログや議事録、シェアナレ!で書いたものなどを投稿しています。 docbase.io ブラウザ内のエディタでDocBaseの記事(DocBaseではメモとよんでいます)を編集する場合は、エディターへ画像や動画などのファイルをドラッグアンドドロップすることでアップロードすることが出来ます。その時、以下のように自動でMarkdownを挿入してくれます。 画像なら ![ ファイル名 ]( URL ) 動画などならリンク [ 動画 ]( URL ) なので、ブラウザ内のエディタを使っている場合、ファイルを挿入したい位置にカーソルをあわせ、ドラッグアンドドロップでアップロードするだけで良いのです。特段珍しい機能とは思いませんが、嬉しい機能ですよね。 ところで、私は何らかの文章を書くとき、EmacsやVim、nanoといったコマンドライン上で動作するテキストエディタを使っていて、この記事もそこで書いています。ちなみにどのエディタを使っているかはナイショです。 こういったエディタを使うのは、出来るだけターミナルから動きたくないからなのですが、そこでメモを書いていると困ってしまうことが1つあります。メモに貼り付けたい画像などのURLが先にわからないことです。 ![ image ]( ここがわからない ) これはなかなか大変なことです。書いているときは、例えば画像を挿入したい位置に __ここに画像__ などのマーカーを置いておき、あとからブラウザ内エディタにコピペ、マーカーを探して順番通りに画像をドラッグアンドドロップしていかなければなりません。挿入したいファイルが現れるたびにアップロードし、URLを取得する方法もありますが、先に述べたように私は出来るだけターミナルから動きたくありませんでした。 これをなんとかしたい…というよりはなんとかしなければならなかったので、なにか使えるものは無いかと探していました。するとDocBaseが公開しているAPIを見つけました。 help.docbase.io このAPIはレスポンスとして以下のようなJSONを返すようです。 [ { " created_at ": " [ここは投稿した時間] ", " id ": " [ここはID] ", " markdown ": " ![example.png]([ここは画像へのURL]) ", " name ": " example.png ", " size ": 285 , " url ": " [ここも画像へのURL] " } ] 嬉しいことにMarkdownへ埋め込みが返されます。これをメモに貼り付ければ良いので、一度もターミナルから離れずにメモを完成させられそうですね。 CLIツールにする さて、毎回 curl コマンドを叩くのも良いのですが、いささか書き換えが面倒です。アップロードしたいファイルへのパスを与えるだけでアップロードまで行ってくれるシェル関数を書くなどをするのも有りでしょう。 でも今回は、Goで専用のCLIツールを作ってみようと思います。ここでようやく本題です。前置きが長くて申し訳ない。ちなみになぜGoでCLIツールを作るのかと言うと、単純にGoでCLIを作るのが好きなのと社内でGoを使ったCLIツールを作る人が増えてほしいからです。 GoにはCLIフレームワークとして公開されているものがたくさん有りますが、今回は cobra を使います。この間公開された GitHubのCLIツール にも使われていますね。私もよく使いますが、後に紹介する viper との連携が強力で好きです。 github.com github.com まずはどんなものを作ったのか、どんなものが作れるのかを理解してもらいたいので、ヘルプの出力と upload サブコマンドで example.png をアップロードしたときの様子を以下のGIFに示します。 このような感じですね。非常にシンプルです。 実装 さて、早速私が実装したものを見てもらいます。 package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "github.com/spf13/cobra" "github.com/spf13/viper" ) // docbaseコマンドの本体 var rootCmd = &cobra.Command{ Version: "0.0.1" , PreRunE: func (cmd *cobra.Command, args [] string ) error { // API Tokenとチーム名は必須なので、どちらかが空な場合はアプリを終了する if len (viper.GetString( "token" )) == 0 { return errors.New( "DocBase API Tokenが空です" ) } if len (viper.GetString( "team" )) == 0 { return errors.New( "チーム名が空です" ) } return nil }, Run: func (cmd *cobra.Command, args [] string ) { fmt.Println( "これはサブコマンドを指定しなかったときに実行されるコードだよ" ) }, } // uploadサブコマンド // docbase uplaod という感じに呼び出せる var uplaodCmd = &cobra.Command{ Use: "upload" , Run: func (cmd *cobra.Command, args [] string ) { err := upload(args[ 0 ]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit( 1 ) } }, } func upload(path string ) error { filename := filepath.Base(path) fp, err := os.Open(path) if err != nil { return err } defer fp.Close() content, err := ioutil.ReadAll(fp) if err != nil { return err } // viperからAPIトークンとチーム名の値をもらう token := viper.GetString( "token" ) team := viper.GetString( "team" ) // リクエストボディを作る // [ // {"name": filename, "content": content} // ] data, err := json.Marshal([] struct { Name string `json:"name"` Content [] byte `json:"content"` }{ {Name: filename, Content: content}, }) if err != nil { return err } body := bytes.NewBuffer(data) // http.Request{}を作る r, err := http.NewRequestWithContext( context.Background(), "POST" , fmt.Sprintf( "https://api.docbase.io/teams/%s/attachments" , team), body) // ヘッダーに環境変数から拾ったアクセストークンをセット。これがないと弾かれる r.Header.Set( "X-DocBaseToken" , token) r.Header.Set( "Content-Type" , "application/json" ) // ここから実際にリクエストを投げる処理 client := &http.Client{} res, err := client.Do(r) if err != nil { return err } defer res.Body.Close() // レスポンスを読んで、Stdoutに出力 resBody, err := ioutil.ReadAll(res.Body) if err != nil { return err } // Jsonの出力 var b [] byte resJson := bytes.NewBuffer(b) json.Indent(resJson, resBody, "" , " " ) fmt.Println(resJson.String()) return nil } func init() { // オプションの追加 // PersistentFlags に追加すると、追加されたcobra.Commandに追加したサブコマンドでも使えるオプションになる // そうしたくない場合は Flags に追加すれば良い // --tokenオプション、文字列を受け付ける。デフォルト値は ""、最後の引数は --helpで出力されるそのオプションの説明 rootCmd.PersistentFlags().String( "token" , "" , "DocBase API Token" ) // こっちは--teamオプション。 rootCmd.PersistentFlags().String( "team" , "" , "Team name" ) // viperで値を管理 // "token" に、`token`という名前のオプションの値をバインドする viper.BindPFlag( "token" , rootCmd.PersistentFlags().Lookup( "token" )) // "token" に、`DOCBASE_TOKEN` という環境変数の値をバインドする viper.BindEnv( "token" , "DOCBASE_TOKEN" ) viper.BindPFlag( "team" , rootCmd.PersistentFlags().Lookup( "team" )) viper.BindEnv( "team" , "DOCBASE_TEAM_NAME" ) // uploadサブコマンドを追加する。 rootCmd.AddCommand(uplaodCmd) } func main() { if err := rootCmd.Execute(); err != nil { os.Exit( 1 ) } } かなり愚直な実装になりましたが、とりあえずできました。ではこれを使って実際にファイルをアップロードしてみましょう $ export DOCBASE_TOKEN= "ここにあなたのアクセストークン" $ export DOCBASE_TEAM_NAME= "ここにあなたのチーム名" $ go run main.go upload ./example.png これで、以下のようなレスポンスが返ってきました。 [ { " id ": " アップロードしたファイルのID ", " name ": " example.png ", " size ": 285 , " url ": " https://image.docbase.io/uploads/アップロードしたファイルのID ", " markdown ": " ![example.png](画像へのURL) ", " created_at ": " 2020-12-08T17:17:09+09:00 " } ] アップロード出来ていそうですね。ここから "markdown" の値を取り出し、メモにを貼り付けるだけです。 あとは、 go build や go install でバイナリを得れば良いですね。 $ go build -o docbase $ mv ./docbase " $PATH の通っているところ" $ go install オプション 今回の実装では、 cobra と viper を組み合わせて、APIトークンとチーム名をオプションで切り替えられるようにしました。ちょうど実装の以下の部分ですね。 func init() { // オプションの追加 // PersistentFlags に追加すると、追加されたcobra.Commandに追加したサブコマンドでも使えるオプションになる // そうしたくない場合は Flags に追加すれば良い // --tokenオプション、文字列を受け付ける。デフォルト値は ""、最後の引数は --helpで出力されるそのオプションの説明 rootCmd.PersistentFlags().String( "token" , "" , "DocBase API Token" ) // こっちは--teamオプション。 rootCmd.PersistentFlags().String( "team" , "" , "Team name" ) // viperで値を管理 // "token" に、`token`という名前のオプションの値をバインドする viper.BindPFlag( "token" , rootCmd.PersistentFlags().Lookup( "token" )) // "token" に、`DOCBASE_TOKEN` という環境変数の値をバインドする viper.BindEnv( "token" , "DOCBASE_TOKEN" ) viper.BindPFlag( "team" , rootCmd.PersistentFlags().Lookup( "team" )) viper.BindEnv( "team" , "DOCBASE_TEAM_NAME" ) // uploadサブコマンドを追加する。 rootCmd.AddCommand(uplaodCmd) } オプションが指定されればその値を、されなければ環境変数を使う。というような感じで viper が値をよしなに決定してくれます。これにより、以下のような使い方で、APIトークンとチーム名を切り替えられるようになりました。 # オプションなし。環境変数を読みに行く $ docbase # --tokenに値を渡す # APIトークンは `hoge` として処理がすすむ。チーム名は環境変数の値のまま $ docbase --token=hoge # --teamも渡してみる # APIトークンは `hoge`、チーム名は `fuga` として処理される $ docbase --token=hoge --team=fuga cobra , viper は非常に強力で他にもまだまだできることがあるので、是非READMEなどを読んでみてほしいです。 まとめ 今回は「ターミナルから動かずにDocBaseにファイルをアップロードしたい!」という願いを叶えるため、Goを使って、DocBaseにファイルをアップロードするCLIツールを作りました。フレームワークとしては cobra と viper を選択し、この後の機能拡張も考えて「アップロード処理を行う upload サブコマンド」として実装しました。これをきっかけに社内でGoが大流行すればいいなあと思っています。 また、本当にターミナルから一歩も離れないことを目指すのであれば、メモの更新・作成を行うサブコマンドを実装する必要がありますが、投稿する前にプレビューなども見たいので、そこは手動にすることとしました。他のAPIは今後ほしいという要望が(私から)出れば実装すると思います。 明日の記事は id:nesh さんです!楽しみですね!
こんにちは、エンジニアの 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 さんです!