Articles tagged with: プログラミング

アクアビット航海記-私の技術とのかかわり方


「アクアビット航海記」では、個人事業主から法人を設立するまでの歩みを振り返っています。
その中では、代表である私がどうやって経営や技術についての知識を身につけてきたかについても語っています。

経営や技術。それらを私は全て独学で身につけました。自己流なので、今までに数えきれないほどの失敗と紆余曲折と挫折を経験して来ました。だからこそ、すべてが血肉となって自分に刻まれています。得難い財産です。

本稿では、その中で学んだ技術の学び方を語りたいと思います。
私自身が試行錯誤の中で培ってきたノウハウなので、これを読んでくだった方の参考になれば幸いです。

ただし先に断っておきますと、私の技術力などそれほど大したものではありません。しょせんは独学ですし。
今までに参画してきた常駐現場では多くの凄腕技術者を見てきました。私が最近棲息しているkintone界隈でも私より技術力の優れた人は無数にいます。
そのため、技術力だけで考えれば、私など手本にする価値はありません。

私が皆さんにお伝えできるのは、最小限の努力で必要な技術を身に付ける嗅覚です。それは備えてきたと思います。本稿ではそれを参考にしてもらえればと思います。

モチベーション

ずばりいうと、私の技術へのモチベーションは、面倒くさがりから来ています。さらに飽きっぽさと。

例えば仕事で何か面倒な作業が必要になったとします。
そう、Excelのブックからブックへの転記のような。

これ、一回や二回ならまだいいのです。でもそれが十回繰り返されてくると、とたんに繰り返しに飽きてしまうのです。そして作業が面倒に思えてしまうのです。これは毎日、同じ場所に通勤する営みについても同じ。

そうなると、この面倒くさい作業をやめるためにどうすればよいか、私の脳内がざわめきだすのです。

多分、新たな仕組みやアルゴリズムを考える労力の方が、繰り返す作業よりも大変なのでしょう。でもそんなことは関係がありません。それ以上同じ作業をしたくない。その思いの方が圧倒的に強いため、私を衝き動かします。アルゴリズムや仕組みを考えることは、繰り返しの作業とは無縁です。飽きないし面倒くささも感じません。本連載第二十五回で書いたように集計作業が面倒でExcelのマクロを作ったのはまさにこの実例です。

選ぶ

今までのキャリアで、私はさまざまな技術や言語に触れてきました。この言語や技術の選び方は、案外大切ではないかと思います。

私はどちらかというと新しいもの好きです。ところが、私のキャリアを振り返ってみると、言語や技術の選択に当たってそこまで冒険をしていません。

例えば、PCはWindowsとMs-Officeを主に使ってきました。サーバーを自分で構築する際も、ファイルサーバーはSamba、LAMP(Linux+Apache+MySQL+PHP)でウェブ環境を構築してきました。CMSはWordPressを主に扱いました。クラウドにしても、βテスターとして関わり始めたころのkintoneは無名でしたが、運営元のサイボウズ社はそのころからすでにグループウエアの雄として業界に地位を確立していました。今やkintoneはわが国でも著名なPaaSに成長しています。

今までに私が携わった技術や言語の中で衰退してしまったものを挙げてみます。ファイルサーバーのSambaやその際にMacをつないだAppleTalk。サービス連携の言語はJSONではなくXMLを学びました。常駐先で触る必要があったLotus NotesやLotus Scriptは衰退の最たるものです。あとはLinuxでサーバーを構築した際、採用したDistributionのRedHat LinuxやMiracle Linuxも今はあまり聞きません。

若い頃に得たVisual Basicの知識やLampの知識が今も生かせることは、私のキャリアにとってとても幸運だったと思います。衰退した言語や技術の習得に使った時間が無駄にならずに済んだので。このことは私のキャリアを考える上でとても重要だと思います。

その際、私がどういう基準で言語や技術を選んだのかは、あまり覚えていません。ただ、その当時からシェアが高いものを選んだように思います。また、安価な環境で使える言語であることも重要でした。例えばスクリプト言語はphpであれば安価なレンタルサーバーでも使えましたが、pythonやgoはサーバーにインストールする必要があったため、学びの対象から外しました。

シェアが高いということは、サポートサイトも多いということ。サポートサイトを必死に読み込めば、たいていのヒントはおのずから公開されていることに気づきます。おそらく私はそれらを踏まえながら、自分の学ぶべき言語を選んでいったように思います。

この時に単に新しいからといって新奇な言語や技術にあまり手を出さなかったことが、私のキャリアをあまり回り道に進ませずに済んだと思います。

調べる

自分が知りたいこと、実装したいことをどう調べると効率的か。

これはとても重要なところです。私が自分でプログラムに関心を持ち始めたのは1999年。まだインターネットが世間に広く使われ始めたばかりのころです。今のように少し検索するだけで技術資料が閲覧できる時代ではありません。つまり、書籍が頼りでした。

書籍は、その分野の全てを語ろうとします。まず、総論から始まり、その後で個別の説明を展開していきます。私はそうした総論の類を読みません。まっすぐ自分が求める機能を探します。書籍の場合は目次や索引が付されていますので、そこから探すと目指す機能を学べます。

その機能の説明を読むと、自分の知らない事が次々に出てきます。メソッドや関数の記述。名前空間や言語体系。細かい文法など。それらを総当たりで調べていきます。その際も、名前空間についての総論は読み飛ばします。直接、該当する名前空間の書き方を探します。そうやって個別の自分の知りたいことだけを拾いながら、その積み重ねで全体を把握していく。それが私のやり方です。
ちなみに私は読書が大好きです。が、本を読む際は全く逆のアプローチをとります。途中の部分を読むなどもってのほか。必ず最初から最後まで通して読みます。ところが不思議なことに技術書を読む際だけはそのやり方だとうまく覚えられないのです。

私が技術の世界に触れ始めたころと違い、今はネット上から情報を得ることができます。ですが、その情報には書籍のような目次・索引がありません。つまり検索エンジンを使うしかないのです。この検索の際にキーワードを入力しますが、そのキーワードにもコツがあります。

技術の言語は国際的に英語が使われています。そのため、日本語だけで検索しても求める検索結果にヒットしないことがほとんどです。まず具体的な文言を英語も含めて検索します。また、エラーメッセージにあたった際はそのメッセージを検索文言に含めます。すると、求める結果が得られると思います。その際、英文が出てきたら大意ぐらいはつかめるぐらいの英文読解力があると楽です。その上でGoogle 翻訳やDeepLのような翻訳サイトを使って日本語で意味をつかみます。

なお、当たり前ですが得た結果をきちんと読解する力は必要です。私は文系学部で学んだ技術者ですが、読解力が私のキャリアを助けてくれたと確信しています。パッと読んで分かったつもりになってしまうと、結局遠回りになります。じっくりと文章を読むように心がけましょう。

実装する

この後の連載で、私がどのように実装の経験を積んでいったかは書いていく予定です。独立するまでにはかなりの回り道と試行錯誤と無数の失敗を繰り返しました。

日中は現場に常駐していた私が、個人の業務で無理せずに実装するにはどうすればよいか。全ては五里霧中の中でした。少しずつ実績を積み上げられ、しかも安価な投資額で実装環境が整えられるような案件を痛い失敗の中で少しずつこなしていったのが私のキャリアです。kintoneに出会うまでは。

私のように個人事業主から法人を設立するまでの歩みは、自分でいうのもなんですが、相当難しいと思います。

私のようにホームページの制作から始め、まずHTMLやCSS、JavaScriptを操るスキルを身に付け、そこからサーバーの選定や調達に進み、さらにphpなどの言語がデフォルトであるWordPressのようなCMSに手を染めていくと、キャリアとしてよいのではないかという気がします。この路線は、今のところまだ衰退の兆しがそれほどなさそうですし。

その際も、自分でサーバーを立ち上げ、LAMPをインストールし、AWSやGCP、Azureといったより高度な環境を選ぶより、まずは小規模な環境から始められる規模の案件をこなすとよいでしょう。要するに安価なレンタルサーバーでも十分要件が満たせるようなものです。

ブレイクスルー

とはいえ、ロジックの構築や予期せぬバグの出現など、実装にあたっては問題が生じます。

それをどのように克服していくかは切実な問題です。それで挫折し、折れた心を抱えながら情報処理業界からも去っていく人もいるでしょう。

そもそも、どれだけ本やウェブサイトを読んでも概念がちっともつかめない場合、どうすればよいのでしょう。正直、私にも概念がつかめずに苦戦したことが何度もありました。本連載第三十二回で書いた、行列のExcelからAccessの三次元を理解したのはまさにその一つ。

そこでも書きましたが、当時チームの部下だった年下のOさんに教えを請いました。そこで教えてもらったことで私は一つ目のブレークスルーを果たしました。この時、妙なプライドや自負があって独学にこだわっていたら、今の私はなかったと思います。

私はキャリアのほとんどを独学で積み上げてきたことに誇りも自負も持っています。ですが、今でもまだまだ分からないことが無数にあります。今の私がそうした事態にぶつかった時、二回り以上も年が離れた部下に教えを請い、頭を下げられると確信できます。しょせんは私のキャリアなど独学であり、正当に大学で情報科学を学んだ方には絶対に勝てないことが分かっていますので。

ブレークスルーを果たすには、自分の中で突き詰めて考えることは必要です。でも、概念を理解するためのちょっとした気づきを自分の中だけで得るのは難しいでしょう。その時、相手が誰であろうとヒントを与えてくれる方には頭を下げ、謙虚でいられるかどうか。それが出来る技術者こそが、年配になっても現役でやれる人だと思います。

加齢による好奇心の枯渇

かつてはプログラマー35才限界説、というものがまことしやかに言われていました。35才を超えるとプログラマーとしては使い物にならない、というやつです。

この説はある部分ではあたっています。ただし、それはアルゴリズムの構築が35才を迎えた途端にできなくなる、という意味ではありません。当たっているのは年齢による体力の問題です。それはどうしようもありません。徹夜でコーディングする作業は40歳を過ぎると難しくなるのではないでしょうか。

むしろ、ロジックの組み立てをきちんと自分の頭で考えた経験を35歳までに積んでいることのほうが大切かと。そうした経験があれば、60歳の半ばであっても第一線で問題なくやれると思います。身近にその生きた例を知っています。私自身、50歳の声が聞こえ始めていますが、まだやれると思っています。

また、今の言語はフレームワークなども充実しています。また、基本的なアルゴリズムについてはライブラリが豊富に用意されています。そのため、それを呼び出すだけでよいのです。加えてkintoneのようなPaaSを使えばデータベースの構築や通知・権限設定も手間をかけずに実装できます。

そうした意味ではプログラマー35才限界説とは、かつて情報処理業界の言語や環境が発展途上だったころの名残だと思っています。ちなみに私は文系学部の出身なので、文系プログラマー限界説にも反対の立場です。女性エンジニアの方も優秀な方が多いので、男性だけが優位というのも間違っています。

ただし、それ以外に限界説が当てはまる人はいます。それは体力の問題ではなく、心の柔軟さの問題です。肉体とともに心は徐々に柔軟さを失っていきます。実年齢が30歳であっても、自分が持っている技術や環境から学ぼうとしないと、35才よりも前に限界を迎えます。上に書いたように、自分より詳しい若手に頭を下げられるかも限界の年齢を決めるでしょうね。

例えば新卒で情報処理業界に入り、会社が用意してくれた既存の業界や言語や環境の中で安定した仕事をこなしていたとします。その状態に甘んじて新たな言語や環境を学ぼうとしなかったとすれば、老いはより早くあなたをむしばむはずです。そして気が付いたときには技術者としての活躍の場がない、という悲劇に遭遇します。

私自身、今からDeep LearningやMachine Learning、ブロックチェーンや3Dプリンターを学ぶには億劫な思いを感じます。概念は大体理解しているつもりですが、それを新たな実装として試してみようとする気概が出てきません。私にも間違いなく老いは忍び寄っています。

それを防ぐには好奇心を持ち続けるしかないと思います。これは私の価値観ですが、仕事だけが毎日ではないと思います。さまざまなプライベートの趣味や出会いや楽しみを持ち、仕事以外に多様な刺激を受けるような環境に身を置く。それが40代50代になって少しずつ効いてきて、あなたの身を助けてくれるはずです。

まとめ

私なりに技術との関り方をまとめてみました。もちろんこれは私の例にすぎません。人によってそれぞれのやり方があるはず。ここに書いた内容を基に、皆さんがそれぞれの立場で取り入れられる点があれば、取り入れていただければと思います。


不思議な数列フィボナッチの秘密


かつての私は、数列が苦手だった。
中学生の頃は、一年生の頃こそ数学のテストは全て百点を取っていたが、二次関数が登場する二年生以降は惨敗の連続だった。百点満点中、10点台や20点台を連発していたことを覚えている。

こうした数式を覚えて、いったい世の中の何の役に立つのか。当時の私にはそれが全くわからなかった。当時の教師もそうした疑問には全く応えてくれなかった。もっとも、こちらからそのような問いを発することもなかったのだが。
そんな気持ちのまま、二次関数や座標や配列、そして数式を一方的に教えられても全く興味が持てずにいた。それが当時の私だった。

ところがここ二十年、私はシステムの開発者として生計を立てている。
かつての私が全く人生に役に立たない、と切り捨てていた数学を普段から業務で用いている。
もし昔の私に会えるのなら、勉強を怠っていた自分に数学が役に立つことを教えたいくらいだ。実例を交えながら。

例えばExcelのVBAを使い、セルの値を得たい場合を考えてみる。
D列からデータが始まるセル範囲で、求めたいデータが三列おきに現れるとする。それをどうやってマクロで取り込むか。
そうした際に数列の考え方を使っているのだ。
具体的にはこのようにマクロを組む。
・繰り返しで一定の条件に達するまで処理を行う。
・繰り返しの処理の中で変数の値に1を加える。
・上記の変数に3をかける。
・その掛けた結果に4を加える。
すると、繰り返し処理の中で得られる変数の値は、一つ目は4、次は7、以降は10、13、16、19と続く。これらの列番号をExcelの列番号、つまりアルファベットに当てはめるとD、G、J、M、P、Sとなる。

そのように、一定の規則でつながるセルの位置や値を見極め、それを処理として実装する処理は事務作業で頻繁に発生する。そうした作業はExcelのデータを参照し、編集を行う際に必須だからだ。日常的に必要になってくるといえよう。Excelだけでなく、他のプログラムでもプログラム時に数列の考え方は必須だ。
私は当初、業務に必要になっていたからそうしたプログラムを覚えていた。そしてプログラムを日常の業務で当たり前に使うようになってきたある日、その営みこそ、私がかつて苦手としていた数列や配列そのものということに気づいた。

そもそも私はなぜ数学が苦手になってしまったのだろう。よくわからない。子供の頃は、数字の不思議な性質に興味を持っていた時期もあったはずなのに。
例えば切符に書かれた数字。最近では電車に乗る際に切符を買う機会がなくなってしまったが、かつては切符を買うとその横に四桁の数字が印字されていた。その四桁の数字のそれぞれを、四則演算を使って10にするという遊びは欠かさず行っていた。
その遊び、実は数論の初歩として扱われるそうだ。

そうした数字の不思議な性質は、子供心に不思議に思っていた。
今も、ネットで〇〇数を検索してみると、不思議な数の事例が書かれた記事は無数に出てくる。〇〇には例えば完全、素、三角、四角、友誼、示性、黄金などが当てはまる。

本書は、不思議な数の中でもフィボナッチ数に焦点を当てている。フィボナッチ数とは、並んだ数値の二つの和を次の項目の値としたものだ。0,1,1,2,3,5,8,13,21,34,55,89,144のように。数字が徐々に大きくなっていくのが分かる。しかもそれぞれの数字には一見すると規則性が感じられない。

ところがこの数字の並びを図形にしてみるとさまざまな興味深い動きを描く。例えばカタツムリの殻。螺旋が中から外に広がるにつれ徐々に大きくなる。この大きくなる倍率はすべてフィボナッチ数列に従っている。またあらゆる植物の葉のつき方を真上から見ると、それぞれが日の当たるように絶妙に配置されている。これらも全てフィボナッチ数列の並びに近い。
ほかにもひまわりの種やDNAの螺旋構造など、自然界でフィボナッチ数列が現れる事例は多いという。自然は効率的な生態系を作るにあたってフィボナッチ数を利用しているのだ。

最近では昆虫や植物の機能や動きを研究する動きがある。バイオミメティクスと呼ぶこれらの研究から、科学上の発見や、新たな素材の開発や商品の機能が生まれることがあるという。
フィボナッチ数列を学ぶことで、私たちの非効率的な営みが、より効率的に変わるかもしれないのだ。

本書にはフィボナッチ数にまつわる不思議な性質や計算結果が豊富に紹介される。
正直に言うと、それらの計算結果を実生活や科学の何かに役立てられる自信は私にはない。だが、そうした法則を見つけてきたことで、人類はこれまで科学を発展させてきた。

おそらくフィボナッチ数に隠された可能性はまだあるのだろう。フィボナッチ数は、未来の人類が技術的なブレークスルーを達成する際に貢献してくれると期待している。
例えば、私が思いついたのは暗号だ。暗号化にフィボナッチ数を役立てられるように思える。
暗号の一般的な原理は、ある数とある数を掛けて出来た数値からは何と何を掛けた結果なのかが推測しにくい性質に基づいている。例えば桁数が約300桁ある二つの素数のそれぞれを掛けた数があったとしても、それが掛けられた二つの素数を見つけ出すのに、最新鋭のスパコンでも何億年もかかり、量子コンピューターでも非現実的な時間を要するという。

この暗号を作る際、フィボナッチ数列を使って作るのはどうだろう。フィボナッチ数列も数が膨大だが、それらは全て数式で表せる。
これを何かの解読キーとして用い、何番目のキーを使わなければ復号できないとすれば、暗号として利用できるように思う。もちろん、私が思いつくような事など、数学者がとっくの昔に思いついているだろうけど。
だが、暗号以外にも情報処理の分野でフィボナッチ数が活用できるかもしれないと考えてみた。
私のような素人でも暗号への使い道がすぐに思いつけたほどだから、優秀な数学者が集まればフィボナッチ数列のより有用な使い道を考えてくれるはずだ。

かつての私のような数学が苦手な人にこそ、本書は読んでもらいたい。

‘2020/07/12-2020/07/18


七日間ブックカバーチャレンジ-かんたんプログラミング Excel VBA 基礎編


【7日間ブックカバーチャレンジ】

Day5 「かんたんプログラミング Excel VBA 基礎編」

Day5として取り上げるのはこちらの本です。

私は、もう20年ほど技術者としてお仕事をしています。
開発センターでシステムの開発やテストに従事していた時期も長いです。銀行の本店でも数年間、常駐していました。
ここ数年は、5年近くkintoneのエバンジェリストとして任命をいただいております。
個人事業の主として9年、法人を設立してからは6期目に入りました。
その間、システム・エンジニアを名乗って仕事を得、妻子を養ってきました。

そんな私ですが、システムは誰からも教わっていません。
全て独学です。
高校は普通科でしたから、パソコンはありません。大学は商学部で、パソコンルームはあった気がしますが、私は無関心でした。一度だけ5インチのフロッピーディスクに一太郎で作った文章を保存する課外授業をうけたぐらい。
大学の一回生の頃に、ダブルスクールに通っていましたが、そこで学んだ事で今に活かせているのはブラインドタッチ。Basicの初歩も学んだ記憶はありますがほとんど覚えていません。
大学を出た後、一年近く芦屋市役所でアルバイトをしていた頃、出入りしていた大手IT企業のSEの方から盗み取ったマクロからここまでやってきました。

そして私は新卒で採用された経験も、真っ当な転職活動の経験もありません。ですから、企業で研修を受けた経験もほとんどないのです。

システム・エンジニアとして働く常住現場で、悠長にプログラミングを教えてくれる人などいません。誰もが忙しくコーディングや設計に追われている中、そんな間抜けな質問をしたら退場させられてしまいます。

そんな私がどうやってプログラムを学んだのか。
本書のような入門書からです。

私の場合、キャリアの初期に開発現場ではなく、オペレーションセンターで働き、某企業でシステムの全権を任してもらえるという幸運もありました。

試行錯誤しながらExcelの複雑なワークブックやシートやセルを操り、毎朝のタイトなスケジュールを縫って資料を作る。
Excelのマクロを駆使しなければ、とてもやり遂げられなかったでしょう。

本書は基礎編ですが、応用編とコントロール編も含めて何度も当時読み返したものです。私にとってプログラミングを学ぶ上でこのシリーズには大変お世話になりました。

特に、行列の二次元だけでしかExcelを理解していなかった私を、本書は三次元、四次元といった高次のレベルに引き上げてくれました。スカパーのカスタマーセンターで当時の部下だった元SEの女性にもヒントもらったのが懐かしい。
あれこそまさにブレークスルーの瞬間でした。

かつて、システムを扱うにはプログラミングやデータベース、ネットワークの知識が必須でした。
ですが、昨今のシステムにはそうした知識が不要になりつつあります。クラウドシステムやノンコードプログラミングの普及によって。
そして、ビジネスの現場で働く人にこそ、プログラミングの初歩的な理解が必要になる。そんな時代はすでに始まっています。

Excelマクロは、処理を自動的にマクロとして記述する仕組みがあります。それと本書を組み合わせると、システムの初歩の概念を学んでいただけるのではないでしょうか。
本書はプログラミングに不得手なビジネス現場のオペレーターの皆様にこそ読んでほしいと思います。

それでは皆さんまた明日!
※毎日バトンを渡すこともあるようですが、私は適当に渡すつもりです。事前に了解を取ったうえで。
なお、私は今までこうしたチャレンジには距離を置いていました。ですが、このチャレンジは参加する意義があると感じたので、参加させていただいております。
もしご興味がある方はDMをもらえればバトンをお渡しします。

「かんたんプログラミング Excel VBA 基礎編」
単行本 ソフトカバー:351ページ
大村あつし(著)、技術評論社(2004/3出版)
ISBN978-4-7741-1966-3

Day1 「FACTFULLNESS」
Day2 「成吉思汗の秘密」
Day3 「占星術殺人事件」
Day4 「ワーク・シフト」
Day5 「かんたんプログラミング Excel VBA 基礎編」
Day6 「?」
Day7 「?」

★ ★ ★ ★ ★ ★ ★
7日間ブックカバーチャレンジ
【目的とルール】
●読書文化の普及に貢献するためのチャレンジで、参加方法は好きな本を1日1冊、7日間投稿する
●本についての説明はナシで表紙画像だけアップ
●都度1人のFB友達を招待し、このチャレンジへの参加をお願いする
#7日間ブックカバーチャレンジ ##かんたんプログラミング


ワーキングツリーにはkintoneとboxをお飾り!


kintone Advent Calendar 2019の24日目の記事です。

  Topへ↓

ん?この著者、この間もAdvent Calendarでみたで? はい。二度目の登場です。

kintoneは優れたツールですが完全ではない?
なーんてディスられても動じず、欠点を正直に認めるのもkintoneの憎めないところ。そう思ってやまない著者です。

kintoneの欠点のいくつかはすぐに挙げられます。
例えばブラウザーベースで動いているので、ファイルアップロードの作業が面倒、とか。
添付ファイルフィールドに画像データを放り込みまくると、一ユーザーあたり5GBの容量の制限が足かせになってくる、とか。

そんな限界を解消するためのささやかなクリスマスプレゼントを皆様にお届けしたいと思います。
なに、ちょっとした贈り物です。クリスマスツリーによくぶら下がっている箱のような。
箱・・・つまりboxです。
今回の記事では私の2019年の失敗事例も公開しているので、ひょっとしたら皆さまのご参考になるかもしれません。

box for kintoneのご紹介

  Topへ↑

世の中にオンラインストレージ製品はたくさんありますよね。その中でもboxが存在感を出しているのはご存じでしょうか。

kintoneとboxの連携はbox for kintoneというプラグインとして公開されています。
それを使えばブラウザー上でアップロードなどせず、ドラッグ&ドロップでブラウザー上から操作できちゃうのです。

クライアントツールのbox Driveをインストールすれば、Windowsのエクスプローラと同じ操作でbox上にファイルをアップロードできてしまう。なんて優れもの。

boxを使うと無尽蔵(契約プランによる)を誇る容量にファイルを置きまくり。
それをkintoneの画面上からに自由に呼び出せる。素晴らしい!
上に書いたkintoneの弱点を周囲のツールが補ってくれるいい例です。
本稿もそうしたkintoneを補ってくれる一つの例としてお役に立てれば幸いです。お日柄もよいので。

box for kintoneの使い方は、Cybozu developer networkにも出ています。
https://developer.cybozu.io/hc/ja/articles/205070124-Box-for-kintone その記事に従えば、簡単にbox for kintoneを導入できるはずです。

ところが、この記事で書かれているのは、一つのレコードに一つのフォルダーを対応させるところまで。
kintoneでちょっとしたシステムを作ろうとすれば複数アプリにまたがった構成が必要です。それに応じてboxのフォルダー構成も複数の階層にまたがってしまいます。

合同会社アクアビットダム設計なる会社

  Topへ↑

たとえば、大阪と東京に支店がある合同会社アクアビットダム設計があったとします。
この会社はダムを独自の技術で製造し、お客様にお納めしている設定です。

受注システムをkintoneで構築するにあたり、
大阪支店[組織]の
長井何某[個人]が担当する
担当案件[案件]の
施行状況[施工]と
湛水状況[湛水]を管理すると仮にしましょう(工程はしょりすぎ。ちなみに最後の工程は水を貯める工程です)。

他に顧客マスタがあるでしょうがここは割愛。また、[組織]と[個人]はアプリではなく、kintoneのアカウントを使用する想定です。
この場合3アプリですね。

ここでご注文からの流れをkintoneで管理したとしましょう。
各アプリの連動はkintoneのアクション機能を使ったとします。

ダム完成までにはさまざまな状況を報告していかねばなりません。するとダムの進捗に合わせて写真が大量に溜まっていきます。
kintoneの添付ファイルフィールドにファイルをアップしていると、すぐに容量が危うくなりかねません。
ここでboxの出番です。

ここでboxで写真を管理しようとした場合、box内のフォルダー構成はこのようになると思います。

さて、先ほどご紹介したbox for kintoneを思い出してみましょう。
プラグイン設定画面にルートフォルダーのIDを設定していましたね。

つまり、アプリ自体にルートフォルダーのみを作る仕様。
それって、どのレコードであろうと共通で1つのフォルダーだけ、、、
いやいや多層boxと多層アプリでは対応できないのはちょっと、、、
結論! box for kintoneだとちょっとキツイかも。

合同会社アクアビットダム設計にboxを

  Topへ↑

ということで、本稿では多段階にわたるboxの連動例をお伝えしたいと思います。また、その時にしでかしてしまった失敗と、そのリカバリ例もお伝えしたいと思います。

まず、話を簡単にするため、合同会社アクアビットダム設計としてのルートフォルダーを設定しておきましょう。

さらに、支店ごとにフォルダーを設定し、支店の配下に担当ごとのフォルダーも生成しておくと話が早いですね。

実際のboxのフォルダー構成はこんな感じ。

ここでルートフォルダーのフォルダーIDを取得しておきます。boxの画面から取れます。

その状態で、案件アプリに新規レコードを登録します。
案件アプリの項目には案件の主管支店と、案件の主担当を指定するフィールドも忘れずに。もちろん必須項目として。

なぜ必須項目にするのでしょう。
その理由は、レコードが保存成功後、案件フォルダーを作る際にどこのフォルダーの配下に作成するか決めなければならないためです。

boxのフォルダー生成APIについて

  Topへ↑

ここでboxの仕様を押さえておきましょうか。
サービスの仕様を確認するには、APIから逆引きしたほうが理解しやすい。いわゆる技術者あるあるです。
boxのAPIはこちらのサイトをご覧になると良いでしょう。
https://ja.developer.box.com ・・developerサイトトップ
https://ja.developer.box.com/reference ・・APIレファレンス

boxのフォルダー作成の項を読むと、親であるフォルダーのIDがパラメーターとして必須のようです。
編集時には親フォルダーのIDは必須ではなくなりますが、もしフォルダーの場所を移動する際は親フォルダーのIDは指定しなければなりません。

案件フォルダーを作る際は、親となる担当者フォルダーのIDを把握しておかねばなりません。
そしてその上の支店フォルダーも。

つまり、案件レコードの保存のタイミングで行うべきことは、まず、そのレコードの支店フィールドの値に等しい支店フォルダーを検索することです。
その際、基準となるのはルートフォルダーです。

ルートフォルダーの下にある支店フォルダーを検索し、そのIDを特定します。
間髪入れずに支店フォルダーの配下にある担当者フォルダーを検索します。
これは同一担当者が複数支店にフォルダーを持っている場合など、運用も考慮していますが、支店フォルダーのIDを内部で保持できるのであれば、いきなり担当者フォルダーから検索してもよいです。
重要なのは案件フォルダーを作成するにはその親フォルダーのIDを事前に必ず保持しておくことです。

先ほど、kintoneの案件アプリの支店と担当者の両フィールドは必須でなければならないとしたのには、そういう理由があったのです。
これら二つのフィールドの値がないと、案件を保存する際に生成されるべき案件フォルダーの保存先が迷子になってしまうので。

JavaScriptで実装してみた

  Topへ↑

続いてはいよいよboxの操作を行います。

その前に本稿ではboxの権限周りには踏み込まないことを言っておきます。
OAuthについては、もともとbox  for  kintoneで用意されていたclient IDを使用します。本当はbox内でアプリを作成し、そのアプリ内で設定した権限を認証しなければならないのですが。
box for kintoneに甘えてしまいましょう。

ついでにpromise処理が考慮されたAPI実行部分もbox for kintoneの処理を流用させていただきましょう。

処理の大まかな順序としては以下の通りです。
ただ、のちに述べますが、このコードは動きません。なのでコードは画像として参考程度に載せます。

まず、イベントが動くタイミングはapp.record.create.submit.successです。新規作成処理成功後ですね。

処理の都合上、この中で別のアプリに更新を行い、その結果が成功した場合にboxフォルダー生成処理を呼び出しています。

boxフォルダー生成処理では、まずルートフォルダーから支店フォルダーを検索します。
続いて支店フォルダーから担当者フォルダーを検索します。

ここでboxの検索の仕様が立ちふさがってきます。
boxの検索仕様として、対象の種類、生成時刻、オーナーIDや親フォルダーIDなどは指定できるのですが、肝心の文字列を完全一致で検索できないのです。queryというパラメータがあるにもかかわらず、そこに指定した文字列は曖昧検索として処理されてしまうのです。
APIレファレンス

つまり、親フォルダーに属する検索対象が複数ありうる場合、検索文字列に工夫が必要です。例えば姓名の間にスペースが入る場合など。
「長井 権兵衛」と「長井 主水」が対象のフォルダー配下にあって「長井 権兵衛」を検索したい場合、queryに「長井 権兵衛」を設定してもマッチしません。
ではどうやればよいか。
スペースの前後の文字列で検索するのです。
この場合、「長井」または「権兵衛」で検索します。すると前者は二件がヒットし、後者は一件がヒットします。
その結果を再度ループして回し、一件ごとにname属性の値が検索文字列に一致するかを確認する。
そのような面倒な処理がboxの検索には必要です。

このコードも実際は使っていませんが、軽く提示します。

これで、担当者フォルダーIDまで求められました。

boxのフォルダー生成と検索にまつわる問題

  Topへ↑

続いてはフォルダーの生成に移りましょう!
APIレファレンス

生成にあたっては、名前と親フォルダーのIDを指定するだけです。

これで、案件レコードが保存されたら案件フォルダーを作成するところまでができました。

ここで当初想定していたboxの構成を見てみましょう。
案件フォルダーの配下に「提案状況」「施工状況」「湛水状況」の三フォルダーがあります。
このフォルダーの生成にも実は厄介な問題が潜んでいます。

例えば、案件のレコードが保存されました。そして案件フォルダーが生成されました。
そしたら、アクション機能によって施工状況アプリにレコードをコピーし、施工状況アプリでもレコードが保存された瞬間、案件レコードの時と同じように施工状況フォルダーを生成すればええんちゃうの?と思ったでしょう。

ところが、フォルダーを生成するには親のフォルダーの指定が必須です。
親フォルダー、つまり案件フォルダーをフォルダー生成処理の直前で検索してフォルダーIDを取得しなければなりません。
ところがこの親フォルダーの検索取得にはひとつハードルが控えています。そのハードルとは、コンテンツが生成されてから検索可能となるまでに時間がかかる、というものです。
boxはなんらかのコンテンツが作成されてから、それが検索可能となるまでにbox内部でindexを構築しており、それに時間が掛かるのです。
boxの APIレファレンスには以下のように書かれています。

つまり、案件フォルダーの生成からすぐ、施工状況アプリのレコードを保存した場合、親となるべきフォルダーが検索できないため、親フォルダーの指定ができないのです。

boxを多層構造でkintoneと連動させる場合、この仕様上の制限は現状では避けられません。

この制限を回避するため、発想を切り替えました。
つまり、案件フォルダーが生成された後、同時に配下のフォルダーも作ってしまうのです。

box APIでは、フォルダー生成が成功した時点で返り値として生成されたフォルダーのIDが得られます。このIDを使えば配下のフォルダーも即時に生成できます。

このコードも実際は使っていませんが、軽く提示します。

この下の処理ではさらに生成した案件フォルダーのURLを取得し、そのURLやフォルダーIDをkintoneの2アプリに更新して設定しています。

ここまででkintoneのapp.record.create.submit.successイベントを見てきました。その結果、実装ができそうです。
テストでもフォルダーが意図通りに生成されました。開発用のPCでも、お客様のご担当者様のPCでも。

バグ大魔王降臨!

  Topへ↑

ところが! やったと思った安心のかげに潜むのが落とし穴。バグが出てしまったのです。
テストではうまく動いていたのに、いざ本番になるとうまくいかない。なんということでしょう!

実は、その根本的な原因は今もなお究明できていません。
事象としてはboxにAPIリクエストを投げた後、何も戻ってこないのにプログラムが終了してしまいます。httpレスポンスすら帰ってこずに。
それも終了する場所がまちまちなのが始末が悪い。複雑なPromiseの構造に加え、referredを混在させたことにも問題があったのかもしれません。
この不具合がやっかいなのは、boxからのレスポンスを待つ間、app.record.create.submit.successの結果が完了できないことにあります。その間、ブラウザーは固まってしまい、kintoneを利用されている皆様にはただ困惑が。

そして、この不具合の原因がブラウザーにあるのか、box側にあるのか、kintone内部にあるのか、それともPCのスペックにあるのか。はたまたネットワーク環境によるものなのか。いまだに分っていません。
ただ、ブラウザー上でレスポンスを待つ運用はまずい、という悔いだけは骨身に沁みました。
私はその原因を追究するよりもお客様の運用を円滑に進めることを優先しました。
その決断として、ブラウザーに依存する実装を止めました。

AWSへ処理を移管

  Topへ↑

では、どうすればよいか。

幸いなことにkintoneにはWebhookという機能が備わっています。Webhookには、レコード保存時にWebhookのリクエストを受け付けてくれるWebhook URLを設定できます。

私がWebhook URLとして設定したのはAWSのAPI Gatewayで設定したURLでした。
API Gatewayについての説明は割愛しますが、kintoneから受け取ったWebhookのリクエストに含まれるJSONを読み取り、それを後続の処理に渡すことができます。
後続の処理にはAWS Lambdaを選びましたので、同じAWS上で処理が連携できます。

AWS LambdaではNode.jsを使い、ほぼkintoneのkintone.app.create.submit.successで実装したのに近いコーディングを行いました。
box Node SDKがAWS Lambdaから簡単に使用できるので、それを使えば似たような実装ができるのです。
ただし、boxのアプリは一から作る必要があります。設定もあれこれ行う必要が生じました。
最初、こちらのブログの力も借りました。ありがとうございました。
Lambda関数のコードを以下に掲示します。なお、このコードは動いているものを基にいろいろといじっているので参考になると思います。

/**
 * This sample demonstrates how to call Box APIs from a Lambda function using the Box Node SDK.
 *
 * For step-by-step instructions on how to create and authorize a Box application,
 * see https://github.com/box/samples/tree/master/box-node-lambda-sample.
 */
const BoxSDK = require('box-node-sdk');                                                // Node.jsのbox-node-sdkモジュールを呼び出す
const request = require('request');                                                    // Node.jsのrequestモジュールを呼び出す
const boxConfig = JSON.parse(process.env.BOX_CONFIG);                                  // AWS Lambdaの環境変数のBOX_CONFIGの値をJSONで扱えるように

boxConfig.boxAppSettings.appAuth.keyID = boxConfig.boxAppSettings.appAuth.publicKeyID; // 9行目で取り出したkeyIDにpublicKeyIDを代入

const sdk = new BoxSDK(boxConfig.boxAppSettings);                                      // 9行目で取り出したboxAppSettingsをsdkに代入

/**
 * Create a service account client that performs actions in the context of the specified
 * enterprise.  The app has a unique service account in each enterprise that authorizes the app.
 * The service account contains any app-specific content for that enterprise.
 * Depending on the scopes selected, it can also create and manage app users or managed users
 * in that enterprise.
 *
 * The client will automatically create and refresh the service account access token, as needed.
 */
const client = sdk.getAppAuthClient('enterprise', boxConfig.enterpriseID);             // boxアプリが適用できるアカウントのグローバル設定を管理

var DOMAIN = '*********.cybozu.com'; //kintone環境のドメイン                            // *****はご使用のkintoneのサブドメインを
var APP_ID_1287 = 1287;   //案件管理アプリのアプリID
var BASE_URL = "https://" + DOMAIN + '/k/v1/';
var APITOKEN_1287 =  "kintoonkaramottekitatookunwokokoniiretene";                      // kintoneの案件アプリのAPIトークン
var headers_1287 = {'X-Cybozu-API-Token': APITOKEN_1287};                              // リクエストで使用するヘッダ
var FolderId;
var updaterecordid_1287;

exports.handler = (event, context, callback) => {                                      // eventはkintoneのWebhookからAPI Gatewayを経由したレコード情報
                                                                                       // contextはLambda関数に関する情報
    const API_BASE_PATH = 'https://api.box.com/2.0';                                   // box Node SDKの文法に準拠

    // targetnameはコンテンツの文字列
    // typeはコンテンツの対象。本稿の場合はfolder
    // content_typesは検索対象とするプロパティ。本稿の場合はname
    // limitは検索結果として戻す件数。
    // idsは親フォルダーのフォルダーID
    // methodは本稿では全てGETなので使用していない
    // dataは本稿の場合検索対象(支店,担当者,案件No)のうち、担当者の場合["担当名"]で渡ってくる。
    function searchFolder(targetname, type, content_types, limit, ids, method, data, success, error) {  //157,159,161行目から呼び出されて検索処理を実施
        if (data !== undefined) {                                 // dataが指定されている場合
            if (data[0] === "担当名") {                            // dataの配列の最初の要素が"担当名"の場合
                targetname = targetname.split(' ')[0];           // 受け取るtargetnameは「長井 権兵衛」の様に全角スペースで区切られた姓名なので姓を取得
            }
        }

        return new Promise(function (resolve, reject) {           // Promiseを設定
            client.search.query(                                  // 24行目でclientとして承認されたbox Node SDKのsearchクラスのquery関数を呼び出し
                "\"" + targetname + "\"",                         // 最初のパラメーターは検索対象文字列。文字列なのでエスケープした""で囲む。でも曖昧検索
                {                                                 
                    fields: 'id,name,modified_at,extension,permissions,collections',  //検索結果として返すコンテンツのプロパティ
                    type: type,                                                       //folder
                    content_types: content_types,                                     //検索対象はnameプロパティ
                    limit: limit,                                                     //結果として返す件数
                    ancestor_folder_ids: ids,                                         //親フォルダーID 
                    offset: 0                                                         //オフセットしないので0
                })
                .then(function(results){                                              //結果が取得されたのでこのPromiseチェーンへ
                    if (data !== undefined) {                                         //dataが指定されている場合
                        if (data[0] === "担当名") {                                   //dataの配列の最初の要素が"担当名"の場合
                            for (var i = 0; i < results.entries.length; i++) {        //戻り値の件数分(limitで指定した件数分)
                                if (results.entries[i].name === data[1]) {            //戻り値のnameプロパティがdataの2番目の要素(担当名)か
                                    resolve(results.entries[i].id);                   //Promiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                                }
                            }
                        }
                    } else {
                        resolve(results.entries[0].id);                               //Promiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                    }
                })
                .catch(function(error){ // エラーの場合
                    reject(error);
                });
        });
    }

    // createParamは生成フォルダー名と親フォルダーIDが含まれたJSONオブジェクト
    function postFolder(createParam) {                     //176行目から呼び出されてフォルダー生成処理を実施
        return new Promise(function (resolve, reject) {    // Promiseを設定
            client.folders.create(createParam.parent.id, createParam.name)  //24行目でclientとして承認されたbox Node SDKのfolderクラスのcreate関数を呼出
                                                                            //1つ目は親フォルダーID、2つ目は生成するフォルダーの名称 
                .then(function(results){                                    //85行目の処理が成功したのでこのPromiseチェーンへ
                    var ankenid = results.id;                               //生成したフォルダーIDを以下のforeach内で使うためにankenidに代入 
                    var subfolders = [                                      //生成した案件フォルダーの配下に作成する三つのフォルダー名を配列にしています
                        "提案資料",
                        "施工状況",
                        "湛水状況"
                    ];

                    var promiseset = [];                                    //三つのフォルダーの生成が終わるまで待つPromiseを三つ作るので配列を設定
                    subfolders.forEach(function(val,index,ar){              //89行目で生成した配列の各要素をループします
                        promiseset[index] = new Promise( function( resolve, reject ) {  //95行目で生成した配列にPromiseを設定します。
                            client.folders.create(ankenid, val)             //24行目でclientとして承認されたbox Node SDKのfolderクラスのcreate関数を呼出 
                                                                            //1つ目は親フォルダーID(案件フォルダー)、2つ目は生成するサブフォルダーの名称
                                .then(function(results){                    //98行目の処理が成功したのでこのPromiseチェーンへ
                                    resolve(results.id);                    //97行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                                }).catch(function(error){                   //98行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                          //97行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                        });
                    });
                    Promise.all( promiseset )                               //97行目で設定した三つのPromiseが全て完了したらここに来る
                        .then( function ( message ) {
                        resolve(ankenid);                                   //84行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                    })
                        .catch( function ( reason ) {                       //97行目で設定した三つのPromiseのどれかが失敗したらここに来る
                            console.log( reason ) ; // "失敗!!"
                        reject(false);                                      //84行目のPromiseは失敗したとエラーオブジェクトを返す
                    });
                })
                .catch(function(error){                                     //85行目の処理は失敗したらここに来る
                    reject(error);                                          //84行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }

    // boxフォルダーIDは更新対象となるフォルダーID
    // createParamは更新フォルダー名と親フォルダーIDが含まれたJSONオブジェクト
    function putFolder(boxフォルダーID, updateParam) {             //165行目から呼び出されてフォルダー更新処理を実施
        return new Promise(function (resolve, reject) {         // Promiseを設定
            client.folders.update(boxフォルダーID, updateParam)    //24行目でclientとして承認されたbox Node SDKのfolderクラスのupdate関数を呼出
                                                                //1つ目は対象となるフォルダーID、2つ目は更新するフォルダー情報の含まれたJSONオブジェクト 
                .then(function(results){                        //126行目の処理が成功したのでこのPromiseチェーンへ
                    resolve(results.id);                        //125行目のPromiseは完了したと戻り値のidプロパティ(フォルダーID)を返す
                })
                .catch(function(error){                         //126行目の処理が失敗したのでこのPromiseチェーンへ
                    reject(error);                              //125行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }

    function getFolderURL(createdid) {                          //178行目から呼び出されてフォルダーの共有処理を実施
        return new Promise(function (resolve, reject) {         // Promiseを設定
            client.folders.update(createdid, {shared_link: client.accessLevels.OPEN}) 
                                                                //24行目でclientとして承認されたbox Node SDKのfolderクラスのupdate関数を呼出
                                                                //1つ目は対象となるフォルダーID、2つ目は更新するフォルダーのプロパティ(共有設定)
                .then(function(results){                        //139行目の処理が成功したのでこのPromiseチェーンへ
                    resolve(results.shared_link.url);           //138行目のPromiseは完了したと戻り値の共有URLプロパティ(リンクURL)を返す
                })
                .catch(function(error){                         //139行目の処理が失敗したのでこのPromiseチェーンへ
                    reject(error);                              //138行目のPromiseは失敗したとエラーオブジェクトを返す
                });
        });
    }
    function createBoxFolder(支店, 担当者, 案件No, boxフォルダーID) {  //212行目から呼び出されてフォルダーの共有処理を実施
        return new Promise(function (resolve,reject) {             // Promiseを設定
            var rootfolder = "12345678910";                        //boxのフォルダー制御のルートとなるフォルダーのフォルダーIDを静的に代入
            var ownerbranchfolder;                                 //支店フォルダーのフォルダーID
            var personinchargefolder;                              //担当者フォルダーのフォルダーID
            var createParam;                                       //searchFolder関数へはダミーオブジェクト。putfolderとpostfolderへはJSONオブジェクト

            searchFolder(支店, "folder", "name", 10, rootfolder, 'GET', createParam).then(function (branchfolderid) {  //45行目へ
                ownerbranchfolder = branchfolderid;                           //searchFolderからの返り値を上位スコープのownerbranchfolderへ代入
                searchFolder(担当者, "folder", "name", 10, ownerbranchfolder, 'GET', ["担当名",担当者]).then(function (personfolderid) {  //45行目へ
                    personinchargefolder = personfolderid;                    //searchFolderからの返り値を上位スコープのpersoninchargefolderへ代入
                    searchFolder(案件No, "folder", "name", 10, personinchargefolder, 'GET', createParam).then(function (projectfolderid) {  //45行目へ
                        var name = "案件No" + " " + 案件No;                    //生成/更新する案件フォルダーの名称を設定する
                        createParam = {name: name, parent: {id: personinchargefolder}};  //案件フォルダーの設定情報をJSONオブジェクトに組み立てる
                        if (projectfolderid.length > 0) {                     //161行目で案件フォルダーが存在した場合(フォルダー情報更新)
                            putFolder(projectfolderid, createParam).then(function (updatedid) {  //124行目へ
                                FolderId = updatedid;                         //161行目の処理で得た更新したフォルダーIDをスコープ外の168行で使うため
                                getFolderURL(updatedid).then(function (updatedurl) {             //137行目へ
                                    resolve(FolderId+"****"+updatedurl);      //151行目のPromise完了をフォルダーIDと共有URLプロパティ(リンクURL)で返す
                                }).catch(function(error){                     //167行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                            //151行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                            }).catch(function(error){                         //165行目の処理が失敗したのでこのPromiseチェーンへ
                                reject(error);                                //151行目のPromiseは失敗したとエラーオブジェクトを返す
                            });
                        } else {
                            postFolder(createParam).then(function (createdid) {       //83行目へ
                                FolderId = createdid;                                 //生成したフォルダーIDを以下の179行目で使うためにFolderIdに代入
                                getFolderURL(createdid).then(function (createdurl) {  //137行目へ
                                    resolve(FolderId+"****"+createdurl);      //151行目のPromise完了をフォルダーIDと共有URLプロパティ(リンクURL)で返す
                                }).catch(function(error){                             //178行目の処理が失敗したのでこのPromiseチェーンへ
                                    reject(error);                                    //151行目のPromiseは失敗したとエラーオブジェクトを返す
                                });
                            }, function(res) {                                        //176行目のフォルダー生成処理でrejectレスポンスが返った場合
                                if (res.status && res.status === 409) {               //176行目のフォルダー生成処理でrejectレスポンスが409返った場合
                                    if (res.context_info                              //176行目のフォルダー生成処理でrejectレスポンスが競合を示した場合
                                        && res.context_info.conflicts
                                        && res.context_info.conflicts.length > 0) {
                                        return;                                       //150行目のcreateBoxFolder関数を終える
                                    }
                                }
                            }).catch(function(error){                                 //176行目のフォルダー生成処理でエラーが帰った場合
                                reject(error);                                        //151行目のPromiseは失敗したとエラーオブジェクトを返す
                            });
                        }
                    }).catch(function (error) {                                       //161行目のフォルダー検索処理でエラーが帰った場合
                        // 非同期処理失敗。呼ばれない
                        console.log(error);
                    });
                }).catch(function (error) {                                           //159行目のフォルダー検索処理でエラーが帰った場合
                    // 非同期処理失敗。呼ばれない
                    console.log(error);
                });
            }).catch(function (error) {                                               //157行目のフォルダー検索処理でエラーが帰った場合
                // 非同期処理失敗。呼ばれない
                console.log(error);
            });
        });
    }

    var recordjson = JSON.parse(event.body);                                 //34行目で受け取ったkintoneのWebhookのレコード情報をJSON形式で扱えるように
    updaterecordid_1287 = recordjson.record.レコード番号.value;               //210行目のレコードデータの「レコード番号」フィールドの値を代入
    createBoxFolder(recordjson.record.支店.value[0].code,                    //150行目へ
                    recordjson.record.担当者.value, 
                    recordjson.record.案件No.value, 
                    recordjson.record.boxフォルダーID.value).then(function(idurl) {
        if (idurl) {                               //212行目のcreateBoxFolderの戻り値(168、179行目で値設定)
            var targetrecordids = [updaterecordid_1287+"**"+APP_ID_1287];   //211行目で設定したレコード番号と27行目で設定したアプリID
            var kintonepromiseset = [];                                     //kintoneのレコードアップデートが終わるまで待つPromiseの配列を設定
            targetrecordids.forEach(function(val,index,ar){                 //217行目で生成した配列の各要素(本稿では1つ)をループします
                kintonepromiseset[index] = new Promise( function( resolve, reject ) {  //218行目で生成した配列にPromiseを設定します。
                    var body_post = {                                                  //kintoneの既存案件アプリを更新するレコードを組み立てます。
                        app: val.split("**")[1],                                       //217行目で設定した配列の**で区切られた右側(アプリID)
                        id: val.split("**")[0],                                        //217行目で設定した配列の**で区切られた左側(レコード番号)
                        record: {
                            boxフォルダーID: {
                                value: idurl.split("****")[0]                       //212行目のcreateBoxFolderの戻り値の****で区切られた左のフォルダーID
                            },
                            表示: {
                                value: idurl.split("****")[1].replace("*******.box.com","app.box.com")
                                         //212行目のcreateBoxFolderの戻り値の****で区切られた右のURL(契約のboxのサブドメインをapp.box.comに置換の必要あり)
                            }
                        }
                    };
                    var options_getsalesamount = {                            //リクエストのbody部分を組み立てます。
                        url: BASE_URL + 'record.json',                        //28行目で設定したURLのルートと一行レコードの更新なのでrecord.jsonを連結
                        method: 'PUT',                                        //更新なのでPUT
                        headers: headers_1287,                                //30行目で設定したAPIトークン
                        'Content-Type': 'application/json',                   //リクエストのボディ部分のタイプ
                        json: body_post                                       //221行目で設定したボディ部分
                    }
                    //レコードを取得
                    request(options_getsalesamount, function (error, response, body) {    //Node.jsのrequestモジュールで234行のリクエストを送信
                        if (error) {                                                      //242行目の値がerrorだったら
                            console.log('Error: ' + error.message);
                            reject();                                                     //220行目のPromiseは失敗したとエラーオブジェクトを返す
                        }
                        console.log("kintone recordput:succcess"+val);
                        resolve();                                                        //220行目のPromise完了を返す
                    });
                });
            });
            Promise.all( kintonepromiseset )                                              //220行目で設定したPromiseが全て完了したらここに来る(本稿は1つ)
                .then( function ( message ) {                                             //252行目の処理が成功したのでこのPromiseチェーンへ
                    context.done(null, {text: "kintone POST and Box Folder Create success!"});  //Lambdaの処理結果をログとして残す
            })
                .catch( function ( reason ) {
                    context.done(null, {text: "Box Folder Create failed!"});              //Lambdaの処理結果としてエラーログ
                return;
            });
        } else {
            context.done(null, {text: "Box Folder Create failed!"});                      //Lambdaの処理結果としてエラーログ
        }
    }, function(res) {                                                                    //212行目の返り値がrejectで戻ってきた場合
        context.done(null, {text: "Box Folder Create failed!"});                          //Lambdaの処理結果としてエラーログ
        return false;                                                                     //212行目の結果としてfalseを返す
    });
};

 

なんとか実装

  Topへ↑

いずれにせよ、私が2019年に出した唯一の大きなバグがこれでした。
結局、バグが出てから実運用にこぎつけるまでにさらに二カ月ほどの時間をいただきました。お客様にも多大なご迷惑をおかけしてしまいました。

これが実装できたことで、案件アプリにレコードを登録した時点で、Webhookが発動し、AWS API GatewayからAWS Lambdaを介してboxへのフォルダー生成と、レコードに対応するboxのURLとフォルダー番号をkintoneの該当レコードに登録することができました。

kintoneの画面上にboxのフォルダーを出す部分はbox for kintoneの内部にも書かれている通りです。
実際それを使わせていただいています。ありがとうございます。
以下にコードを載せていますが、疲れてきたのでコード内のコメントは割愛します。ごめんなさい。

(function() {
    'use strict';

    var BOX_CLIENT_ID = 'wkgp4k64whsha8mwvg7k5k63cim82mmv';   //sample_plugin_default
    // localStorage
    var LOCAL_STORAGE_PREFIX = 'kintone.plugin.' + BOX_CLIENT_ID;
    var LOCAL_STORAGE_JUDGED_ALLOW_ACCESS = LOCAL_STORAGE_PREFIX + '.judgedAllowAccess';

    var config = [];

    var BOX_EMBED_WIDTH = 840;
    var BOX_EMBED_HEIGHT = 420;

    var getUrl = function(path) {
        var matchedGuestSpacePath = location.pathname.match(/^\/k\/(guest\/\d+\/)/);
        var guestSpacePath = '';
        if (matchedGuestSpacePath !== null && matchedGuestSpacePath.length === 2) {
            guestSpacePath = matchedGuestSpacePath[1]; // "guest//"
        }
        var apiPath = '/k/' + guestSpacePath + path;
        return apiPath;
    };

    var boxApi = {
        clientInfo: {'provider': 'box', 'client': BOX_CLIENT_ID},

        getAccessToken: function() {
            // add a hash parameter for distinguishing OAuth redirect
            var delimiter = (location.hash.indexOf('#') === 0) ? '&' : '#';
            location.hash += delimiter + BOX_CLIENT_ID + '.oauth_redirect=true';
            kintone.oauth.redirectToAuthenticate(this.clientInfo, location.href);
        },
        hasAccessToken: function() {
            return kintone.oauth.hasAccessToken(boxApi.clientInfo);
        }
    };

    var validateConfig = function(record) {
        config['folderId'] = '0';//Box親フォルダーID
        config['keyFld'] = '顧客名';//kintoneキーフィールド
        config['boxUrl'] = '表示';//Box共有リンクの格納先
        config['boxFolderId'] = "boxフォルダーID";
        config['access'] = 'Open';//Box共有リンクのアクセス権[Collaborator/Company/Open]
        config['prohibitToDownload'] = 'false';//コラボレータにのみダウンロードを許可する

        if (!config) {return false; }
        return true;
    };

    var decorateBoxLinkField = function(boxUrl) {

        var boxLinkPattern = /^https:\/\/([a-zA-Z0-9]+).box.(com|net)(\/s\/[a-z0-9]+)$/;
        var match = boxUrl.match(boxLinkPattern);
        if (!match) {
            return;
        }
        var iframeSrc =
            'https://app.box.com/embed_widget/000000000000' +
            match[3] +
            '?theme=gray' +
            '&show_parent_path=no' +
            '&show_item_feed_actions=no' +
            '&partner_id=233';

        var elEmbed = kintone.app.record.getFieldElement(config.boxUrl);
        if (elEmbed === null) {return; }
        $(elEmbed).empty();

        var width = BOX_EMBED_WIDTH;
        var height = BOX_EMBED_HEIGHT;

        $(elEmbed).parent().css({
            'width': (width + 100) + 'px',
            'height': 'auto',
            'background-color': 'rgba( 255, 255, 255, 0 )'
        });
        var embedIframe = $('', {
            src: iframeSrc,
            width: width,
            height: height,
            frameborder: '0',
            allowfullscreen: 'true',
            allowscriptaccess: 'always'
        });
        $(elEmbed).append(embedIframe);
    };

    var judgedAllowAccessFlag = {
        isSet: function() {
            return (localStorage.getItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS) !== null);
        },

        set: function() {
            localStorage.setItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS, 'true');
        },

        remove: function() {
            localStorage.removeItem(LOCAL_STORAGE_JUDGED_ALLOW_ACCESS);
        }
    };

    kintone.events.on('app.record.detail.show', function(e) {
        if (validateConfig(e.record)) {
            var boxUrl = e.record[config.boxUrl].value;
            if (!e.record[config.boxUrl].value) {

                var elEmbed = kintone.app.record.getFieldElement(config.boxUrl);
                if (elEmbed === null) {return null; }
                $(elEmbed).empty();

            } else {
                decorateBoxLinkField(boxUrl);
            }
        }

        return e;
    });

    var checkAccessToken = function() {
        var oauth_redirect_param = BOX_CLIENT_ID + '.oauth_redirect=true';
        if (location.hash.indexOf(oauth_redirect_param) !== -1) {
            judgedAllowAccessFlag.set();

            // remove a hash parameter
            location.hash = location.hash.replace(oauth_redirect_param, '');

            var t = setInterval(function() {
                if (location.hash.indexOf(oauth_redirect_param) !== -1) {
                    // cancel button was clicked
                    clearInterval(t);
                    location.href = getUrl(kintone.app.getId() + '/');
                }
            }, 500);
        } else if (!judgedAllowAccessFlag.isSet() || !boxApi.hasAccessToken()) {
            kintone.oauth.clearAccessToken(boxApi.clientInfo, function(body, status, headers) {
                boxApi.getAccessToken();
                return null;
            });
        }
    };

    kintone.events.on('app.record.create.show', function(e) {
        if (validateConfig(e.record)) {
            checkAccessToken();
            e.record[config.boxUrl]['disabled'] = true;
            e.record[config.boxFolderId]['disabled'] = true;
        }

        return e;
    });

    kintone.events.on('app.record.edit.show', function(e) {
        if (validateConfig(e.record)) {
            if (!e.record[config.boxUrl].value) {
                checkAccessToken();
            } else {
//                e.record[config.keyFld]['disabled'] = true;
                e.record[config.boxFolderId]['disabled'] = true;
            }
            e.record[config.boxUrl]['disabled'] = true;
        }
        return e;
    });

    kintone.events.on('app.record.index.edit.show', function(e) {
        if (validateConfig(e.record)) {
            e.record[config.boxUrl]['disabled'] = true;
            e.record[config.keyFld]['disabled'] = true;
            e.record[config.boxFolderId]['disabled'] = true;
        }
        return e;
    });
})();

 

 

まとめ

  Topへ↑

実案件ではさらに凝った実装(フォルダー数も階層も本稿の例よりさらに多い)が施されています。
そして、古くboxが設定されていないレコードには手作業がたまに発生しているものの、実運用に乗っています。
この記事ではそれ以上の情報を出すことはお客様の業務に関わるのでここまでにしとうございます。

本稿がkintoneを運用している皆様にとって少しの手助けになれば幸せです。

kintone上で大量の添付ファイルに困っていらっしゃる方や、社内ファイルサーバーからの移行でお困りの方。他のPaaSからkintoneへ移行する作業があって、添付ファイルの扱いにお困りの方。
弊社では本稿のようなboxとkintoneの連動事例を何例も手掛けております。お困りの際はおっしゃってくださいませ。

最後に蛇足ですが、boxの案件で例に挙げた三つのダムは、私が実際に訪れてダムカードを入手した場所です。


当エントリーの参考にさせていただいたブログ

  Topへ↑

最後になりましたが、このエントリー作成にあたり、以下の2サイトからの情報を参考にさせていただきました。ありがとうございました。

 box APIレファレンス
 AWS Lambda上でBox Node SDKを利用する-九龍堂雑録


コーチングのグラフってkintoneで出せるんやって!


kintone2 Advent Calendar 2019の5日目の記事です。

  Topへ↓

突然ですが皆さん、コーチングって聞いたことがありますか?
あっ! そこのあなた、ページはそのままに!
これは間違いなくkintone Advent Calendarの記事ですから。
ほら!

きとみちゃん楽しいですよね!
https://kintone.cybozu.co.jp/jp/kitomi/

日々、お仕事に励むきとみちゃん。
きとみちゃんとお仕事をする仲間はとっても個性が豊か。

ちょっぴりあわてんぼうでドジっ子のきとみちゃんがkintoneに救われる姿は微笑ましいです。
ちなみに私は巻物で見積書を出してくださる麻呂な方が好きです。この方のお名前はなんでおじゃる?

さて、きとみちゃんがお仕事をする上で助けになる手法はkintoneの他にもさまざまなものがあります。

その中の一つが冒頭に書いたコーチングなのです。

コーチングを一言で言い表すなら、
・相手の学習や成長、変化を促し、相手の潜在能力を解放させ、最大限に力を発揮させる。
でしょうか。

詳しくはWikipediaの「コーチング」
をご覧くださいませ。

ビジネスにフォーカスを当てたコーチングの歴史はまだまだ浅いです。
ここでお伝えしておかなければならないのは、自己啓発セミナーとは違う、ということです。

と、偉そうにウンチクを述べる私ですが、コーチングを受けた経験は人生で1,2回だけ。
では、そんな私がkintone Advent Calendarで何を語るというのでしょうか。

結論を先に書いちゃうと、kintoneでこんなグラフを作ってみましょう!
ということなんですね。

グラフとデータのご説明

  Topへ↑

上に登場したのは四つの傾向を円グラフにしたものです。
それぞれの傾向の文字列にマウスを合わせると、事前に登録しておいたキーワードが出てくる。
これ、実は以前、お客様に依頼されて作ったkintoneにChart.jsを組み込んだグラフ生成の仕掛けです。

私はコーチングには無知です。
ですから、kintoneに入力画面を作り、その結果を集計することで、設問に応じた四つの傾向が算出できる、ということを知ったときは新鮮でした。

お客様によれば、
相手をほめる場合の、個人に響くキーワードは4つの傾向に分けられる
だそうです。

それに合わせて、こんな入力画面を作ってみました。

仮に20問の設問としています。それぞれの4つの傾向ごとに5問を設問しました。
それぞれの問いごとに
・よく当てはまる
・当てはまる
・当てはまらない
・まったく当てはまらない
の4種類の答えをラジオボタンで設定しています。

もちろん、さらに設問数を増やすことも可能ですし、設問数を自在に増減させたい、というご要望もあるでしょうね。
その場合はサブテーブルを使えばよさそうです。
この記事ではサブテーブルではなく、20問に固定したバージョンでお届けしてみます!

実際の内容

  Topへ↑

はい。ではアプリの設定画面です。フォームはこんな感じ。

一番左の文字列フィールドは設問の文字列を入力します。
フィールドコードは上から順にquestion_1からquestion_20としています。

真ん中のドロップダウンフィールドは4つの傾向を選びます。
フィールドコードは上から順にtrend_1からtrend_20としています。

右のラジオボタンフィールドはそれぞれの答えを入力する欄です。
フィールドコードは上から順にanswer_1からanswer_20としています。

で、続いてはグラフを表示するカスタマイズビューを設定してみましょう。

こんな感じですね。

続いてはロジックです。
実は、このグラフを作るには以下の二つのJavaScriptファイルを設定するだけ。

上に設定したのは、Chart.jsです。
Cybozu Developer Network
からCDNのページに移動してもらえれば。

そこのChart.jsに書かれているURLをコピーし、上の画面の
から

に貼って保存するだけ! きとみちゃんでもできますよね?
htttps://がダブらないようにだけ気を付けて!

続いてグラフ表示のロジックです

  Topへ↑

では続いてきとみちゃんとグラフ.jsの内容を。
ここからはVisual Studio Codeの画面にコメントを入れています。




ちょっと見にくいので、直に貼ったコードも提供します。右にスクロールしてくださいね。

(function () {
  "use strict";

  // 一覧ページ
  kintone.events.on('app.record.index.show', function(event) {                       //一覧画面表示時の定型文です
    var record = event.records[0];
    var itemcount = 20;
    var 1_Score = 0;
    var 2_Score = 0;
    var 3_Score = 0;
    var 4_Score = 0;
    var selectedScore = 0;
    var dataLabelPlugin = {                                                          //ここは以下の162行目で呼び出されるチャートのプラグインコンフィグで呼び出される部分です。
      afterDatasetsDraw: function (Chart, easing) {                                  //afterDatasetsDrawとはプラグインコアAPIとして呼び出されるChart.js内部のフックです。要は描画後です。
        var ctx = Chart.ctx;                                                         //チャートが描画されている対象のDOM要素です。157行目で定義され、159行目でChartオブジェクトに渡されます。
        Chart.data.datasets.forEach(function (dataset, i) {                          //対象チャートをループしています。データは77行目で一種類で指定していますのでループは一回のみです。               
          var meta = Chart.getDatasetMeta(i);                                        //チャートのメタデータを取得しています。データやラベルも含まれています。
          if (!meta.hidden) {                                                        //チャートのhiddenプロパティがTrueの場合そもそもチャートが描画されません。
            meta.data.forEach(function (element, index) {                            //メタ要素のデータをループします。今回は4種類ですね。
              ctx.fillStyle = 'rgb(0, 0, 0)';                                        //円グラフの中の文字の色です。rgb(0, 0, 0)は黒を表しています。
              var fontSize = 16;                                                    //36-37行目で文字の場所を設定するためのフォントサイズを16pxで設定しています。表示フォントのサイズとは別に。
              ctx.font = "24px \"Helvetica Neue\", Helvetica, Arial, sans-serif";    //これが実際に描画される文字のフォント情報です

              var sum  = function(arr) {                                             //ここでは対象となるデータの合計値を返します。4種類のデータの合計です。
                  return arr.reduce(function(prev, current, i, arr) {
                      return prev+current;
                  });
              };
              var percentString = ((dataset.data[index] / sum(dataset.data))*100).toFixed(1) + "%";  //それぞれのデータの値を全体の合計で割り、パーセントの文字列を構築します。
              var dataString = Chart.data.labels[index];                                             //それぞれのデータのラベルです。79行目で定義した4つの傾向のラベルですね。 
              ctx.textAlign = 'center';
              ctx.textBaseline = 'middle';

              var padding = 5;
              var position = element.tooltipPosition();
              ctx.fillText(dataString, position.x, position.y - (fontSize / 2) - padding);           //30行目で設定したラベルの値を計算した位置に表示します。
              ctx.fillText(percentString, position.x, position.y - (fontSize / 2) - padding + 35);   //29行目で設定した値のパーセントの文字列を計算した位置に表示します。
            });
          }
        });
      }
    };
    for (var i=1 ; i<=itemcount ; i++){                                                              //ここから76行目までは大人の事情でいろいろとあいまいですがお許しを
      switch( record['answer_' + i]['value'] ) {                                                     //要するに20レコードの設問の答えを基に四つの傾向に加算しているのです
        case 'よく当てはまる':
          selectedScore = 係数は内緒よ♪;
          break;
        case '当てはまる':
          selectedScore = 係数は内緒よ♪;
          break;
        case '当てはまらない':
          selectedScore = 係数は内緒よ♪;
          break;
        case 'まったく当てはまらない':
          selectedScore = 係数は内緒よ♪;
          break;
      }
      switch( record['trend_' + i]['value'] ) {
        case '一つ目の傾向':
          1_Score = 1_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;           //要するに20レコードの設問の答えを基に四つの傾向に重みづけしているのです
          break;
        case '二つ目の傾向':
          2_Score = 2_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
        case '三つ目の傾向':
          3_Score = 3_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
        case '四つ目の傾向':
          4_Score = 4_Score + selectedScore + 山藤ゆりさんに教えてもらった魔法の値を加えるの♪;
          break;
      }
    }
    1_Score = ロジック関数は内緒よ♪(1_Score);
    2_Score = ロジック関数は内緒よ♪(2_Score);
    3_Score = ロジック関数は内緒よ♪(3_Score);
    4_Score = ロジック関数は内緒よ♪(4_Score);                                          //さらに四つの傾向に値を秘密ロジックで精緻化しています。この辺も大人の事情が絡んでいます。
    var pieChartData = {                                                             //161行目でChartオブジェクトに渡されるデータとラベルと背景色のホバー色や枠の組み合わせです。四要素です。   
      labels : ["リーダー合理系","アイディア活動系","ヘルプ支援系","クール分析系"],       //ラベルですね。四つの要素に分かれています。
      datasets : [                                                                   //四つの要素のそれぞれの色の指定です。
        {
          backgroundColor: [
            '#ff6384',
            '#36a2eb',
            '#cc65fe',
            '#ffce56'
          ],
          hoverBackgroundColor: [
              "#FF2384",
              "#3662EB",
              "#cc25fe",
              "#FF8E56"
          ],
          hoverBorderColor: [
              "#000000",
              "#000000",
              "#000000",
              "#000000"
          ],
          hoverBorderWidth: [
              2,
              2,
              2,
              2
          ],
          data : [1_Score,2_Score,3_Score,4_Score]                                   //四つの要素の値です。大人の事情で実際の回答から複雑に計算された結果が格納されます。
        }
      ]
    }
    var tooltipkeyword = {                                                           //ここは四つの傾向ごとに176行目で乱数を設定し、任意のキーワードを表示するようにしています。
      type : [
        {
          word : [
            '同業者もあの人を噂している',
            '他の部署でも話題になっている',
            '○○さんしかできない',
            '自分で判断し、動ける人',
            '部署のメンバーに信頼されている',
            'あのひとには任せられる'
          ],
          title : "任せる、難題、未知の分野、他に頼めない、誰にもできない"
        },
        {
          word : [
            '発想がおもしろい!!',
            '一緒にいるだけで楽しい!!',
            'さすがアイデアマン!!',
            'すばらしいサービス精神!!',
            'うちの部署のムードメーカー!!',
            'その自由な発想がうらやましい!!'
          ],
          title : "自由にして、思いっきり、楽しく、面白く、みんなでいっしょ"
        },
        {
          word : [
            'みんなが働きに感謝している',
            '縁の下の力持ち',
            '一緒にいて落ち着く',
            '丁寧で親切で信頼できる',
            '細かいところによく気が付く',
            '相手の気持ちを分かってくれる'
          ],
          title : "感謝、ありがとう、仲良く、話し合い、相手の気持ち"
        },
        {
          word : [
            '詳しく業務を理解している',
            '商品のことをよく知っている',
            'わが社のことになんでも詳しい',
            'うちの課の歩く辞書',
            'あの人に聞けば間違いない',
            'このデータ量は大したもの'
          ],
          title : "情報、正確、正しく分析、予定通り、計画通り"
        }
      ]
    }
    var canvas = document.getElementById('canvas').getContext("2d");                 //Chartが描画されるDOM要素を指定するChart.jsの定型文です。id="canvas"はカスタマイズビューで指定しました。
    canvas.canvas.height = 256;                                                      //描画される領域の高さを指定しています。
    var test_chart = new Chart(canvas, {                                             //ここでChartオブジェクトをインスタンスとして実体化させています。
      type: 'pie',                                                                   //type: 'pie'はグラフの種類ですね。円グラフです。
      data: pieChartData,                                                            //77行目で定義したデータの実態です。
      plugins: [dataLabelPlugin],                                                    //プラグインコンフィグで関数を呼び出すことができます。その関数は13行目をご参照ください。
      options: {                                                                     //ここからはオプション情報です。
        animation: {
          animateRotate: true,
          animateScale: true
        },
        tooltips: {
          titleFontSize: 48,
          bodyFontSize: 36,
          callbacks: {
            label: function (tooltipItem, data){                                     //ここは描画後にマウスカーソルが乗った時の事前に内部でtooltipItemに定義された情報を基に値を返します。
                return pieChartData["datasets"][0]['data'][tooltipItem['index']] + "ポイント"       //77行目で定義されたデータから該当するデータを表示し
                  + "  キーワード → " + tooltipkeyword["type"][tooltipItem['index']]["title"];      //さらにキーワードとして109行目で定義された四つの傾向のタイトルを表示します。
            },
            afterLabel: function (tooltipItem, data){                                //172行目のラベルの後に別の情報を表示させるにはafterLabelツールチップコールバックが呼び出せます。
                  return "「" + tooltipkeyword["type"][tooltipItem['index']]["word"][Math.floor(Math.random()*(6-0)+0)] + "」";
            }                                                                        //さらにテキストとして109行目で定義された四つの傾向の文言のオブジェクトから乱数で選ばれた文言を表示します。
          }
        },
      }
    });
  });
})();

あとはこのJavaScriptファイルを

にのようにアップロードしていただければ。

どうでしょう。kintoneのデータにChart.jsを組み合わせるだけで、
kintoneのデータを分析することができてしまうのです。

Chart.jsにはさまざまなグラフが用意されているので、
kintoneの標準グラフでは表現できないことも可能です。

コーチング用の分析ツールとしても使えてしまうkintoneの奥深さを楽しんでいただけたらきとみちゃんも喜ぶはずです!
よかったら以下にChart.jsの公式サイトのリンクも貼っているのでご参考になさってくださいね。

当エントリの参考にさせていただいたブログ

  Topへ↑

最後になりましたが、このエントリ作成にあたり、以下の2サイトおよび、コーチングについて教えて頂いたお客様からの情報を参考にさせていただきました。ありがとうございました。

 Chart.jsドキュメント翻訳
 Chart.js公式サイト


kintoneのPromiseを説明できるスキル


以前よりお付き合いさせていただいている株式会社アディエム様(https://adiem.jp/)より、
先日ご依頼を受けたたkintone開発案件は、何重にも入れ子になった多重Promise処理が必要でした。

弊社にてコーディングと単体テストを行い、無事納品にこぎつけられたのですが、
アディエム社の技術者様にもコードの説明を行う必要が生じました。

弊社の代表もPromiseの習得にはかなり手を焼いたのですが、そのスキルを習得できたかの判断基準は、その内容を人に説明できるかどうかです。つまり今回、うまく説明できたかどうかは、弊社代表がPromiseを理解できているかのベンチマークにもなりました。

説明を行った結果、アディエム社の技術者様にPromise処理をご理解していただけたようです。追加の処理を実装し、さらにテストまでも行えるまでになったとか。その結果を以下のようなメッセージでいただきましたのでご紹介します。

先日はコードのレクチャーをありがとうござました。
本日、検索部分のエラーハンドリングを追加し、本番環境にリリース致しました。
負荷テストとして4001件のデータを使用して、正常に更新されることも確認しました。

kintone APIのノーマル呼び出しパターン、kintone promise を使ったパターン、promiseでも thenにresolve, rejectを引き渡すパターン、thenとcatchを書くパターンと、かなりケースの整理が できました。また返値の扱いもデバックすることで理解が進みました。
まだうまく関数化して可読性の良いソースを書く自信はありませんが、長井さんソースを参考にさせて 頂きたいと思います。

以上、ご報告まで。

弊社代表は以前より、kintoneのエバンジェリストとしてサイボウズ社より任命されております。
最近はこうしたマンツーマンに近い形で、技術をお伝えする案件も増えつつあります。
その中でこうしたご評価を頂戴したことは、弊社代表にとっても自信になりました。もちろん、スキルの習得に終わりはありません。新たな技術も次々と世の中に生まれています。まだまだ切磋琢磨していかねば。精進します。

今後もアディエム社とはkintone案件のご提案からコーディング・テストまでを協業できる関係を築いていければと思います。
kintoneでのシステム開発のご相談、アディエム社、および弊社にお気軽にお寄せ下さいませ。

また、もし御社の技術者に対し、こうしたマンツーマン形式でのレクチャーをご要望の際は、
ご連絡をください! ご相談に乗らせていただきます。


消えた少年たち<下>


上巻のレビューで本書はSFではないと書いたた。では本書はどういう小説なのか。それは一言では言えない。それほどに本書にはさまざまな要素が複雑に積み重ねられている。しかもそれぞれが深い。あえて言うなら本書はノンジャンルの小説だ。

フレッチャー家の日々が事細かに書かれていることで、本書は1980年代のアメリカを描いた大河小説と読むこともできる。家族の絆が色濃く描かれているから、ハートウォーミングな人情小説と呼ぶこともできる。ゲーム業界やコンピューター業界で自らの信ずる道を進もうと努力するステップの姿に焦点を合わせればビジネス小説として楽しむことだってできる。そして、本書はサスペンス・ミステリー小説と読むこともできる。おそらくどれも正解だ。なぜなら本書はどの要素をも含んでいるから。

サスペンスの要素もそう。上巻の冒頭で犯罪者と思しき男の独白がプロローグとして登場する。その時点で、ほとんどの読者は本書をサスペンス、またはミステリー小説だと受け取ることだろう。その後に描かれるフレッチャー家の日常や家族の絆にどれほどほだされようとも、冒頭に登場する怪しげな男の独白は読者に強烈な印象を残すはず。

そして上巻ではあまり取り上げられなかった子供の連続失踪事件が下巻ではフレッチャー家の話題に上る。その不気味な兆しは、ステップがゲームデザイナーとしての再起の足掛かりをつかもうとする合間に、ディアンヌが隣人のジェニーと交流を結ぶのと並行して、スティーヴィーが学校での生活に苦痛を感じる隙間に、スティーヴィ―が他の人には見えない友人と遊ぶ頻度が高くなるのと時期を合わせ、徐々に見えない霧となって生活に侵食してゆく。

上巻でもそうだが、フレッチャー夫妻には好感が持てる。その奮闘ぶりには感動すら覚える。愛情も交わしつつ、いさかいもする。相手の気持ちを思いやることもあれば、互いが意固地になることもある。そして、家族のために努力をいとわずに仕事をしながら自らの目指す道を信じて進む。フレッチャー夫妻に感じられるのは物語の中の登場人物と思えないリアルさだ。夫妻の会話がとても練り上げられているからこそ、読者は本書に、そしてフレッチャー家に感情移入できる。本書が心温まるストーリーとして成功できている理由もここにあると思う。

私は本書ほど夫婦の会話を徹底的に書いた小説をあまり知らない。会話量が多いだけではない。夫婦のどちらの側の立場にも平等に立っている。フレッチャー夫妻はお互いが考えの基盤を持っている。ディアンヌは神を信じる立場から人はこう生きるべきという考え。ステップは神の教えも敬い、コミュニティにも意義を感じているが、何よりも自らが人生で達成すべき目標が自分自身の中にあることを信じている。そして夫妻に共通しているのは、その生き方を正しいと信じ、それを貫くためには家族が欠かせないとの考えに立っていることだ。

この二つの生き方と考え方はおおかたの日本人になじみの薄いものだ。組織よりも個人を前に据える生き方と、信仰に積極的に携わり神を常に意識しながらの生き方。それは集団の規律を重んじ、宗教を文化や哲学的に受け止めるくせの強い日本人にはピンとこないと思う。少なくとも私にはそうだった。今でこそ組織に属することを潔しとせず個人の生き方を追求しているが、20代の頃の私は組織の中で生きることが当たり前との意識が強かった。

本書の底に流れる人生観は、日本人には違和感を与えることだろう。だからこそ私は本書に対して傑作であることには同意しても、解釈することがなかなかできなかった。多分その思いは日本人の多くに共通すると思う。だからこそ本書は読む価値がある。これが学術的な比較文化論であれば、はなから違う国を取り上げた内容と一歩引いた目線で読み手は読んでいたはず。ところが本書は小説だ。しかも要のコミュニケーションの部分がしっかりと書かれている。ニュースに出るような有名人の演ずるアメリカではなく、一般的な人々が描かれている本書を読み、読者は違和感を感じながらも感情を移入できるのだ。本書から読者が得るものはとても多いはず。

下巻が中盤を過ぎても、本書が何のジャンルに属するのか、おそらく読者には判然としないはずだ。そして著者もおそらく本書のジャンルを特定されることは望んでいないはず。自らがSF作家として認知されているからといって本書をSFの中に区分けされる事は特に嫌がるのではないか。

本書がなぜSFのジャンルに収められているのか。それはSFが未知を読者に提供するジャンルだから。未知とは本書に描かれる文化や人生観が、実感の部分で未知だから。だから本書はSFのジャンルに登録された。私はそう思う。早川文庫はミステリとSFしかなく、著者がSF作家として名高いために、安直に本書をSF文庫に収めたとは思いたくない。

本書の結末は、読者を惑わせ、そして感動させる。著者の仕掛けは周到に周到を重ねている。お見事と言うほかはない。本書は間違いなく傑作だ。このカタルシスだけを取り上げるとするなら、本書をミステリーの分野においてもよいぐらいに。それぐらい、本書から得られるカタルシスは優れたミステリから得られるそれを感じさせた。

本書はSFというジャンルでくくられるには、あまりにもスケールが大きい。だから、もし本書をSFだからと言う理由で読まない方がいればそれは惜しい。ぜひ読んでもらいたいと思える一冊だ。

‘2017/05/19-2017/05/24


消えた少年たち〈上〉


本書は早川SF文庫に収められている。そして著者はSF作家として、特に「エンダーのゲーム」の著者として名が知られている。ここまで条件が整えば本書をSF小説と思いたくもなる。だが、そうではない。

そもそもSFとは何か。一言でいえば「未知」こそがSFの焦点だ。SFに登場するのは登場人物や読者にとって未知の世界、未知の技術、未知の生物。未知の世界に投げこまれた主人公たちがどう考え、どう行動するかがSFの面白さだといってもよい。ところが本書には未知の出来事は登場しない。未知の出来事どころか、フレッチャー家とその周りの人物しか出てこない。

だから著者はフレッチャー家のことをとても丁寧に描く。フレッチャー家は、五人家族だ。家長のステップ、妻のディアンヌ、長男のスティーヴィー、次男のロビー、長女で生まれたばかりのベッツィ。ステップはゲームデザイナーとして生計を立てていたが、手掛けたゲームの売り上げが落ち込む。そして家族を養うために枯葉コンピューターのマニュアル作成の仕事にありつく。そのため、家族総出でノースカロライナに引っ越す。その引っ越しは小学校二年生のスティーヴィーにストレスを与える。スティーヴィーは転校した学校になじめず、他の人には見えない友人を作って遊び始める。ステップも定時勤務になじめず、ゲームデザイナーとしての再起をかける。時代は1980年代初めのアメリカ。

著者はそんな不安定なフレッチャー家の日々を細やかに丁寧に描く。読者は1980年代のアメリカをフレッチャー家の日常からうかがい知ることになる。本書が描く1980年代のアメリカとは、単なる表向きの暮らしや文化で表現できるアメリカではない。本書はよりリアルに、より細やかに1980年代のアメリカを描く。それも平凡な一家を通して。著者はフレッチャー家を通して当時の幸せで強いアメリカを描き出そうと試み、見事それに成功している。私は今までにたくさんの小説を読んできた。本書はその中でも、ずば抜けて異国の生活や文化を活写している。

例えば近所づきあい。フレッチャー家が近隣の住民とどうやって関係を築いて行くのか。その様子を著者は隣人たちとの会話を詳しく、そして適切に切り取る。そして読者に提示する。そこには読者にはわからない設定の飛躍もない。そして、登場人物たちが読者に内緒で話を進めることもない。全ては読者にわかりやすく展開されて行く。なので読者にはその会話が生き生きと感じられる。フレッチャー家と隣人の日々が容易に想像できるのだ。

また学校生活もそう。スティーヴィーがなじめない学校生活と、親に付いて回る学校関連の雑事。それらを丁寧に描くことで、読者にアメリカの学校生活をうまく伝えることに成功し。ている。読者は本書を読み、アメリカの小学校生活とその親が担う雑事が日本のそれと大差ないことを知る。そこから知ることができるのは、人が生きていく上で直面する悩みだ。そこには国や文化の差は関係ない。本書に登場する悩みとは全て自分の身の上に起こり得ることなのだ。読者はそれを実感しながらフレッチャー家の日々に感情を委ね、フレッチャー家の人々の行動に心を揺さぶられる。

さらには宗教をきっちり描いていることも本書の特徴だ。フレッチャー夫妻はモルモン教の敬虔な信者だ。引っ越す前に所属していた協会では役目を持ち、地域活動も行ってきた。ノースカロライナでも、モルモン教会での活動を通して地域に溶け込む。モルモン教の布教活動は日本でもよく見かける。私も自転車に乗った二人組に何度も話しかけられた。ところがモルモン教の信徒の生活となると全く想像がつかない。そもそもおおかたの日本人にとって、定例行事と宗教を結びつけることが難しい。もちろん日本でも宗教は日常に登場する。仏教や神道には慶弔のたびにお世話になる。だが、その程度だ。僧侶や神官でもない限り、毎週毎週、定例の宗教行事に携わる人は少数派だろう。私もそう。ところがフレッチャー夫妻の日常には毎週の教会での活動がきっちりと組み込まれている。そしてそれを本書はきっちりと描いている。先に本書には未知の出来事は出てこないと書いた。だが、この点は違う。日々の中に宗教がどう関わってくるか。それが日本人のわれわれにとっては未知の点だ。そして本書で一番とっつきにくい点でもある。

ところが、そこを理解しないとフレッチャー夫妻の濃密な会話の意味が理解できない。本書はフレッチャー家を通して1980年代のアメリカを描いている。そしてフレッチャー家を切り盛りするのはステップとディアンヌだ。夫妻の考え方と会話こそが本書を押し進める。そして肝として機能する。いうならば、彼らの会話の内容こそが1980年代のアメリカを体現していると言えるのだ。彼らが仲睦まじく、時にはいさかいながら家族を経営していく様子。そして、それが実にリアルに生き生きと描かれているからこそ、読者は本書にのめり込める。

また、本書から感じ取れる1980年代のアメリカとは、ステップのゲームデザイナーとしての望みや、コンピューターのマニュアル製作者としての業務の中からも感じられる。この当時のアメリカのゲームやコンピューター業界が活気にあふれていたことは良く知られている。今でもインターネットがあまねく行き渡り、情報処理に関する言語は英語が支配的だ。それは1980年代のアメリカに遡るとよく理解できる。任天堂やソニーがゲーム業界を席巻する前のアタリがアメリカのゲーム業界を支配していた時代。コモドール64やIBMの時代。IBMがDOS-V機でオープンなパソコンを世に広める時代。本書はその辺りの事情が描かれる。それらの描写が本書にかろうじてSFっぽい味付けをあたえている。

では、本書には娯楽的な要素はないのだろうか。読者の気を惹くような所はないのだろうか。大丈夫、それも用意されている。家族の日々の中に生じるわずかなほころびから。読者はそこに興を持ちつつ、下巻へと進んでいけることだろう。

‘2017/05/13-2017/05/18