叫ぶうさぎの悪ふざけ

うさぎが目印のWebエンジニアが、得たことや思ったことを言の葉に乗せて叫ぶ場所です。

学習記録:12月18日(火):はじめよう!要件定義 Chapter-06 [準備編]企画を確認する(つづき)

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の18日目のエントリーです。

ちなみにこのエントリー、最後に盛大なポエムというか、なんというかな話が入ります。そんな話はいらんわ、って場合は飛ばしてもらえればなあと思うんですが、本題よりそっちの方をエントリーにした方がよかったんじゃないかな…と思わなくもないので、もし何か思うところがありましたらば、忌憚のないご意見をいただけたらいいなあと思います。

閑話休題

本日は約4時間の研修を受けてきて、情報が溢れ気味でございます。が、間違いなく今後の仕事の仕方を変える内容でしたし、いかに僕らが 当たり前を具現化する仕事をしている のか、そしてそれを しょっちゅう忘れて頻繁に事故を起こす のかを思い知らされました。

研修内容をそのまま公開するわけにはいかないので研修レポートは書きません(手元にはしっかり残す)が、はじめよう!要件定義、を読み進めるにあたり、研修で学んだことなどは取り入れつつアウトプットにしたいと思います。

というわけで、前回の 学習記録:12月17日(月):はじめよう!要件定義 Chapter-05 要件定義、その前に & Chapter-06 [準備編]企画を確認する に引き続き要件定義の学習を進めます。

Chapter-06 [準備編]企画を確認する

昨日はこの章の途中までで終わっていました。企画書でゴール、つまり実現したいものを確認し、実現したいものからの逆算で「UI、機能、データ」を合意して進めていくわけですが、そのゴールが曖昧だとダメで、じゃあまとめるためにどんな項目があるのか、ということで以下の項目があればいいよ、まで進みました。

  1. プロジェクトの名称
  2. なぜこのプロジェクトをやるのか
    • 目的
  3. 何を
    • 目的達成のために作るもの
    • 作るものの説明
    • 作るものを利用する人
    • 利用する人が得られる便益
  4. どのように
    • 作るための体制
    • 期限

企画の内容を関係者が理解するための企画書

重要なのは企画を通すための企画書ではなく

  • 通った企画がいったいどのようなものなのか、を
  • 企画を実現するために作業する関係者、が
  • 理解するために必要な企画書

である、とあります。当たり前すぎてぐうの音も出ないんですけど、今まで多数の 企画を通すための企画書 を見てきましたし、そういう企画書ほど、 企画をするための企画書 だったりしたなあと思い返したりしました。

そもそも出発点は何かしらの情熱であり、それを実現するために前に進むための企画であり、その内容を共有するための企画書である。

だから、意思決定に対する「企画提案書」ではなく、実施が決まった企画の「決定報告書=プロジェクトそのものの仕様」が必要とあります。

つまり企画書を通す前の お願いします!の企画書 と 企画書を通した後にやる決定報告としての これをやります!の企画書=プロジェクト仕様書 が必要と理解しました。

RFPやプロジェクト計画書との違い

RFP支援の仕事もしたことがあるので、大変興味深い章です。要望を形にして、実現可能性や技術検討ができるようにするもの、くらいのふわっとした認識でした。

企画書が、RFP、いわゆる提案依頼書や、プロジェクト計画書と何が違うのか。

プロジェクト計画書から、マネジメント向けの情報を除外したサマリー

それが欲しいとあります。

  • 除外したいもの
    • 予算、リスクなど実務スタッフの担当領域外の情報
  • 不安や反発をなくす
    • プロジェクト仕様を知らないまま指示通りに作業しろ、と言われても…
    • 知らないことは先手を打つこともできない。プロなのに。
    • 何より人間は腑に落ちないことに不安や反発を覚え、手が止まりがち
    • 作業そのものが停滞しちゃうよね、と

企画書とは何か

プロジェクトについての紹介を、5分で手際よく事前知識のない人にできますか という問いかけが出てきます。

実務担当に不要な情報や、不安や反発を招きそうな内容だと、これから目指すゴールを紹介するのは難しいよな、と思います。

今から自分たちはこういうゴールを目指していくのだ と大雑把に理解できるために、 紹介するための企画書を作成 する。

そしてそれを共有する。 これが最初の材料 となる。

サンプル①:施設予約システム

サンプルとして、以下のプロジェクト仕様が掲載されています。僕なりに少し情報を足してみます。

サンプルには別紙参照の別紙がありませんが(そりゃそうだ)、それも含めてプロジェクト仕様書だと認識しました。で、そのプロジェクト仕様書を関係者で共有することで、自分たちが向かうゴールとその理由がわかる、ということかなと理解しました。

  • プロジェクトの名称
    • 施設予約システム
  • なぜ
    • 目的
      • インターネットを通じ、利用者が手軽に施設予約できるようにすることで、施設の稼働を活性化する
  • 何を
    • 目的達成のために作るもの
      • インターネットからの施設予約
      • 予約状況の確認
      • 実績データの他のシステムとの連携
    • 作るものの説明
      • 別紙参照(各機能の必要性とかUIとかかな)
    • 作るものを利用する人
      • 一般利用者
      • 窓口担当者
      • マネージャ
      • 経費部門
      • システム管理者
        • アカウント管理にシステム管理者を常に含めるのは重要
    • 利用する人が得られる便益
      • 別紙参照(フローや図があったりして別資料なのかな)
  • どのように
    • 作るための体制
      • 別紙参照(体制図が決まってないからかな)
    • 期限
      • 今年度中に完成
      • 新年度から運用開始

書籍にはもう1つサンプルがあったのですが、そちらは割愛します。ゲームを作るとなった場合にどうなるか、が書かれていました。

コラム:企画の良し悪し

企画もなしにいきなりソフトウェアは作れないよね、要件定義できないよね、狙いや企てのない取り組みはあり得ないよね、とあります。

まさにその通りで、企画の原点には情熱や思いから発する狙いや企てがあると僕も思います。

それは、趣味のプログラミング(他の趣味にも当てはまりそう)ですら、狙いや企てはある、と。そして、企画には良し悪しがある。

企画の良し悪しはどこに起因する?

利用者便益(ベネフィット)の観点から精査されているかどうか。

と述べられています。

成果と評価は違うとあります。

  • 成果はアウトプットであり、作り手が生み出す
    • 目玉焼きそのものは成果、しかし評価じゃない
  • 評価は受け入れる側が生み出す
    • 成果を受け入れ側がインプットし、そこから生まれる
    • 美味しい、いまいち、など
  • 評価の後は効果
    • 美味しければ嬉しくなったり幸せな気持ちになる
    • いまいちなら残念な気持ちになったり怒ったりする

この違いを認識せず、成果ばかりに目がいってしまい、評価や効果に対する狙いがあやふやなのが、筋の悪い企画であると述べられています。

効果の想定が曖昧

使い勝手のいいアプリを作る、という例で進んでいきます。

  • 使い勝手がいい、と評価するのはユーザーであり作り手ではない
  • ユーザーが評価した結果、どんな効果を狙うのか、が意外と曖昧
    • たぶん、職人文化を色濃くもつ日本人にすごくありがち
    • So What?(で?だからなに?) と問われたら回答できない

あるよなあそういうこと、って思いながら読んでました。

ストーリーを描けることが重要

  • 成果=例えば完成した「在庫管理システム」
  • 利用者が「仕事がしやすくなった!」という評価の先に…
  • 在庫回転率が向上するという効果が生じる
  • というストーリーが描けるものが良い企画

しかし実際にはストーリーに飛躍や断絶があって、実際の要件定義をする段になると、プロジェクトの目的に対して辻褄が合わない、矛盾した要件になってしまうよね、とあります。

重要なのは個別の正確性より全体の一貫性

個々に優れていても、例えば洗濯物を片付けたい、と言われ、いかに高性能かつ最先端の洗濯機を激推ししたとしても、おそらく以下の「洗濯」というプロセスに置いて、洗濯機は一部の、つまり個別の正確性の1つでしかないんじゃないかな、って考えました。

  • 洗濯物が発生するタイミング
    • 毎日、毎時、人、など多数かつランダム
  • 洗うと判断するタイミング
    • 定量に達したら?
    • 旅行などのイベントに関連する?
  • 洗うという行為
    • 洗濯物別に区分け
    • ネットに入れる
    • 順番を考えて実行する
  • 洗濯開始までの導線
    • カゴに放り込まれている
    • 仕分けする
    • ネットで包む
    • 洗濯機に入れる
  • 洗濯機を操作する
    • 洗剤を入れる
    • 柔軟剤をポケットに入れる
    • 水の量を設定する
    • 時間を設定する
    • 乾燥が必要か判断する
    • 乾燥のON・OFF
    • 洗濯実行する
  • 洗濯機が回る
  • 洗濯が完了する
  • 洗濯機から取り出す
  • 干す
    • ハンガーに通す
    • 洗濯バサミで止める
    • 洗濯棒にかける
    • 乾燥にかける
    • 陰干しにする
  • 乾いたか確認する
  • 乾いていれば取り込む
    • 定められた位置へ積む
    • 分ける必要があれば分ける
  • たたむ
    • しまう場所別にたたむ
    • 利用する人別にたたむ
    • 簡単なものからたたむ
  • しまう
    • タンスにしまう
    • クローゼットにしまう
    • 脱衣所にしまう
    • あるべき利用位置に設置する

思うこと

まさにその通りだなってのは前回にも思いを綴った記憶があるんですけど、仕事のための仕事を作る人っているよな、とも思いました。誰かのために、つまり利用者に便益をもたらすことから発進しているはずなのに、そこがすっぽり抜け落ちている。

利用者に便益をもたらす観点から考えずに、 企画を立ち上げなくてはならないからターゲットと市場を考えて、無理に企画を考案する なんてことあるよな、と。

得てしてそういう企画は利益にも結びつかず、便益をもたらす利用者の数も非常に限定的かもしれないなって思いました。

とはいえ、世の中の不便なことをちゃんと調べないと、企画も出てこない、ということも言えると思います。ゆえに、思いつきを徹底的に精査する、矛盾や無理のない内容であるかを突き詰める。

そうして初めて、企画としての実現可能性が見えてくるんだろうなって思いました。

やってみたいこと&やったこと

洗濯する、をちょっと考えても、これだけのプロセスとタスクと手順があり、しかもそれが天気や家族の健康状態、学校、イベント、ともすると気分にも左右され、かつ大物(布団カバーとか)があるか否かで分岐も変化するよなってことに気づきました。

また、今までずっと考えていて行動に移せなかった おとーさんとして1日を回すために経験すべきこと をどう認識するか。それって要件定義と同じ考えで形にできないかな、と考えたりしました。

で、24時から1時間ほど奥さんとずっと話してたんですけど、

  • 「家事育児、俺ができるようになるために経験したい、見える化したい」
  • (この間、色々やりとりあり、奥さんは辛抱強く僕の主張を聞いてくれている。眠いのに)
  • 「で、何がしたいねん。母親の代わりをしたいのか、手伝いたいのかわからないよ」
  • ( ゚д゚)ハッ!
  • (自分に問いかけたのち)「俺にできる1日の回し方を体験したい」
  • 「つまり、おとーさんとして1日を回したいってことね」

ってことを話してました。かなり要約すると。このあいだの1時間、奥さんはすごい忍耐力と言語力で僕に接してくれたと思います。

我が家は僕が外貨を稼ぎ、奥さんが専業主婦です。当然役割も意識も求められるものも違う。

僕は外貨を稼ぐためにタスクを見える化し、確実にこなして成果に繋げ、評価してもらう。その結果として対価をいただく。

では奥さんは?

常に変動する要素に対し、分単位、ときに秒単位で判断し、行動している。毎日命がけで。その行動の元となる情報は天気だったり子供の健康状態だったり控えている学校行事だったりするし、ご飯をどのくらい食べたかで次のやることにも影響してくる。

これらを、家の中にあるあらゆる要素、あらゆる外的要因、あらゆる子供の状態をインプットとして無限の分岐から判断を下して行動する。

僕とは根本的にやってることが違うわけです。

で、その認識が僕にはない。なぜならそこまで全部判断するような一気通貫の1日を回しているわけではなかったから。

でも、知りたい。なぜか。できるようになりたい。なぜか。奥さんが1日家を空けても回るようにしたい。なぜか。奥さんが入院したら回らないから?それだけじゃない。俺にできることを増やしたい。なぜか。奥さんが大変そうな、辛そうなことを減らしたい。減るのか。減らない。それに意味はあるのか。意味はあると思う。経験を積むために協力してもらうことはできないか。それは可能といえば可能。

こんな問答に付き合ってくれて、以下のことがわかりました。

  • 1日のゴールは娘が時間内にご飯を食べ、明日のために19時台に寝ること
  • ゴールに到達するためには、日によって選択肢が全く違うこと
  • 正解は何もないこと
  • おかーさんの代わりは絶対にできないこと
  • 部分的な家事育児の積み重ねの経験は必要なこと
  • 経験から、部分の前後関係に気づけるようになること
  • 連鎖していくことで、イレギュラーな分岐に対応する能力が上がること(おかーさんほどではない)

他にもたくさん話したし、おかーさんの家事育児は本当に命がけで休みがないです。だからこそ話の途中では非常に厳しく固い雰囲気だなって僕が感じる場面もありました。

命がけだから当たり前の話し方をしてるんですよね。それは奥さんにとっては当たり前。

でも僕はその当たり前を本質的に知るまでに至っていなかった。部分的にやっていただけだった。

それがわかり、厳しいわけでも固いわけでもなく、淡々と解説して、先を読み、僕の話を聞いてくれ、それに答えてくれている。

で、上記のことがわかった、というか初めて共有できたんですね。

そして、なんでそんなこと急に、しかもこの寝る直前に言ったのか。という話になって。

そういうことが考えられるくらい、気持ちに余裕ができてきたのかもね

って言ってくれたのがすごく嬉しかったし、今の僕の状態を奥さんも喜んでくれている、ということもわかりました。

実際、11月に入ってから、僕はお腹を壊していません。それまでは何かある都度、ひどい腹痛に10年くらい悩まされてきましたが、それが初めてピタリと止んでいるんですよね。(なので、根っこの体調そのものは非常に良好です)

そんな話から、生活や環境を今よりもっとよくしていくにはどうしようか?来年度からPTA役員だから家にいてほしいスケジュールはなるべく早く共有するからサポートよろしくね。そういえば洗濯機そろそろやばいよね。2月になったら最終決断しようか。そういえば食洗機ってどう?便利は便利、でもたまに傷がつくのよね。えーまじで。ほらほらこんな感じ、だから使うときは洗えてもお気に入りの食器はやめようね。そうだね、大事な子たちだもんね。そういえばソファを占領している大きなぬいぐるみ(すみっこぐらしのプライズ、つまりゲーセンから連れて帰ってきた子たち)のおうちどおしようかしらね?壁に固定する家具を選ばなくてわ!そういえばクリスマスはお揃いのちっこいバッグでいいかな?娘も欲しいって言ってたから共同で使うかな。それはいいわね。

などなど話は飛躍したし、要件定義について学習した結果の何気ない(と思っていた)問いかけが、奥さんに負担を強いてしまったんですけど、少なくとも僕は 家事育児が全然わからない。俺は雰囲気で部分的にやっている 状態から 命がけでやってるし代わりはできないし、おとーさんとして考える ってことはわかったし、それによって 24時間臨戦態勢のおかーさん をたまの1日でも休んでもらえるために僕にできること、をより具体的に考えることができるようになったし、あとは経験を積もう、って意識になれました。

要件定義から大きく話は逸れましたし、家事育児はそもそも要件とかそういう次元ではないので、安易に仕事っぽく話した結果の責任は僕には負えませんが、僕としては言葉にできないくらいの出来事に繋がったりしました。

仕事の思考で家事育児をやろうとすると破綻するのはわかったし、根本的に違うのもいったんはわかったし、最終的に言えることは、僕の奥さんはやっぱり最高だな。

この人に命を預けるって決めてからもう10年以上経っていますが、様々な環境の変化についていけていなかった自分がいたんですよね。それはきっと自分にとっては負い目に近かったんじゃないかなって今では思います。

今年は色々あったし、 おとーさんとしての1日を回したい なんて話、奥さんにとっては「めんどくさい話だな!」って一言で片付けられてしまわれかねない。手伝いって意識じゃ全然ダメだし、やりたいって言った以上はアテにするわけだから、依頼したら100%やれるもんじゃないと依頼できないし、依頼するんじゃなく行動をみて自分で判断できないと本当はだめ、ってこともあります。

けど、僕の気持ちは汲んでくれて、僕自身は前に進むことができた。あとは行動で示すしかないなって気持ちを新たにして、今日は寝ようと思います。

( ˘ω˘ )スヤァ

学習記録:12月17日(月):はじめよう!要件定義 Chapter-05 要件定義、その前に & Chapter-06 [準備編]企画を確認する

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の17日目のエントリーです。

さて、明日は 徹底的に要件定義について学ぶ研修 があり、その教材の元になっているのが、この はじめよう!要件定義 という書籍だったりします。

ですので今日はMySQLはいったんおやすみし、前回の 学習記録:12月10日(月):はじめよう!要件定義 Chapter-03&Chapter-04 に引き続き要件定義の学習を進めます。

Chapter-05 要件定義、その前に

というわけで前章までは

  1. UI
  2. 機能
  3. データ

を決める、という流れで、実際に詳細を見ていきますというところで終わっていました。

UI定義という工程

UIが決まった、というゴールに到達するには、何をどうすればいいかと問われます。

どんな材料を使って成果物(ゴール)に向かうのか。それをまずUI定義から始めるということかな、と理解してます。

要件定義の旅への出発準備

目玉焼きという成果を出すには卵という材料が必要なのと同じく、定義されたUIという成果を出すにも材料が必要と書かれています。

そこで材料を揃えるための要件定義の旅、その出発の準備をするとあり、要件定義そのものではないとあります。

要件定義を行うために必要な材料を揃えるための作業がしばらく続くとあり、僕自身、要件定義を「何を持って完了とするか」を曖昧なまま今まで来てしまったので、非常に楽しみな内容です。

現実のプロジェクトあるある

要件定義と称して、要件定義の準備を行なっているケースが非常に多い、とあります。

よって、これから出てくる準備段階を要件定義としてやってしまっているってことは、要件定義の前工程である準備を先送りした事実の現れである、という理解を僕はしました。

そんなプロジェクトは好ましい状態じゃないよね、ということで次に続きます。

Chapter-06 [準備編]企画を確認する

僕は企画の部署に所属していたことがあるのでなんとなくわかるのですが、要件の前に企画が必ずあるんですよね。なのでそのことかなと思いつつ読み進めてみます。

ゴールを確認する

ソフトウェアを作る、というゴールを確認すると、依頼する・作るからには、思いつきだけじゃない、 企画意図が明確に定義されている はずとあります。

本当にその通りだと思いますし、思いつきだけで作ったプロダクトで成功した物を僕はみたことがありません。

以前、事業コンテストをきっちり行なっていた会社でも、思いつきだけではなく、役員全員に対しプレゼンをし、利益の可能性、市場の状態、人員確保、スケジュール、損益分界点など、裏付けの情報を含めてしっかりやっていました。

また、そうじゃないと予算など確保できないよね、とあり、その通りだよなと思います。

で、ソフトウェアはその企画意図を達成できるものじゃなきゃいけないし、企画意図から外れた要件を定義してしまったら、いくら要件定義通りに作っても納得が得られない、ということになるとあります。

当たり前の話ですけど、新規事業でも、受託の要件定義でも、できてない場面たくさんあるし、たくさん見てきたなー、と思い出します。

企画書を確認できない=ゴールの定義が不明瞭

企画確認は企画書を見るのが手っ取り早い。しかし現実では見ることができない場面がある、とあります。

  • 社内秘なので企画書を外部に見せられない
  • そもそも企画書がない

大雑把にいうとこの2つですとあり、そうだよなって思います。

重要なこと

企画書が見られない場合、それに代わるものが存在しない、つまりゴールが不明瞭である状態が問題であるとあります。

不明瞭とは、これを見ればわかるよ、という状態が得られないこと。それにより

  • 毎回口頭で解説を受けることになる
  • 記録に残らない
  • 時間の経過で簡単に変わってしまうことになる
    • 口頭の内容がブレる
  • 説明が面倒で端折る

ことで、結果として内容理解が関係者でバラバラになる。それぞれの「今回のゴール」がバラバラになり、理解の個人差が混乱を招くとあります。

全くその通りだし、ゴールが曖昧なままなのは、社内の他のチームから見ても不安になりますし、「何を目的にして、何を利益に還元するのだろう」と疑問にも思います。

次は「企画書を作成するためにまとめる項目」が具体的に出てきます。

企画書を作成するためにまとめる項目

バラバラになる問題は、プロジェクトマネジメントの問題で、要件定義の作業の問題ではないとあります。そりゃそうですよね。仕組みや組織の問題ってことでいいのかな。

しかし材料を揃えないといけない。揃わないと不明瞭な要件定義のまま進むことになる。

ゆえに、関係者全員がひと目で「理解を共有できる」企画書を作る必要があり、その最低限の項目が列挙されています。

  1. プロジェクトの名称
  2. なぜこのプロジェクトをやるのか
    • 目的
  3. 何を
    • 目的達成のために作るもの
    • 作るものの説明
    • 作るものを利用する人
    • 利用する人が得られる便益
  4. どのように
    • 作るための体制
    • 期限

この程度で構わない、と文中にありますが、この程度が全然できてないの多いよなーと思ってたら、次の文に このようなサマリーが存在しないプロジェクトが実に多数見受けられます とあってわかりみが深かったです。

なので、企画会議に出た人以外は、企画意図を理解する手がかりがないまま進んでしまう、つまり 企画者が後工程に対して理解不足である とあります。

これ、エンジニアでもマーケターでもディレクターでもなんでも、職種に関係なく、理解不足な場面あるよなって思いましたし、企画意図って上述の項目全部に理由があるはずで、それが 解説できなかったらその時点でその企画はダメなんだろうなあ と思いました。

僕が思うこと

名前って大事だよなって思います。業務システムで曖昧なまま進むのをみたことありますが、呼び方ひとつ取っても曖昧で、なんというか情熱というか、気持ちが入りにくいんじゃないかなって思ったりします。

目的はもっと大事というか、これ企画する一番最初にあるよねって思いますし、 目的ないのはただの趣味 だよなって思ったりもします。

また、目的達成のために作るものは何で、その内容はどんなもので、利用する人、つまりターゲットだと思いますが、そのターゲットはどんな人たちなのか。

で、その ターゲット=利用する人が何を得られるのか。押し付けじゃないよね、使って幸せになれるよね、って話だよな って思いながら読みました。

この辺に 無理なこじつけがある場合は、企画そのものに無理がある んだろうなあ、と思ったりもします。

また、企画といってもゼロスタートのサービスじゃなく、 既存サービスの新機能にも同じことが言える と思います。

以前、人材系のサービスに関わっていたときのプロジェクトマネージャは非常に優秀な人で、 各部署で企画が立ち上がったら片っ端からエンジニアを派遣し、スピーディーに実現可能性について即答できる状況を作った りしてました。

エンジニアだからといって企画、要件定義に関わらなくていいてことは全くなくて、むしろそこから関わることで、熱量や背景、歴史、目的、つまり 企画意図が正しく伝わり、同じ方向のゴールをみてプロジェクトが進められる んだろうなって思います。

読んでみて

ずっと自分の中にあった 思いつきだけじゃない、様々な根拠に裏付けられた企画意図 があるのが大前提だよな、という思いが、ここで見事に解説されていて、頷きしかないなって思いました。

そして、世の中にはそういった、思いつきだけで突っ走っては消えていくサービスが多いよな、ということも思いました。

それが良い悪いはさておき、思いつきだけで走るのは博打以外の何者でもなく、それが道楽ならいいですけど、利益をあげる必要がある場所で開発に関わるなら、 勝算のある博打を打つ 必要があるよなって強く思いました。

また、自分の今までの事業や企画に対する思いが間違えていなかった。それを確認できただけでも、読んでみて得られることは大きかったです。

次以降、読み進めるのが非常に楽しみですし、明日から始まる研修が楽しみで仕方ありません。

というわけで今週は要件定義週間になりそうです!

学習記録:12月16日(日):【MySQL】frmファイル欠損、ibdファイル存在、テーブル構成を推測したい【リストア不能】

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の16日目のエントリーです。

結論から言ってしまうと、テーブル修復でエラーになっていたのは .ibdファイル がそもそもなかったからでした。おそらく、MacBookのストレージ容量不足でファイルを退避させた際に、なぜか .frmファイル を一緒に退避させなかったみたいですね(遠い目)

というわけで、振り返りつつ、どうにか .ibdファイル を直接読むことはできないものか、ともがいてみます。

昨日まで

昨日は MySQL 5.7.19 の環境をマイナーバージョン指定で作ろう、と思い立ったものの、おそらくソースからの make install じゃないと無理では…ということで、もともと使っていた環境に、エラーのあったテーブルをリストアし、mysql_upgradeを再実行すればいいかな、と思っていました。

なんと .frmファイル欠損

が、いざリストアしてみると肝心のテーブルがない…。あ、テーブル個別でバックアップ取っていたな、と思ってzipファイルを開くと…なんと .ibdファイルそのもの

.frmファイル がテーブル構造を表すファイルで、 .ibdファイル が実データを表すファイル。これではテーブル構造がわからぬ…しかも前回、公式にしたがって DROP TABLE しているので構造がわからない。

うーむ困った…。

`.ibdファイルを直接読んで、レコードの状態をみることはできないものか。

と探していると、 innodb_ruby というツールを提供しているかたを発見。

wiki に書かれている通りにインストールし、使ってみました。

innodb_ruby

以下の手順でインストールしました。

インストール

# バックアップを取っていたと思ったらibdファイルだけだったディレクトリで作業
$ pwd
/Users/mamy1326/dev/vagrant/mysql57/var/lib/mysql/mamy1326/innodb_ruby
$ sudo gem install innodb_ruby
Password:
Fetching: bindata-2.4.4.gem (100%)
Successfully installed bindata-2.4.4
Fetching: digest-crc-0.4.1.gem (100%)
Successfully installed digest-crc-0.4.1
Fetching: innodb_ruby-0.9.15.gem (100%)
Successfully installed innodb_ruby-0.9.15
Parsing documentation for bindata-2.4.4
Installing ri documentation for bindata-2.4.4
Parsing documentation for digest-crc-0.4.1
Installing ri documentation for digest-crc-0.4.1
Parsing documentation for innodb_ruby-0.9.15
Installing ri documentation for innodb_ruby-0.9.15
Done installing documentation for bindata, digest-crc, innodb_ruby after 3 seconds
3 gems installed

GitHubから諸々取得

$ git clone https://github.com/jeremycole/innodb_ruby.git
Cloning into 'innodb_ruby'...
remote: Enumerating objects: 2663, done.
remote: Total 2663 (delta 0), reused 0 (delta 0), pack-reused 2663
Receiving objects: 100% (2663/2663), 16.79 MiB | 483.00 KiB/s, done.
Resolving deltas: 100% (1451/1451), done.
naosama-mac:mamy1326 mamy1326$ cd innodb_ruby
naosama-mac:innodb_ruby mamy1326$ innodb_space -s ../
c_log.ibd    innodb_ruby/ 
naosama-mac:innodb_ruby mamy1326$ innodb_space -s ../c_log.ibd system-spaces
name                            pages       indexes     
/Library/Ruby/Gems/2.3.0/gems/innodb_ruby-0.9.15/bin/innodb_space:206:in `block in system_spaces': undefined method `pages' for nil:NilClass (NoMethodError)
    from /Library/Ruby/Gems/2.3.0/gems/innodb_ruby-0.9.15/bin/innodb_space:211:in `system_spaces'
    from /Library/Ruby/Gems/2.3.0/gems/innodb_ruby-0.9.15/bin/innodb_space:1976:in `<top (required)>'
    from /usr/local/bin/innodb_space:22:in `load'
    from /usr/local/bin/innodb_space:22:in `<main>'
$ cd innodb_ruby

innodb_space 実行

藁をもすがる気持ちで実行してみます。 なお、実行コマンドは wiki と、 日々の覚書 mysqlディレクトリーに知らない.ibdファイルがある in MySQL 8.0.0 を参考にさせていただきました。

  • -f オプション
    • 対象はibdata1ではなく、テーブル個別のibd
  • 対象ファイル
    • c_log.ibd ファイルに対し読み込みを実行する
  • page-dump
    • innodb_rubyが理解できるほとんどの構造体の表現を含むページの内容をインテリジェントにダンプします
    • 直訳ですけど、page単位でdumpしてくれる…のか?
    • -p 5 が5ページ分のdumpってことかな?
  • -T オプション
    • 指定されたテーブル名を使用します、とのこと。データベース名とテーブル名を指定します

で、dumpした結果。正直よくわかりませんな(遠い目

$ innodb_space -f ../c_log.ibd -T mamy1326/c_log -p 5 page-dump | less
#<Innodb::Page::Index:0x00007fe6b10dcb78>:

fil header:
{:checksum=>816151722,
 :offset=>5,
 :prev=>nil,
 :next=>nil,
 :lsn=>42947405534,
 :type=>:INDEX,
 :flush_lsn=>0,
 :space_id=>139}

fil trailer:
{:checksum=>816151722, :lsn_low32=>4292699870}

page header:
{:n_dir_slots=>3,
 :heap_top=>274,
 :garbage_offset=>0,
 :garbage_size=>0,
 :last_insert_offset=>265,
 :direction=>:right,
 :n_direction=>4,
 :n_recs=>11,
 :max_trx_id=>0,
 :level=>2,
 :index_id=>106,
 :n_heap=>13,
 :format=>:compact}

fseg header:
{:leaf=>
  <Innodb::Inode space=<Innodb::Space file="../c_log.ibd", page_size=16384, pages=559616>, fseg=6>,
 :internal=>
  <Innodb::Inode space=<Innodb::Space file="../c_log.ibd", page_size=16384, pages=559616>, fseg=5>}

sizes:
  header           120
  trailer            8
  directory          6
  free           16096
  used             288
  record           154
  per record     14.00

page directory:
[99, 181, 112]

system records:
{:offset=>99,
 :header=>
  {:next=>125,
   :type=>:infimum,
   :heap_number=>0,
   :n_owned=>1,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>125,
 :data=>"infimum\x00",
 :length=>8}
{:offset=>112,
 :header=>
  {:next=>112,
   :type=>:supremum,
   :heap_number=>1,
   :n_owned=>8,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>112,
 :data=>"supremum",
 :length=>8}

garbage records:

records:
{:format=>:compact,
 :offset=>125,
 :header=>
  {:next=>139,
   :type=>:node_pointer,
   :heap_number=>2,
   :n_owned=>0,
   :min_rec=>true,
   :deleted=>false,
   :length=>5},
 :next=>139}

{:format=>:compact,
 :offset=>139,
 :header=>
  {:next=>167,
   :type=>:node_pointer,
   :heap_number=>3,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>167}

{:format=>:compact,
 :offset=>167,
 :header=>
  {:next=>181,
   :type=>:node_pointer,
   :heap_number=>5,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>181}

{:format=>:compact,
 :offset=>181,
 :header=>
  {:next=>195,
   :type=>:node_pointer,
   :heap_number=>6,
   :n_owned=>4,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>195}

{:format=>:compact,
 :offset=>195,
 :header=>
  {:next=>209,
   :type=>:node_pointer,
   :heap_number=>7,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>209}

{:format=>:compact,
 :offset=>209,
 :header=>
  {:next=>223,
   :type=>:node_pointer,
   :heap_number=>8,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>223}

{:format=>:compact,
 :offset=>223,
 :header=>
  {:next=>237,
   :type=>:node_pointer,
   :heap_number=>9,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>237}

{:format=>:compact,
 :offset=>237,
 :header=>
  {:next=>251,
   :type=>:node_pointer,
   :heap_number=>10,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>251}

{:format=>:compact,
 :offset=>251,
 :header=>
  {:next=>265,
   :type=>:node_pointer,
   :heap_number=>11,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>265}

{:format=>:compact,
 :offset=>265,
 :header=>
  {:next=>153,
   :type=>:node_pointer,
   :heap_number=>12,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>153}

{:format=>:compact,
 :offset=>153,
 :header=>
  {:next=>112,
   :type=>:node_pointer,
   :heap_number=>4,
   :n_owned=>0,
   :min_rec=>false,
   :deleted=>false,
   :length=>5},
 :next=>112}

次の課題

まずもってテーブル構造を、レコードから推測できないことにはリストアは不可能そうです。

しかしせっかく .ibdファイル を読み込むところまできたので、できる限りのことをしてみようと思います。

これで自由にレコード読めるようになったら面白いじゃん?

せっかく便利なツールを作ってくれている人がいるし、個人的に中身を直接読んでみたいと常々思っていたので、趣味的に願ったり叶ったり。

アップグレードはしばし棚上げして、 .ibdファイル の読み方を学習してみたいと思います。

もしこのエントリを読んでくださっているかたがいらしたならば、おそらく面白くないんじゃないかなあ…と思いながらも、自分自身はやっていて楽しいのでこうしてエントリにしているわけでございます。

1歩ずつ地道に進めてみて、結果までたどり着き、誰かしらの有益な情報になればいいなあ、と思いながら、やれるところまでやってみようと思います。

で、もしダメなら諦めて、くだんのテーブルは捨ててアップグレードを進めたいと思います。

アップグレード時にテーブル修復が失敗していたのも、そもそも .ibdファイル がなかったからで、ないならないで、消して進めれば本題はクリアできると思いますので。

というわけで、明日も引き続き .ibdファイル と戯れたいと思います!

学習記録:12月15日(土):【MySQL8.0アップグレード】5.7.19→5.7.24→8.0.11 アップグレード手順【5.7.24 アップグレード後編-MySQL 5.7.19の環境を新規に作ってみる】

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の15日目のエントリーです。

昨日の振り返り

昨日のエントリーを出して間も無く、yoku0825さんに考察していただき、ログ( /var/log/mysqld.log )を遡った結果、エラーを起こしていたテーブルの .ibd ファイルが最初からなかったことが判明したので、ちょっと遠回りだけど以下の方法で再実行してみようと思います。

今後の方針

  • 5.7.19 の環境を作る
  • リストアする
  • .ibdファイルが揃っていることを確認する
    • 今回エラーのあったテーブルにSELECTできることも確認する
  • 5.7.24 へアップグレードする
  • 結果を確認する

んで、無事に 5.7.24 になったら、8.0.11 へさらにアップグレードしてみる計画です。

MySQL 5.7.19の環境を構築する

手順は以下を想定しています。

  • vagrantでubuntu18.04の環境を作る
  • MySQL 5.7.19 をインストール
  • データをリストア
  • MySQL 5.7.24 にアップグレード
  • エラーが出ないことを確認

昨年7月に vagrant で作った際は、リストアするデータが大きすぎて vagrant のデフォルト容量である 4GB をオーバーし、リストアできませんでした。

そこで、容量を 40GB まで増やして boxを作成し、リストアしました。が、昨年やったときはすごくめんどくさかった(イメージファイルを変換したり色々やった。正直覚えておりません…)です。しかし今はデフォルトで100Gあるのでそのままやります。

新しい vagrant box を作成

まっさらから作っていきます。Dockerでやればいいと思うかたもいらっしゃるとは思うんですが、今回の主題は

MySQL 5.7.19 から 5.7.24 にアップグレード したのち MySQL8.0.11(またはもっと最新)にアップグレード

することです。Dockerでそれをやる方法を少なくとも僕は知らないし、手軽に色々いじり倒したいので、vagrantを採用しています。

ここら辺はちゃんと理解してないんですが、主題とは逸れるので掘り下げずに進めます。

バックアップ

忘れずに以下をバックアップします。

  • Vagrantfile
  • my.cnf
  • データベース

box作成

ubuntuで作っていきます。

$ cd ~/dev/vagrant
$ mkdir mysql8.0
$ cd mysql8.0
$ vagrant init bento/ubuntu-18.04
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.
$ vim Vagrantfile
$ cat Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-18.04"

  # ポートとIPアドレスの設定
  config.vm.network "forwarded_port", guest: 80, host: 8080
  config.vm.network "private_network", ip: "192.168.33.10"

  # マウントするディレクトリ
  config.vm.synced_folder "./vagrant", "/vagrant"

  # メモリ容量(6G)
  config.vm.provider "virtualbox" do |vb|
    vb.memory = "6144"
  end
end
# マウントするディレクトリ忘れずに
$ mkdir vagrant

vagrant up でbox作成

ポートとか他のboxとぶつかってないか確認忘れずに。ぶつかると以下のエラー出ます。

ポートフォワードのエラー

ついうっかり他からコピーしてきた時のあるあるですかね。俺たちは雰囲気でvagrantを利用している。

==> default: Setting the name of the VM: mysql80_default_1544881511628_84089
Vagrant cannot forward the specified ports on this VM, since they
would collide with some other application that is already listening
on these ports. The forwarded port to 8080 is already in use
on the host machine.

To fix this, modify your current project's Vagrantfile to use another
port. Example, where '1234' would be replaced by a unique host port:

  config.vm.network :forwarded_port, guest: 80, host: 1234

Sometimes, Vagrant will attempt to auto-correct this for you. In this
case, Vagrant was unable to. This is usually because the guest machine
is in a state which doesn't allow modifying port forwarding. You could
try 'vagrant reload' (equivalent of running a halt followed by an up)
so vagrant can attempt to auto-correct this upon booting. Be warned
that any unsaved work might be lost.

vagrant up でbox作成

$ vagrant up
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'bento/ubuntu-18.04' could not be found. Attempting to find and install...
(中略)
==> default: Mounting shared folders...
    default: /vagrant => /Users/mamy1326/dev/vagrant/mysql8.0/vagrant
$ vagrant status
Current machine states:

default                   running (virtualbox)

The VM is running. To stop this VM, you can run `vagrant halt` to
shut it down forcefully, or you can run `vagrant suspend` to simply
suspend the virtual machine. In either case, to restart it again,
simply run `vagrant up`.

vagrant box の中で諸々更新

ubuntu18.04.1 のboxができました。次は諸々更新。

$ vagrant ssh
Welcome to Ubuntu 18.04.1 LTS (GNU/Linux 4.15.0-29-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sat Dec 15 13:56:05 UTC 2018

  System load:  0.0               Processes:           90
  Usage of /:   2.8% of 61.80GB   Users logged in:     0
  Memory usage: 3%                IP address for eth0: 10.0.2.15
  Swap usage:   0%                IP address for eth1: 192.168.33.10


0 packages can be updated.
0 updates are security updates.


$ sudo apt update -y
(中略)
$ sudo apt upgrade -y
(中略)
# 途中GUIが立ち上がり、キーボードについて聞かれるので適切に選択する、主に言語配列かな
(中略)
update-initramfs: Generating /boot/initrd.img-4.15.0-29-generic
vagrant@vagrant:~$ 

aptitude インストール

ubuntuのパッケージ管理ソフトウェアのaptitudeをインストールしておく。どうやら16.04あたりからデフォルトでインストールされないようになってるらしい。

$ sudo apt-get install aptitude
Reading package lists... Done
(中略)
update-alternatives: using /usr/bin/aptitude-curses to provide /usr/bin/aptitude (aptitude) in auto mode
Processing triggers for libc-bin (2.27-3ubuntu1) ...
vagrant@vagrant:~$ 

MySQL 5.7.24 のインストール

マイナーバージョンの指定の仕方がわからず。5.7系で最新の 5.7.24 をインストール。make installしかないのかな?

$ mysql --version
-bash: mysql: command not found

# パッケージを確認する
$ sudo aptitude search ^mysql-.*5.7
p   mysql-client-5.7                                                               - MySQL database client binaries                                                          
p   mysql-client-5.7:i386                                                          - MySQL database client binaries                                                          
p   mysql-client-core-5.7                                                          - MySQL database core client binaries                                                     
p   mysql-client-core-5.7:i386                                                     - MySQL database core client binaries                                                     
p   mysql-server-5.7                                                               - MySQL database server binaries and system database setup                                
p   mysql-server-5.7:i386                                                          - MySQL database server binaries and system database setup                                
p   mysql-server-core-5.7                                                          - MySQL database server binaries                                                          
p   mysql-server-core-5.7:i386                                                     - MySQL database server binaries                                                          
p   mysql-source-5.7                                                               - MySQL source                                                                            
p   mysql-source-5.7:i386                                                          - MySQL source                                                                            
p   mysql-testsuite-5.7                                                            - MySQL 5.7 testsuite                                                                     
p   mysql-testsuite-5.7:i386                                                       - MySQL 5.7 testsuite

# 必要な物を選んでインストール
$ sudo aptitude install -y mysql-client-5.7 mysql-client-core-5.7 mysql-server-5.7 mysql-server-core-5.7
The following NEW packages will be installed:
(中略)
Processing triggers for ureadahead (0.100.0-20) ...
                                         
vagrant@vagrant:~$ mysql --version
mysql  Ver 14.14 Distrib 5.7.24, for Linux (x86_64) using  EditLine wrapper

マイナーバージョンアップの再現どうするよ

現時点でマイナーバージョン指定のインストール方法がわからず(あんまり指定してインストールすることないもんなあ)

マイナーバージョンアップを実体験して成功させる、のも主目的の1つなので、昨日使っていた環境に対し、以下を実行してみます。

  • データベース削除
  • リストア
  • mysql_upgrade 実行

前の環境に戻って作業

先ほどバックアップはとりましたので、順番に実行していきます。

vagrant に対する操作

一応快適に操作したいので、今作った環境は止めて、前の環境で作業します。

$ vagrant halt
(中略)
$ cd ~/dev/vagrant/mysql57
$ vagrant up
$ vagrant ssh

データベース削除&作成

リストアの前に削除&空のデータベース作成します。

$ mysql -uroot -p mysql
Enter password: 
mysql> drop database mamy1326;
Query OK, 40 rows affected (0.58 sec)
mysql> create database mamy1326;
Query OK, 1 row affected (0.00 sec)

リストア

$ mysql -uroot -p mamy1326 < mamy1326.sql
Enter password: 

時間切れ

ここで今日の学習時間オーバー。15GBくらいのリストアなので、そこそこ時間かかります。

一応問題なくテーブルはリストアされていて、くだんの大きなテーブルも順調にサイズが増えているので、待てばリストアされるでしょう。

ここはリストアを待って、引き続き日が変わってから(になると思う)作業を続けたいと思います。

感想

久しぶりにリストアとかやるんですけど、去年散々やったのでこの辺は覚えてるもんですね。

あと、今までマイナーバージョンまで指定(というか意識)してインストールしたことなかったので、やり方がわかりませんでした。

マイナーバージョンが最新のならサクッとインストールできるんですけどね(当たり前)

ここら辺、検証したいわー環境個別に作りたいわー、っていうニーズは確実にあるはずなので、方法を探したいところです。

幸い前の環境は削除してないので、そちらで作業を続け、バージョンアップについての知見を得られ、アウトプットできたら、素直にMySQL 8.0の環境を普通に作って今後の検証作業とスライド作成など進めようかな、って思っています。

ただ、yoku0825さんにいただいたご恩を返すには、バージョンアップを成功させることが必要。これだけはしっかりやっておきたいと思います。

よっし引き続きやるぞー。

学習記録:12月14日(金):【MySQL8.0アップグレード】5.7.19->5.7.24->8.0.11 アップグレード手順【5.7.24 アップグレード後編-エラーを調べる】

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の14日目のエントリーです。

結論からいうと、どうも サイズの大きなibdファイルが失われている ようです。結果、データファイルが読み取れず、アップグレードのチェック時にエラーとなっていた模様。

今回はそのエラーを /var/log/mysqld.log から、なるべく1つずつ調べていきます。

昨日の振り返り

趣味なので振り返りからやっていきます。

/var/lib/mysql/mysql_upgrade_info ファイルが書き込めなかった

昨日はパーミッションの関係で、以下のファイルが書き込めなかったのを解決し、そのさきへ…というところで止まっていました。

Could not create the upgrade info file '/var/lib/mysql/mysql_upgrade_info' in the MySQL Servers datadir, errno: 13

12月15日(土) 1:36 追記

最強の外部API神であるお豆腐さん が今日と昨日のエントリーをみてくれて、なんとまとめてくださった…あなたが神か(神です)。本当にありがとうございます!

とある豆腐のエラー考察(未完)

で、 /var/lib/mysql/mysql_upgrade_info ファイルが書き込めなかった の正解を引用します。

さすがやで…根拠をインクルードファイルで提示までしてくださる…。というわけで、 mysql_upgrade 実行ユーザーが vagrantユーザーで、/var/lib/mysql への書き込み権限を持たなかったため でした。(あってるかな?)

これもっかい最初からやり直そうかなって思ってます。

/var/lib/mysql/ パーミッション変更し、mysql_upgradeを再実行

で、パーミッションを変更してmysql_upgradeを実行し、ファイルが出力されるところまで実行しました。

$ sudo chmod 775 /var/lib/mysql
$ ls -lda /var/lib/mysql
drwxrwxr-x 7 mysql mysql 4096 1213 22:53 /var/lib/mysql
$ mysql_upgrade -uroot -p
Enter password: 
(中略)
Upgrade process completed successfully.
Checking if update is needed.

バージョン情報のテキストファイルだった

で、結果としては 5.7.24 のアップグレードのバージョンが出力されるファイルだったようです。

$ cat /var/lib/mysql/mysql_upgrade_info
5.7.24

それから

とはいえ本題のエラーはまだこれからでした。

Tablespace is missing for table

で、続いて昨日は以下のエラーが出て、特定のテーブルが5.7.19から5.7.24へのアップグレードされる段階でエラーを出力していました。ちなみにMySQL自体はアップグレードができています。

$ mysql_upgrade -uroot -p
(中略)
mamy1326.c_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/c_log.
Error    : Tablespace is missing for table `mamy1326`.`c_log`.
error    : Corrupt

そこで今日は、このエラーの原因を究明、解決し、テーブルを復旧するところまで進めたいです。

試しにSELECT

MySQLの起動はできたので試しにSELECTしてみましたが、同じエラーになります。 というわけでこれは趣味。丁寧に調べていきます。

mysql> select * from c_log limit 1;
ERROR 1812 (HY000): Tablespace is missing for table `mamy1326`.`c_log`.

/var/log/mysqld.log エラーログを見る

困った時のエラーメッセージ。素直にエラーログをみます。

該当部分を抜き出し

今回のテーブルの部分のみ抜き出してみました。 色々出てますので、1つずつ調べてみます。趣味ですので。

2018-12-14T13:21:28.160781Z 2 [ERROR] InnoDB: Failed to find tablespace for table `mamy1326`.`c_log` in the cache. Attempting to load the tablespace with space id 139
2018-12-14T13:21:28.160847Z 2 [ERROR] InnoDB: Operating system error number 2 in a file operation.
2018-12-14T13:21:28.160856Z 2 [ERROR] InnoDB: The error means the system cannot find the path specified.
2018-12-14T13:21:28.160862Z 2 [ERROR] InnoDB: Cannot open datafile for read-only: './mamy1326/c_log.ibd' OS error: 71
2018-12-14T13:21:28.160868Z 2 [ERROR] InnoDB: Operating system error number 2 in a file operation.
2018-12-14T13:21:28.160873Z 2 [ERROR] InnoDB: The error means the system cannot find the path specified.
2018-12-14T13:21:28.160880Z 2 [ERROR] InnoDB: Could not find a valid tablespace file for `mamy1326/c_log`. Please refer to http://dev.mysql.com/doc/refman/5.7/en/innodb-troubleshooting-datadict.html for how to resolve the issue.
2018-12-14T13:21:28.160996Z 2 [Warning] InnoDB: Cannot calculate statistics for table `mamy1326`.`c_log` because the .ibd file is missing. Please refer to http://dev.mysql.com/doc/refman/5.7/en/innodb-troubleshooting.html for how to resolve the issue.

Failed to find tablespace for table mamy1326.c_log in the cache. Attempting to load the tablespace with space id 139

直訳すると以下になります。

キャッシュ内のテーブル mamy1326.c_logのテーブルスペースを見つけることができませんでした。スペースID 139で表スペースをロードしようとしています

何のことやらさっぱりです。 グーグル先生に頼っても「クラッシュしたんで復旧してや」って記事ばかり。俺はこのエラーメッセージの意味と原因を知りたいんじゃああああああ!

と思うけど他にもエラーメッセージがたくさん出てるので次々調べてみます。

Operating system error number 2 in a file operation.

これも直訳します。

ファイル操作中のオペレーティング・システム・エラー番号2。

ざっくりとしかわからへんがな(´・_・`)

次調べましょう。

The error means the system cannot find the path specified.

これも直(ry

エラーは、システムが指定されたパスを見つけることができないことを意味します。

お、これは前のエラーと繋がってることを意味しそうです。何かパス間違ってるんかな。ibdファイルかな。うーん。

しかし my.cnf へのパスの情報はこれくらいしか設定してない。

[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

とりあえず次のエラーを調べます。

Cannot open datafile for read-only: './mamy1326/c_log.ibd' OS error: 71

おおおおっ。これは…ッ!ibdファイルが開けない…ッ!パーミッションか…ッ!

突然のJOJO風、失礼しました。取り乱しました。

やっとエラーメッセージみただけでわかりそうな感じになってきたので、ファイル確認します。

$ sudo ls -la /var/lib/mysql/mamy1326/c_log.ibd
ls: /var/lib/mysql/mamy1326/c_log.ibd にアクセスできません: そのようなファイルやディレクトリはありません

テーブルの実体ファイルがないじゃない。ちなみに定義ファイルはありました。

Could not find a valid tablespace file for mamy1326/c_log. Please refer to http://dev.mysql.com/doc/refman/5.7/en/innodb-troubleshooting-datadict.html for how to resolve the issue.

結局のところ、最後のエラーメッセージで、公式をみてトラブルシューティングをするべし、とありがたくも指摘までいただけているのでみていくことにします。

というかこれ、アップグレードしようとして、ファイルでかすぎて変換できなかったんじゃないかな、と今の時点で予測を立ててます。

公式を参照する

公式の 14.19.3 InnoDB データディクショナリの操作のトラブルシューティング を参照します。(内容は全て翻訳機にかけた内容です)

序文

テーブル定義に関する情報は、.frm ファイルと InnoDB データディクショナリの両方に格納されます。.frm ファイルをあちこちに移動したり、データディクショナリの操作の最中にサーバーがクラッシュしたりすると、これらの情報のソースに整合性がなくなることがあります。

frmファイルにテーブル定義、ibdファイルがデータディクショナリ、という認識です。これがクラッシュしたら整合性取れなくなるよと最初に言われています。

データファイルを開くことができません。

公式の前半部分に、今回のエラーメッセージズバリの内容が書かれていました。innodb_file_per_tableパラメータはデフォルトでONになっているので、表領域は共有ではなく個別領域です。その表領域である .ibd ファイルが欠落しているよと言われます。

innodb_file_per_table (デフォルト)有効にした場合、次のメッセージが起動時に表示されることがあります ファイルあたりのテーブル 表領域のファイル(.ibdファイル)が欠落しています。

はい、その通りでした。サンプルのエラーメッセージも今回のものとほぼ同じです。

[ERROR] InnoDB: Operating system error number 2 in a file operation.
[ERROR] InnoDB: The error means the system cannot find the path specified.
[ERROR] InnoDB: Cannot open datafile for read-only: './test/t1.ibd' OS error: 71
[Warning] InnoDB: Ignoring tablespace `test/t1` because it could not be opened.

で、解決方法としては、DROP TABLEしなさいと書かれています。

これらのメッセージに対処するには、DROP TABLEステートメントを発行して欠落しているテーブルに関するデータをデータ辞書から削除します。

DROP TABLEしてみる

.ibd ファイルが欠落した理由は今のところわかっていませんが、幸いこのテーブルはバックアップがあります。そこで素直に公式に従い、DROP TABLEしてみます。

ちなみにログインした結果から、 Server version: 5.7.24-log MySQL Community Server (GPL) が確認できます。

$ mysql -uroot -p mamy1326
Enter password: 
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.24-log MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

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

mysql> DROP TABLE c_log;
Query OK, 0 rows affected (0.02 sec)

無事DROP TABLEすることができました。

テーブル定義も消えてますね。

$ sudo ls -la /var/lib/mamy1326/c_log.*
ls: /var/lib/mamy1326/c_log.* にアクセスできません: そのようなファイルやディレクトリはありません

次の課題

DROP TABLEしたものの、こいつを復元するのはどうやって?がまだわかっていません。

バックアップからリストアするんだろうな。ということでリストアするとは思います。

今回はここまで

さて、いったん学習時間としては時間切れ。引き続き明日調べていきます。

それにしても5.7.19で構築した環境にテスト的に大きめなデータを入れた状態で、マイナーバージョンアップしただけでもこういうことが起きる。いい経験をしているなーと思います。

現段階での予測では、以下の状態だったのかもしれないなあと思ったりしてます。

  • vagrantは40GB
  • テーブルスペースの総容量は20GBちょっと
  • ストレージの空き容量は4GB程度
  • なんらかの理由でデータファイルを消した
    • 手で消してた?
  • またはmysql_upgrade実行時に復元できなかった
    • 容量不足?

12月15日(土) 2:10 追記

/var/log/mysqld.log を遡ってみると前から起きているエラーがあった。

  • そもそもibdファイルを作成できていなかった?
    • 過去のmysqld.logを遡ったら Cannot open datafile for read-only: './mamy1326/c_log.ibd' OS error: 71
    • そもそも去年の7月に環境作ったとき、ディスクフルで書き込めてなかった
      • 2017-07-07T22:01:02.800765Z 3 [ERROR] /usr/sbin/mysqld: The table 'c_log' is full
    • よってvagrantのbox容量を40Gに拡張したのち、リストアしていた
      • しかしそれでもibdファイルは正しく作られていなかったか、どこかで消えていた
      • 2017-09-21の時点でも同じように、ibdファイル読めないよ(ないよ)と言われていた
      • 2017-09-21T13:53:39.939744Z 0 [ERROR] InnoDB: Cannot open datafile for read-only: './mamy1326/c_log.ibd' OS error: 71
  • ログには別のエラーも出ていたが、MySQLの起動には成功していた
    • 例えばこれ
    • 2018-04-29T13:07:39.271664Z 3 [ERROR] InnoDB: Failed to find tablespace for tablemamy1326.c_login the cache. Attempting to load the tablespace with space id 139
    • このエラー出てる場合、MySQLの起動に失敗する事例しか検索できなかったけど、なんでだろう…
  • ibdファイルがないため、統計情報の計算ができません、と言われているので、この時点で欠落していた
    • 2018-04-29T13:08:18.405618Z 3 [Warning] InnoDB: Cannot calculate statistics for tablemamy1326.c_logbecause the .ibd file is missing. Please refer to http://dev.m ysql.com/doc/refman/5.7/en/innodb-troubleshooting.html for how to resolve the issue.

つまり、 アップグレード実行時に .ibdファイルが欠落していた ことがわかりました。

今後の進め方

幸いバックアップデータがあるので、リストアしてもう一度実行してみようと思います。

今度は、全てのibdファイルが揃っていることを確認した上で。

で、ストレージ容量不足なのか、データが悪いのか、順番に切り分けしてみようと思います。

いやーでも手動でibdファイルは消さないと思うんだよなあ…。

12月15日(土) 2:15 追記

最初から .ibd ファイルがなかったことが判明したので、ちょっと遠回りだけど以下の方法で再実行してみようと思います。

  • 5.7.19 の環境を作る
  • リストアする
  • .ibdファイルが揃っていることを確認する
    • 今回エラーのあったテーブルにSELECTできることも確認する
  • 5.7.24 へアップグレードする
  • 結果を確認する

んで、無事に 5.7.24 になったら、8.0.11 へさらにアップグレードしてみる計画です。

さて、明日もやるぞ!

学習記録:12月13日(木):【MySQL8.0アップグレード】5.7.19->5.7.24->8.0.11 アップグレード手順【5.7.24 アップグレード前編】

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の13日目のエントリーです。

MySQL8.0.11を触る必要が出てきて、じゃあ手元の環境をアップグレードしよう!と思い立ったけど一発でうまくいかなかったので、未来の自分のためにエントリーに残します。

現状

$ cat /etc/redhat-release
CentOS Linux release 7.3.1611 (Core) 
$ mysql --version
mysql  Ver 14.14 Distrib 5.7.19, for Linux (x86_64) using  EditLine wrapper

5.7.19->5.7.24

というわけでまず、5.7の最新版にアップグレードしましょう。

MySQL停止

忘れずに。

$ sudo systemctl stop mysqld.service

依存性のアップグレード

yumってなんぞ?って話はさらっと触れるんですが、一言ていうと パッケージの統合管理システム です。

パッケージの!統合管理システム!

なるほどよくわからん。

というわけでWikipedia先生から引用

パッケージ管理システム

パッケージ管理システムとは、OSというひとつの環境で、各種のソフトウェアの導入と削除、そしてソフトウェア同士やライブラリとの依存関係を管理するシステムである。

つまるところ、

  • OSや各種ソフトウェアが 必要としているライブラリを管理 していて
  • 不必要なライブラリをインストールしない ことで
  • ちゃんと動くようにパッケージを管理している

ってことかな(自信なし)

yumとは

そんなわけで…

依存関係(これからインストールしようとしている、MySQLコミュニティ版がどういうライブラリで構成されているのか)の情報を最新にするために、以下のコマンドを実行します。

$ sudo yum upgrade mysql-community-server
読み込んだプラグイン:fastestmirror
base                                                                                                                                                  | 3.6 kB  00:00:00     
epel/x86_64/metalink                                                                                                                                  | 4.9 kB  00:00:00     
epel                                                                                                                                                  | 3.2 kB  00:00:00     
extras                                                                                                                                                | 3.4 kB  00:00:00     
mysql-connectors-community                                                                                                                            | 2.5 kB  00:00:00     
mysql-tools-community                                                                                                                                 | 2.5 kB  00:00:00     
mysql57-community                                                                                                                                     | 2.5 kB  00:00:00     
remi-safe                                                                                                                                             | 3.0 kB  00:00:00     
updates                                                                                                                                               | 3.4 kB  00:00:00     
(1/5): epel/x86_64/updateinfo                                                                                                                         | 940 kB  00:00:00     
(2/5): extras/7/x86_64/primary_db                                                                                                                     | 156 kB  00:00:00     
(3/5): epel/x86_64/primary                                                                                                                            | 3.6 MB  00:00:00     
(4/5): updates/7/x86_64/primary_db                                                                                                                    | 1.3 MB  00:00:00     
(5/5): remi-safe/primary_db                                                                                                                           | 1.4 MB  00:00:04     
Loading mirror speeds from cached hostfile
 * base: ftp.tsukuba.wide.ad.jp
 * epel: ftp.yz.yamagata-u.ac.jp
 * extras: ftp.tsukuba.wide.ad.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp.tsukuba.wide.ad.jp
epel                                                                                                                                                             12748/12748
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ mysql-community-server.x86_64 0:5.7.19-1.el7 を 更新
---> パッケージ mysql-community-server.x86_64 0:5.7.24-1.el7 を アップデート
--> 依存性の処理をしています: mysql-community-common(x86-64) = 5.7.24-1.el7 のパッケージ: mysql-community-server-5.7.24-1.el7.x86_64
--> トランザクションの確認を実行しています。
---> パッケージ mysql-community-common.x86_64 0:5.7.19-1.el7 を 更新
---> パッケージ mysql-community-common.x86_64 0:5.7.24-1.el7 を アップデート
--> 依存性解決を終了しました。

依存性を解決しました

=============================================================================================================================================================================
 Package                                          アーキテクチャー                 バージョン                              リポジトリー                                 容量
=============================================================================================================================================================================
更新します:
 mysql-community-server                           x86_64                           5.7.24-1.el7                            mysql57-community                           165 M
依存性関連での更新をします:
 mysql-community-common                           x86_64                           5.7.24-1.el7                            mysql57-community                           274 k

トランザクションの要約
=============================================================================================================================================================================
更新  1 パッケージ (+1 個の依存関係のパッケージ)

総ダウンロード容量: 165 M
Is this ok [y/d/N]: y
Downloading packages:
Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
(1/2): mysql-community-common-5.7.24-1.el7.x86_64.rpm                                                                                                 | 274 kB  00:00:00     
(2/2): mysql-community-server-5.7.24-1.el7.x86_64.rpm                                                                                                 | 165 MB  00:00:07     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
合計                                                                                                                                          23 MB/s | 165 MB  00:00:07     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  更新します              : mysql-community-common-5.7.24-1.el7.x86_64                                                                                                   1/4 
  更新します              : mysql-community-server-5.7.24-1.el7.x86_64                                                                                                   2/4 
  整理中                  : mysql-community-server-5.7.19-1.el7.x86_64                                                                                                   3/4 
  整理中                  : mysql-community-common-5.7.19-1.el7.x86_64                                                                                                   4/4 
  検証中                  : mysql-community-server-5.7.24-1.el7.x86_64                                                                                                   1/4 
  検証中                  : mysql-community-common-5.7.24-1.el7.x86_64                                                                                                   2/4 
  検証中                  : mysql-community-common-5.7.19-1.el7.x86_64                                                                                                   3/4 
  検証中                  : mysql-community-server-5.7.19-1.el7.x86_64                                                                                                   4/4 

更新:
  mysql-community-server.x86_64 0:5.7.24-1.el7                                                                                                                               

依存性を更新しました:
  mysql-community-common.x86_64 0:5.7.24-1.el7                                                                                                                               

完了しました!

無事に5.7.19 -> 5.7.24 へ、依存関係が更新されたようです。

MySQL 5.7.24 へのアップグレードを実行

以下のコマンドで、アップグレードを実行します。

何をしているかというと、MySQLの全データベースの全テーブルをチェックし、必要であれば修復し、アップグレードしています。

mysql_upgradeとは

まず公式を読んでみます。英語だけど頑張って翻訳機にかけて読むのだ。

公式から引用すると、以下の記述があります。

現在とこれからの非互換性を調べる

mysql_upgradeは、現在のバージョンのMySQL Serverとの非互換性について、すべてのデータベース内のすべてのテーブルを調べます

  • 現在のバージョン、つまり5.7.19と
  • これからインストールしようとする5.7.24で
  • 非互換性について
  • 全てのデータベース内の全てのテーブルを調べる

とあります。 つまり、廃止になった機能や型などがないかを全部チェックしてくれると解釈しました。

ここでもしエラーになれば、それを解決しないとバージョンアップできない、もしくは修復などできなかったテーブルは手動であとで修復してね、と僕は理解しました。

ちなみに MySQL 5.7.24 のリリースノート をみてみると、色々興味深い内容が書いてありますが、主題と大きくそれるので割愛します。

1つだけ言えるのは、アップグレードする際は、リリースノート読んで、影響範囲を事前に調べようね、ってことかなって思います。

システムテーブルもアップグレード

mysql_upgradeは、追加された可能性のある新しい特権や機能を利用できるように、システムテーブルもアップグレードします

思えばslow_logも今ではテーブルで管理できるようになっているわけで、そのほかにもたくさんの管理がシステムテーブルで管理できるようになっていると思います。

そういった、バージョンアップに伴いシステムテーブルで管理できるようになった内容なども、最新に対応するためにアップグレードしてくれるわけだな、と解釈しました。

互換性のないテーブルをチェック・修復

mysql_upgradeがテーブルに互換性がない可能性があると判断した 場合は、テーブルチェックを実行し、問題が見つかった場合はテーブル修復を試みます。表を修復できない場合は、2.11.3項「表または索引の再構築または修復」を参照して、手動表修復方法を確認してください

もし互換性がないテーブル(なくなった機能や非推奨のものなどが含まれる場合かな)がある場合は、

  • テーブルチェックを実行
  • 問題があればテーブル修復を試みる
  • 修復できない場合は手動修復

とあります。 実際にやってみたら、でかいテーブル(数百万〜1000万レコードのテーブルでした)でエラーが出ていたので、何かしら問題があったことがわかります。(アップグレード自体は完了しましたが、修復はされなかったようです)

これはまた mysql_upgrade を実行した後の問題解決として書いていきますので、まずは進めます。

アップグレードの都度、mysql_upgradeが必要

MySQLをアップグレード するたびにmysql_upgradeを実行する必要があります

公式に書いてある通りですね。マイナーバージョンアップであっても毎回 mysql_upgrade を使いましょうということでしょうか。

mysql_upgrade コマンド実行

というわけで、アップグレードするためにコマンドを実行し、各種チェックや必要であれば修復を行なっていきます。

わかる範囲でコメントを入れてますので、実際は # で始まるコメントはありません。

$ mysql_upgrade -uroot -p
Enter password: 
Checking if update is needed.
Checking server version.
Running queries to upgrade MySQL server.

# システムデータベースのチェックを実施
Checking system database.
mysql.columns_priv                                 OK
mysql.db                                           OK
mysql.engine_cost                                  OK
mysql.event                                        OK
mysql.func                                         OK
mysql.general_log                                  OK
mysql.gtid_executed                                OK
mysql.help_category                                OK
mysql.help_keyword                                 OK
mysql.help_relation                                OK
mysql.help_topic                                   OK
mysql.innodb_index_stats                           OK
mysql.innodb_table_stats                           OK
mysql.ndb_binlog_index                             OK
mysql.plugin                                       OK
mysql.proc                                         OK
mysql.procs_priv                                   OK
mysql.proxies_priv                                 OK
mysql.server_cost                                  OK
mysql.servers                                      OK
mysql.slave_master_info                            OK
mysql.slave_relay_log_info                         OK
mysql.slave_worker_info                            OK
mysql.slow_log                                     OK
mysql.tables_priv                                  OK
mysql.time_zone                                    OK
mysql.time_zone_leap_second                        OK
mysql.time_zone_name                               OK
mysql.time_zone_transition                         OK
mysql.time_zone_transition_type                    OK
mysql.user                                         OK

# システムスキーマはアップグレードに成功しました
The sys schema is already up to date (version 1.5.1).

# 次に自分のデータベースをチェック
Checking databases.
mamy1326.ag_media                                  OK
mamy1326.bukken_bad                                OK

# おっ、エラーが出ている。これはテスト用に突っ込んだ1000万レコードのテーブル
mamy1326.c_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/c_log.
Error    : Tablespace is missing for table `mamy1326`.`c_log`.
error    : Corrupt

mamy1326.debug_log                                 OK
mamy1326.dr_log                                    OK
mamy1326.error_log                                 OK
mamy1326.hoge1                                     OK
mamy1326.item_details                              OK
mamy1326.items                                     OK
mamy1326.m_ad                                      OK
mamy1326.m_ad_base                                 OK
mamy1326.m_ad_group                                OK
mamy1326.m_agent                                   OK
mamy1326.m_client                                  OK
mamy1326.m_company                                 OK
mamy1326.m_contact                                 OK
mamy1326.m_log                                     OK
mamy1326.m_maintenance                             OK
mamy1326.m_media                                   OK
mamy1326.m_prefecture                              OK
mamy1326.m_status                                  OK
mamy1326.migrations                                OK
mamy1326.ng_log                                    OK
mamy1326.no_inc                                    OK
mamy1326.paths                                     OK
mamy1326.re_log                                    OK
mamy1326.report                                    OK
mamy1326.report_tmp                                OK
mamy1326.t_access_ip                               OK
mamy1326.t_ad_base_change                          OK
mamy1326.t_ad_change                               OK
mamy1326.t_alert                                   OK
mamy1326.t_carrier_ip                              OK
mamy1326.t_error_log                               OK
mamy1326.t_login                                   OK
mamy1326.t_mail_spool                              OK
mamy1326.t_resign                                  OK
mamy1326.t_retry                                   OK
mamy1326.t_session                                 OK

# おっ、ここもエラーが出ている。これもテスト用に突っ込んだ数百万レコードのテーブル
mamy1326.t_session_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/t_session_log.
Error    : Tablespace is missing for table `mamy1326`.`t_session_log`.
error    : Corrupt

mamy1326.time_stamp                                OK
mamy1326.time_stamp2                               OK
mamy_test.addr_test                                OK
mamy_test.bukken_bad                               OK
mamy_test.bukken_good                              OK
mamy_test.deleted_sites                            OK
mamy_test.m_user                                   OK
mamy_test.products                                 OK
mamy_test.products_status                          OK
mamy_test.tags                                     OK
mamy_test.timestamp_test                           OK
mamy_test.unique_test                              OK
mamy_test.wishlist                                 OK
sys.sys_config                                     OK

# 修復した結果を表示してくれる
Repairing tables
mamy1326.c_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/c_log.
Error    : Tablespace is missing for table `mamy1326`.`c_log`.
error    : Corrupt
mamy1326.t_session_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/t_session_log.
Error    : Tablespace is missing for table `mamy1326`.`t_session_log`.
error    : Corrupt

# 基本的にはこのメッセージが出ていれば、アップグレードは成功
Upgrade process completed successfully.

# おや、何かファイルに書き込めないと言われている
Could not create the upgrade info file '/var/lib/mysql/mysql_upgrade_info' in the MySQL Servers datadir, errno: 13

まずは簡単そうなエラーから解決していきます。

Could not create the upgrade info file '/var/lib/mysql/mysql_upgrade_info' in the MySQL Servers datadir, errno: 13

/var/lib/mysql/mysql_upgrade_info ファイルが書き込めないと言われているようです。

ここは素直に /var/lib/mysql/パーミッションを変更して再実行してみます。

$ sudo chmod 775 /var/lib/mysql
$ ls -lda /var/lib/mysql
drwxrwxr-x 7 mysql mysql 4096 1213 22:53 /var/lib/mysql

再実行

Checking if update is needed. と表示され、無事エラーが出なくなっていることが確認できます。

$ mysql_upgrade -uroot -p
Enter password: 
(中略)
Upgrade process completed successfully.
Checking if update is needed.

テーブル修復を試みた原因

先ほどのmysql_upgradeで、テーブルに問題があり、修復を試みた結果、どうやら失敗していたテーブルが2つありました。エラーはどちらも同じ内容でした。

mamy1326.c_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/c_log.
Error    : Tablespace is missing for table `mamy1326`.`c_log`.
error    : Corrupt

試しにSELECTしてみます。

mysql> select * from c_log limit 1;
ERROR 1812 (HY000): Tablespace is missing for table `mamy1326`.`c_log`.

mysql_upgradeで出ていたエラーと同じ内容が表示され、SELECTできないことがわかります。

ここの調査などについては、明日に持ち越したいと思います。

やってみて

マイナーバージョンアップ、実は初めてやりました。

やってみて、これ結構ハマるんだな、古いテーブルだったりすると結構クリティカルだな、と思うと共に、

  • 正確な実行方法
  • リリースノートの内容からの影響範囲調査

をちゃんとやっとかないといきなり躓くよね、と強く思いました。

正直マイナーバージョンアップは軽い気持ちで実行して、サクッと8.0まであげちゃうぞ!とか思ってたんですが、色々と腰を据える必要がありそうです。

しかし、こういうのを1つずつ調べて進めていく、わからなかった原因やエラーがわかっていく。

ほんと楽しいですね。

色々調べているうちに、時間を忘れて2時間ほど経過していて、アドベントカレンダーに間に合わないところでしたw

というわけでこれも継続していくぞ!

学習記録:12月12日(水):【輪読会まとめ 後編パート2・完結編】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン)

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の12日目のエントリーです。

そして昨日のエントリー 学習記録:12月11日(火):【輪読会まとめ 後編パート2】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン) の続きで、データベースの章の読了完結編です。

昨日はリポジトリパターンの実装で、

  • データ周りの要求変更に負けない設計パターンを取り入れる
  • ビジネスロジックからデータ操作を別レイヤへ

という内容を読み進め、まずは サービスクラスとデータベースアクセスが密結合 な実装をするところまでで終わっていました。

よって今日は リファクタリングで他のデータストアへの変更、モック差し替え のところまで進め、本章を読了したいと思います。

4. リファクタリング

ビジネスロジックを受け持つFavoriteServiceクラスを改めて確認してみるところから。

  • 「いいね」のデータ登録部分は EloquentであるFavoriteクラスに依存
    • つまりMySQLに接続できるEloquentが前提になっている

ビジネスロジックから特定のデータベース操作を取り除く

以下の順序で本質部分以外を取り除き、抽象化を進めていきます。

  1. Repositoryを抽象化するインターフェースを作成
  2. データベース操作を担当するRepositoryクラス作成
  3. Serviceクラスはインターフェースを参照
  4. インターフェースと具象クラスを紐付ける

「いいね」データのON・OFFが可能である、というのがビジネスロジック(Serviceクラス)の本質だったので、「ON・OFF切り替え操作を持つ、いいねデータのリポジトリ」にデータベースの処理を抽象化します。

で、データベース処理の抽象化を表現したインターフェースとして、次のコードを作成します。

リスト5.5.2.13:リポジトリインターフェース

というわけでインターフェースのコードです。このインターフェースの役割を表すということにもなるので、具体的な内容は書かず、メソッド定義のみになります。

具体的な内容は、それこそ具象クラスである、この後の具象クラスに実装します。

インターフェースのファイル名は app/DataProvider/FavoriteProviderInterface.php です。

<?php
declare(strict_types=1);

namespace App\DataProvider;

interface FavoriteRepositoryInterface
{
    public function switch(int $bookId, int $userId, string $createdAt): int;
}

余談

余談ですが、インターフェースはちょうど先日のぺちオブでも出てきて、実際のコードや具体例と共に解説を受けてきました。

そのあとでこの部分を実装してみると、わかりみが深いです。いずれ自分の言葉でSOLID原則をきっちり説明する試みを実行したいと思っています。

インプットはいただいたので、あとはアウトプットだ。

リスト5.5.2.14:リポジトリインターフェースを実装した具象クラス

  • app/DataProvider/FavoriteRepository.php を作成
  • コンストラクタインジェクション(依存性の注入)でデータベースアクセスを行うFavoriteクラスを注入
    • 注入自体はサービスクラスで実施
  • これでデータ操作の実処理はリポジトリクラスに移動
<?php
declare(strict_types=1);

namespace App\DataProvider;

use \App\DataProvider\Eloquent\Favorite;

class FavoriteRepository implements FavoriteRepositoryInterface
{
    private $favorite;

    public function __construct(Favorite $favorite)
    {
        $this->favorite = $favorite;
    }

    public function switch(int $bookId, int $userId, string $createdAt) : int
    {
        return \DB::transaction(
            // いいねがなければ作り、あれば削除、結果を0/1で返す
            function () use ($bookId ,$userId, $createdAt) {
                $count = $this->favorite->where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->count();
                if ($count == 0) {
                    $this->favorite->create([
                        'book_id' => $bookId,
                        'user_id' => $userId,
                        'created_at' => $createdAt
                    ]);
                    return 1;
                }
                $this->favorite->where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->delete();
                return 0;
            }
        );
    }
}

リスト5.5.2.15:サービスクラスのリファクタリング

インターフェースと、その中身を表す具象クラスを作成しました。

次は実際のサービスクラス(Controller/Actionから利用されるビジネスロジック)をリファクタリングします。

前回までは use App\DataProvider\Eloquent\Favorite; でEloquentを直接利用してました。これを抽象クラスであるインターフェースを用い、コンストラクタインジェクションで注入する形式に置き換えていきます。

僕自身の理解のため、既存のコードをコメントアウトし、なぜ変更したかを自分なりの言葉で書いて進めます。

<?php
declare(strict_types=1);

namespace App\Services;

// Eloquentの直接利用をやめ、インターフェースクラスを利用
//use App\DataProvider\Eloquent\Favorite;
use App\DataProvider\FavoriteRepositoryInterface;

class FavoriteService
{
    /**
     * @var FavoriteRepositoryInterface コンストラクタインジェクションで注入されるインターフェースを受け取る変数を用意
     */
    private $favorite;

    /**
     * Create a new service instance.(クラスのインスタンスが作成される際に、インターフェースの注入を受け取り保存するコンストラクタ)
     *
     * @return void
     */
    public function __construct(FavoriteRepositoryInterface $favorite)
    {
        $this->favorite = $favorite;
    }

    /**
     * Switch to Favorite Status
     *
     * @param  int $bookId
     * @param  int $userId
     * @param  string $createdAt
     *
     * @return int  turn on:1 / turn off: 0
     */
    public function switchFavorite(int $bookId, int $userId, string $createdAt): int
    {
        return $this->favorite->switch($bookId, $userId, $createdAt);
        // ここを丸ごとRepositoryクラスへ外出し、インターフェースを通して抽象化
        // サービスはインターフェースだけ考えればいい
        /*
        return \DB::transaction(
            function () use ($bookId, $userId, $createdAt) {
                $count = Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->count();
                if ($count == 0) {
                    Favorite::create([
                        'book_id' => $bookId,
                        'user_id' => $userId,
                        'created_at' => $createdAt
                    ]);
                    return 1;
                }
                Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->delete();
                return 0;
            }
        );
        */
    }
}

解説

これで、同じインターフェースを持つクラスであれば、なんでも動作する状態になりました。

何が嬉しいかというと、例えばテストで データベースにアクセスしない、モッククラスを作った として簡単に差し替えが可能です。

また、他のデータストアを使うことになっても、サービスクラスには関係なく、具象クラスを作って差し替えるだけで良い、という状態にもなりました。

コントローラもこのサービスクラスを使うので、データストアが変わっても影響を受けない状態となります。

リスト5.5.2.16:インターフェースと具象クラスのバインド

現段階では、インターフェースも具象クラス(Repository)も作っただけで、関連づいていません。

そこで、関連づけ(バインディング)を実行します。これでControllerからRepositoryまで一通り繋がり、動作することになります。

なお、書籍には 新たにサービスプロバイダを作成しても構いません とありますが、いったんは書籍の例で動作させてみます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // インターフェースクラスとリポジトリ(具象)クラスを関連づけ
        $this->app->bind(
            \App\DataProvider\FavoriteRepositoryInterface::class,
            \App\DataProvider\FavoriteRepository::class
        );
    }
}

curlコマンドで実行確認

実行してみると、実際に動きますね(当たり前)

写経してるのでtypoしたりreturn忘れて返り値の型が違うよと怒られたりしましたが、スルッと動くと気持ちいいですね!

$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=1&user_id=2' --write-out '%{http_code}\n'
200

というわけでDBを確認してみます。

# 動かす前、レコードなし
mysql> select * from favorites;
Empty set (0.00 sec)

# 動作後、いいねレコード作成
mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       1 |       2 | 2018-12-12 14:28:09 | 2018-12-12 14:28:09 |
+---------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

# 再実行、いいね取り消しのためレコード削除
mysql> select * from favorites;
Empty set (0.00 sec)

別のbook_idで複数レコード作るのを確認

book_idが違うリクエストでAPIを実行します。

$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=2&user_id=2' --write-out '%{http_code}\n'
200
$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=3&user_id=2' --write-out '%{http_code}\n'
200

2レコード作られていることがわかります。

mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       2 |       2 | 2018-12-12 14:29:20 | 2018-12-12 14:29:20 |
|       3 |       2 | 2018-12-12 14:29:36 | 2018-12-12 14:29:36 |
+---------+---------+---------------------+---------------------+
2 rows in set (0.01 sec)

章の終わり

いいねの操作などは、Queueを使用して非同期でもいいでしょうとあります。別の方法も採用できる実装例で進んできたことをここで知ることになるとは、うまい構成だなあとか思ったりしてました。

また、前述しましたが、データストア先を変更する場合は、同じインターフェース(FavoriteRepositoryInterface)を持ったデータ操作クラス(具象クラス)を作成して、バインドしなおせば、ビジネスロジック(サービス)を変更せずとも差し替えが可能になります。

リポジトリパターンは、今まで登場してきた各クラスを疎結合にできる反面、当然ながらクラス数が増えるため、デモや期間限定の機能では不要かもしれません。

しかしシステムの要件や規模の拡張が見込まれるサービスでは、検討に値するデザインパターンです、という言葉で章が締められています。

やってみて

で、ここからは意訳で僕の言葉ですが、

「今まで登場してきた各クラスを疎結合にできる反面、当然ながらクラス数が増えるため、デモや期間限定の機能では不要かもしれません」

という話は、システム全体からみたら逆にレアケースだと思います。要件が追加されないサービスなんか見たこともないし、拡張がされなかったシステムも見たことがないです。

リポジトリパターンが全てを解決するわけじゃ決してないと思いますが、漫然と今のままの実装を続けるのではなく、サービスの特徴を鑑みて適切なデザインパターンを検討し採用する ってことだよな、と思いながら読みました。

また、実際に僕が実装してみた印象でも、どんどん責務が分割されていき、綺麗に繋がっていくのを体感できました。 責務が分割され単一になるということは、より1つのことに集中してコードが書けるということだよな、と実感しました。

クラス数が増える=繁雑になる、では決してない と思います。 むしろ 適切に責務が分割され、1つずつ確実に実装を進められ、悩むことが少なくなる ことを実感しました。

さいごに

オブジェクト指向での実装を進めると、1つのクラスの責務、1つのメソッドの責務は単一責任の原則(だったかな)に則るのが普通だとも思います。

実際に業務で書いているコードに関しても、どんどん分割し、責務を分け、メソッド名を付ける際に2つ以上の責務を持たないか考え、実装を進めたりしています。

非常にスッキリしますし、互いのメソッドやクラスが疎結合になっていくので、のちのメンテも楽になるなあ、って話を上司としたこともあります。

データベースの章を通読し、コードを全部実行し、自分なりの言葉でアウトプットしてみたわけですが、ぺちオブでの勉強会のインプットも相まって、とてもすんなり内容が入ってきました。

今後もデザインパターンを意識しつつ、納期、リソース、要件、などなど、様々な内容から、最適な技術を採用していこうと思わせていただきました。

この章だけでも購入するに十分な理由になると思います。

Laravelやってみたいな、オブジェクト指向で実装するってなんだっけ?って思ってる人がもしいらっしゃいましたら、いろんな意味で読んでみることをオススメします。

別に回し者でもないですが、時間をかけ、自分の中の情報を関連づけて進めていくことで、割とバラバラだった知識が繋がりましたし、自分の中に浸透していなかったインプットが着実に刻まれていく感覚を味わうことができました。

かいつまんで読み進めているので全部読み切るのにまだ結構かかりそうですが、少しずつ進めて、確実にモノにしていこうと思います。

はー楽しかった!!!!