athome-developer’s blog

不動産情報サービスのアットホームの開発者が発信するブログ

バージョン管理システムの比較(という名のポエム)

情報システム部の中嶋です。弊社では以前よりバージョン管理システムVCS)としてGitを採用していますが、Subversion歴が長い*1おっさんとしては、ここで一度ちゃんと比較をしておきたいと思い、4つの論点で比較してみました。(まぁ、控えめに言ってポエムです。)
なお、「チェンジセット記録型」とか「タグ型ブランチ」とかの用語はこの文書のために作った造語なので、あまり気にせずに読み飛ばしていただけると幸いです。

TL;DR

悲観的排他かつ集中リポジトリ 楽観的排他かつ集中リポジトリ 楽観的排他かつ分散リポジトリ
チェンジセット記録型かつコピー型ブランチ VSSなど Subversion、TFVCなど (たぶんない)
スナップショット記録型かつタグ型ブランチ (たぶんない) (たぶんない) Gitなど

「SubversinよりGit」と言う人は、超巨大プロジェクトを扱う機会がなく、マージ先を柔軟に選択できるからGitを選んでいるのではないかと思う。

論点1:悲観的排他と楽観的排他

悲観的排他とは

悲観的排他とは、ファイルの更新をそのファイルの更新権(ロック)を取得してから行うことにより、同時に複数人が同じファイルを更新してしまうことを抑止する方法である。ロックを取得することを「チェックアウト」、更新内容をリポジトリに格納しロックを手放すことを「チェックイン」と呼ぶことが多い。有名なVCSとしては、Visual SourceSafe(VSS)などがある。

楽観的排他とは

楽観的排他とは、「同時に複数人が同じファイルを更新することは稀なので、コンフリクトしたら(自分が修正したファイルを他人も修正してすでにコミットしていたら)その時に考える」という方法である。通常、コンフリクトした場合には、マージを行なってから再度コミットする。一般的に、更新内容をリポジトリに格納することを「コミット」と呼ぶが、リポジトリに格納した更新内容自体をコミットと呼ぶVCSもある。また「チェックアウト」という用語も「作業場の内容を最新に更新する」的な意味で用いられるが、詳細はVCSによって異なる。
近年の楽観的排他を用いたVCSは、コンフリクトが発生した場合にも自動的にマージすることができるようになってきたため、編集者がコンフリクトを解決する場面は少なくなってきている。有名なVCSには、Subversion*2やTeam Foundation Version Control(TFVC)、Gitなどがある。
なお、Subversion*3やTFVCは悲観的排他もサポートしている。

楽観的排他に対する悲観的排他の利点

絶対にコンフリクトが起きないので、マージの必要がない。よって、マージミスは絶対に発生しない。また、Excelファイルなどテキスト形式でないファイルはマージが行えない(もしくは難しい)ため、初めからコンフリクトが起きないようにしておくことは有効な方法である。

悲観的排他に対する楽観的排他の利点

ロックの取得が不要であるため、「他人が更新中であるため、自分が更新できない」という事態が発生しない。また、ロックを取得したまま長期間放置する不心得者への対処が不要である。

論点2:集中と分散

集中リポジトリ型とは

集中リポジトリ型とは、中央にある1つのリポジトリを複数の編集者が共有する方法である。有名なVCSには、VSS、Subversion、TFVCなどがある。

分散リポジトリ型とは

分散リポジトリ型とは、編集者個々がそれぞれリポジトリを所有し、必要に応じてリポジトリ同士を同期させる方法である。一般的には、中央リポジトリを1つ用意し、各編集者はそのリポジトリのコピー(クローン)を手元に作成して使用する。「プッシュ」や「プル」などクローン元のリポジトリと同期するための操作がある。有名なVCSには、Git、Mercurial、BitKeeperなどがある。

分散リポジトリ型に対する集中リポジトリ型の利点

リポジトリをクローンする必要がない。極端に大規模なプロジェクトでなければ問題となることはないが、実際にWindows OSやOfficeなどはリポジトリが巨大であるために分散リポジトリ型への移行が行えずにいた。Microsoftはこの問題を解決するためにGit専用のファイルシステムまで開発することになった*4。また、原理的に悲観的排他は集中リポジトリ型でなければ実現できない*5

集中リポジトリ型に対する分散リポジトリ型の利点

リポジトリは必ず編集者の手元にあるため、オフラインな環境であってもリポジトリにアクセスできる。また、試行錯誤の過程をリポジトリに格納しながら他人には公開しないといったことも行える。
ファイルの更新履歴の表示なども、リポジトリが手元にあるため高速に行うことができる。ただし、ここで表示されるファイルの更新履歴は編集者の手元のリポジトリに記録されているものであるため、必ずしも中央リポジトリと同じとは限らない。

論点3:チェンジセットとスナップショット

チェンジセット記録型とは

チェンジセット記録型とは、コミットの際、リポジトリに現在の状態と直前の状態との変更差分を記録する方法である。リポリトリに格納する操作を「コミットする」、コミットする対象を「チェンジセット」と呼ぶ場合が多い。有名なVCSに、SubversionやTFVCなどがある。
なおVSSも変更差分を記録しているが、同時に行われた複数のファイルに対する変更を1つのものとしては認識せず、ファイル単位で管理している。

スナップショット記録型とは

スナップショット記録型とは、コミットの際、リポジトリに管理対象ファイル全ての現在の状態を記録する方法である。リポジトリに格納する操作も格納する対象も「コミット(する)」と呼ぶ。この方式を採用している代表的なVCSにはGit*6がある。

スナップショット記録型に対するチェンジセット記録型の利点

スナップショット記録型のVCSではファイル名の変更を記録することができない*7。Gitではこの問題を回避するために、「削除されたファイルと追加されたファイルの内容が非常に似ている場合、それはファイル名が変更されたものだ」と判断しUIに表示している。
また、スナップショット記録型は必ず管理対象ファイル全てを同時に扱うため管理対象ファイルの一部だけをチェックアウトすることができないが、チェンジセット記録型では1つのリポジトリのうち必要な部分だけをチェックアウトし作業を行うことができる。このため、チェンジセット記録型はスナップショット記録型よりも大規模プロジェクトに向いていると言える。
さらにスナップショット記録型は各時点でのファイルのスナップショットを保持しているため、チェンジセット記録型よりも格納しているデータ量が多く、特に大きなファイルが存在する場合、リポジトリの肥大化が顕著である。

チェンジセット記録型に対するスナップショット記録型の利点

チェンジセット記録型のVCSでは1つ1つのコミットが変更差分しか持っていないため、1つのコミットだけを独立して操作することはできない。しかしスナップショット記録型のVCSは、そのリポジトリが管理対象としている全てのファイルを1つのコミットの中に保持しているため、1つのコミットだけを独立して操作することができる。これによりコミット順序の変更やコミット単位の調整、分岐元/分岐先以外へのマージが行えるようになっている。

論点4:コピーとタグ

コピー型ブランチとは

コピー型ブランチとは、ブランチの作成を「リポジトリ内でのファイルコピー」として扱い記録する方法である。有名なVCSに、Subversion*8やTFVCなどがある。
なおVSSもコピー型ブランチを採用していると言えるが、ブランチを作成するという行為自体が悲観的排他の利点を損なう行為であるため、利用場面は限られる*9

タグ型ブランチとは

タグ型ブランチとは、ブランチを「コミット指し示すポインタ(ここではタグと表現する)」として扱い記録する方法である。この方式を採用している代表的なVCSには、Git*10がある。

タグ型ブランチに対するコピー型ブランチの利点

コピー型ブランチでは、1つの作業領域に1度に複数のブランチを展開することができる。このため、あるブランチで作業中に他のブランチの作業を行う際、作業中の修正をコミット/退避する必要がない。
また、試験的な実装を行なっている複数ブランチのプログラムを同時に実行するなど、柔軟な対応が行える。これに対し、タグ型ブランチでは1つのブランチしか展開することができない。1つのファイルシステム上に複数のブランチを同時に展開するためには、リポジトリ自体をクローンする必要がある。

コピー型ブランチに対するタグ型ブランチの利点

タグ型ブランチを採用しているVCSにおいて「ブランチの作成」とは、すなわち「タグの作成」であるため、その処理は非常に高速である*11。また、ブランチの内容が作成時に固定化されないため、ブランチの分岐位置を作成後に容易に変更することができる。さらにマージに関しても、マージ先のブランチが指すコミットがマージ元のコミット履歴に含まれる場合はマージ先ブランチが指すコミットを変更するだけで高速かつ確実にマージを行うことができる。


ということで、一通りまとめてみました。後発のシステムほどそれまでのシステムが抱えている問題を解決した良いものになっているわけですが、「人は何かの犠牲なしに何も得ることはできない。」ので多かれ少なかれトレードオフがあったりします。
「じゃぁ、どれを採るか」ということですが、コードレビューをしっかりやるとどうしても変更履歴が無駄に増えてしまうので、ここを変更できるGitが今の所一番使いやすいのかなぁと思っています。


2017年9月8日
公開当初、Mercurialも比較対象に含めておりましたが、内容に誤りがございましたので削除いたしました。

*1:自分は2017年4月にアットホームにJoinしましたが、それまではSubversionやTFVCを使用していました。

*2:http://jtdan.com/vcs/svn/svn-book/svn.basic.vsn-models.html#svn.basic.vsn-models.copy-merge

*3:http://jtdan.com/vcs/svn/svn-book/svn.advanced.locking.html

*4:https://www.infoq.com/jp/news/2017/02/GVFS

*5:Mercurialにはその名もズバリ「Lock Extension」という分散なのに悲観的排他が実現できるエクステンションがあるそうなので、「原理的に〜実現できない」は大げさな表現かもしれません。

*6:https://git-scm.com/book/ja/v1/使い始める-Gitの基本

*7:Subversionも、リポジトリがVer.1.8になるまでは変更が追跡できないことが多かった記憶があります。Ver.1.8になってから、変更の追跡もマージも一気に優秀になりました。

*8:http://jtdan.com/vcs/svn/svn-book/svn.branchmerge.using.html

*9:VSSでも「ファイルの共有」を使用することでバージョン別のブランチを悲観的排他で運用することが可能ですが、マージが前提となるトピックブランチのような運用は想定されていないと思われます。

*10:https://git-scm.com/book/ja/v2/Git-のブランチ機能-ブランチとは

*11:実際にはSubversionにおけるブランチ作成も「このリビジョンからこのブランチを作成した」という情報だけをコミットしているので、そこまでの速度差はないと思われます。

画像システムのリプレイスについて基調講演を行いました

情報システム部の河野です。 入社・開発2年目で、現在はRuby on Rails を使った開発を担当しています。

本日は2017年7月26日(水)に行われました、

ZDNet Japan × TechRepublic Japan & AWS Partner Network 5週間連続セミナーシリーズ データベース編 導入事例から考える–なぜ今、クラウドDBが注目されるのか? の内容について書きたいと思います。

今回このイベントにて、弊社の取り組みについて弊社の鈴木が基調講演をさせて頂きました! 「OracleからAuroraへの移行とオンプレミスとの連携」と題しまして、2016年に行った画像システムのOracle DBのオンプレミス環境からAWSへの移行を行った際の経緯や検討事項、本稼働後の課題についてご紹介しました。

ご存じの方も多いとは思いますが、弊社アットホームは不動産会社と物件を探す方をつなぐ不動産情報サービスを展開しています。 今では物件を探し検討する際、物件の画像がついているのは当たり前とも言える時代です。 弊社の各種サービスでも画像を利用したものは多くあり、その物件画像の配信サービスを支えているシステムは弊社のサービスを支える柱の一つとも言えます。

画像システムの移行と結果

さて、前述にもあります通り画像の需要が増えたことで、画像配信システムのリクエストは1億数千万件/日に上り、キャッシュを利用してもリクエストの件数は増加の傾向にありました。

それにより弊社のシステムには、運用・保守費用の増加や、24時間365日の稼働を行うための安定性確保の課題が浮上してきました。

具体的には

  • Oracleが接続するSANのIO性能の限界
  • ストレージの容量追加やDBサーバのメモリ追加の度に作業が発生(数ヶ月に一度のメンテナンス)
  • ライセンスの上限
  • データ増加に伴いSANの費用が増加(月額課金のサービスを利用)

などです。

そこで2016年7月より、Oracle DBからAmazon Auroraへの移行を行い新システムの運用を開始しました。 弊社がAuroraを採用した理由としては

  • 高可用性
  • 高耐久性
  • 運用の手間が少ない
  • MySQL5.6互換
  • 拡張性

などがあります。

本稼働後の状況

パフォーマンス面では、参照性能はOracle RACとほぼ同等の性能を発揮。大量更新などの際に発生していた、性能劣化が改善しました。

可用性の面では、パラメータチューニングがほとんど不要で、容量追加作業が無くなり、費用面においては、旧システムでかかっていた費用と比較すると半額程度(推定)になりました。

以上のことから、旧システムに比べて随分と楽になった、やってよかった!という結果になりました。

アットホームでは新しい技術を積極的に取り入れるチャレンジをしています。

現在も弊社サービスのリプレイスにて、AWSに移行を伴うプロジェクトも始動中です。

(私もこのプロジェクトに開発メンバーとして参加しています!)

弊社では一緒に開発を行ってくれるエンジニアを募集しています。

興味がある方は下記からエントリお願いします。 ↓↓↓ athome-inc.jp

ASP.NET Coreの環境はASPNETCORE_ENVIRONMENT未設定の場合Productionになる

情報システム部のやまだです。

大分久しぶりに書きますが、これを機にまた書き始めようかな、と。

さて、以前弊社の高野が下記の記事にて

dblog.athome.co.jp

なんでデフォルトがProductionなんだろうか? なにか変な設定が入っているのかな?

と書いていたのですが、その理由も含めてわかったので書き留めておきます。

What happened?

上記にもあるように、アプリケーションの設定をappsettings.jsonに記載してありまして、ステージングや本番など環境ごとにさらに下記のようなファイルを作成していました。

  • ステージング環境(appsettings.staging.json)
  • 本番環境(appsettings.production.json)

appsettingsのあとのファイル名は環境変数のASPNETCORE_ENVIRONMENTの値で決まり、値がstagingならばappsettings.jsonに加えてappsettings.staging.jsonも読み込まれるという、環境変数によって設定を変えるというよくある感じです。RailsだとRAILS_ENV、ExpressだとNODE_ENVですね。

で、ある日、IISで設定したはずのASPNETCORE_ENVIRONMENTの設定が消えていて、気が付いたら本番の設定が適用されていて危機一髪ということがありました。

why?

そもそもIISの設定が無くなったのかはともかくとして、

「そもそもデフォルトってProductionなの?」

「なぜデフォルトがProductionなの?」

ということでちょっと調べて(ググって)みたら見つかりました。

デフォルトってProductionなの?

下記の通りProductionでした。

Hosting/HostingEnvironment.cs at dev · aspnet/Hosting · GitHub

なぜデフォルトだとProductionなの?

TL; DR セキュリティ的によろしくないからProductionにしている

なんでDevelopmentじゃなくてProductionなの?

RailsやExpressはDevelopmentなのに。

ということでさらにググってみたところ、なんかそのものズバリなissueを発見。

Why is hosting environment default value "production"? · Issue #863 · aspnet/Hosting · GitHub

以下、関係ありそうな部分を引用してみます。

blowdart commented on 14 Oct 2016
So, the reason we went with production by default is because both our templates, and in general other people's code add detailed error messages, debug logging, and other things that can cause information disclosure vulnerabilities (at best), or things like auth bypass at worst, depending on what you wrapped in an env.IsDevelopment() check.

By defaulting to production we remove that risk.

In addition launching from inside VS and VS Code sets the environment as development, and best practice generally acknowledges developers should not have every day access to production assets, the risk of defaulting to development is far greater than defaulting to release/production.

 よし、翻訳だ。

ブローダート はコメントしました on 14 Oct 2016
だから、我々がデフォルトでプロダクションを行ったのは、テンプレートと一般的に他の人のコードの両方が、詳細なエラーメッセージ、デバッグログ、および情報漏えいの脆弱性を引き起こす可能性のあるもの、あなたがenv.IsDevelopment()チェックでラップした内容に応じて、最悪です。

デフォルトでは、プロダクションはそのリスクを排除します。

さらに、VSとVSコードの内部からの起動は、開発環境として環境を設定し、開発者が毎回運用資産にアクセスする必要がないことを一般的に認めているため、開発の不履行リスクはリリース/生産のデフォルトよりもはるかに大きくなります。

デバッグ情報とかだだもれしちゃうから安全な方に振っておくよ、ということですね。

ほか、こちらのコメントの方が端的でわかりやすいかもしれません。

DefaultEnvironmentName should be development · Issue #712 · aspnet/Home · GitHub

blowdart commented on 1 Jul 2015
Yes, but your example of web.config is precisely why files don't work. People didn't build in release and publish because it wasn't the default. The amount of debug web sites, with detailed error messages out there is awful. 

ブローダート はコメントしました on 1 Jul 2015
はい。しかし、web.configの例は、ファイルが機能しない理由です。人々はデフォルトではなかったので、リリースでビルドして公開しませんでした。詳細なエラーメッセージが表示されたデバッグWebサイトの量はひどいです。

あー、なるほど。

この辺の方針というか思想というかデザインというかは、言語やフレームワークによって違いそうなので、気を付けたいですね。

ASP.NET Coreのアクセス認可機構

情報システム部の中嶋です。アットホームには2017年4月にJoinし、現在は社内向けの営業支援システムの開発を担当しています。
今日は、ASP.NET Coreが持つアクセス認可の定義とその確認方法について紹介してみたいと思います。


業務システムでは、利用者の役職や役割に応じて利用できる機能、できない機能が存在する場合が一般的です。
ユーザー認証(authentication:いま操作している人がAさんであることを確認する行為)についてはActive Directoryなど外部のシステムに依存することも多いですが、アクセス認可(authorization:Aさんが機能Bを利用しても良いこと)の定義とその確認に関しては各システムが持つ機能に依存するため、それぞれのシステムで用意することが多いと思います。ユーザビリティを考慮するとアクセス認可の確認結果はメニューの表示/非表示で表現されますが*1、URLを直接入力してアクセスされる可能性も考慮するとサーバー上のコントローラークラスにあるアクションメソッドにもアクセス認可を確認する機構が必要となります。
ASP.NET Coreには、この「コントローラークラスのアクションメソッドにおけるアクセス認可の確認」が簡単に行える機構が備わっています。

アクションメソッドの利用を制限する

コントローラークラスのアクションメソッドにおいてアクセス認可の確認を行う方法は非常に簡単で、そのメソッドにAuthorize属性を付けるだけです。メソッドにつけられている場合はそのメソッド、クラスにつけられている場合にはそのクラスに含まれるすべてのメソッドの利用が、許可されたユーザーのみに制限されます。

public class UserController
{
    [Authorize]
    public void ChangePassword(string newPassword)
    {
        // ...
    }
}

例えばパスワード変更のような「許可されたユーザー」が「ユーザー認証されたすべてのユーザー」の場合は引数なしのAuthorize属性使用しますが、「Administratorロールに属しているユーザー」のように特定の条件に制限される場合はAuthorize属性に引数を付けて使用します。

Authorize属性の引数には、ロールを指定するRolesと、ポリシーを指定するPolicyの2つがあります。

ロールを指定して制限する

例えば、Administratorロールに属するユーザーのみが使用可能なアクションメソッドには、Roles引数に「Administrator」というロールの名前を指定します。

public class UserController
{
    [Authorize(Roles = "Administrator")]
    public void GetData(string newPassword)
    {
        // ...
    }
}

こうすることによって、CurrentThreadのCurrentPrincipalがAdministratorロールに含まれている場合のみ、このメソッドを使用できるように制限できます。
またRolesに複数のロールの名前を指定することによって、「いずれかのロールに属しているユーザーが利用できる」という制限を行うこともできます*2

ポリシーを指定して制限する

「ユーザー認証されたユーザーのうち一部のユーザーだけが利用可能だが、その条件はロールではない」という場合には、ポリシーを利用します。
ポリシーを利用する場合には、事前にポリシーを作成したうえで、Authorize属性にPolicy引数を付けて指定します。

public class UserController
{
    [Authorize(Policy = "EmployeeOnly")]
    public void GetData(string newPassword)
    {
        // ...
    }
}

この例では「EmployeeOnly」というポリシーを指定していますが、このポリシーは自分で定義する必要があります。

独自のポリシーを定義する

アクションメソッドの使用を制限する際にロール以外の条件を使用するためには、独自のポリシーを定義する必要があります。
ポリシーはシステム全体に影響する定義ですのでStartupクラスのConfigureServicesメソッドにおいて定義しますが、その方法は判断基準となる属性によって「ロールによって判断するもの」「クレームによって判断するもの」「その他の属性によって判断するもの」の3つに分けられます*3。このうち、「ロールに属していること」と「クレームがアサインされていること」については、簡単にポリシーを定義するために専用のメソッドが用意されています。

ロールを使用してポリシーを定義する

例えばAdministratorロールに属していることを要件とするポリシーは、RequireRoleメソッドを使用して定義し、AddAuthorizationメソッドによってアクセス認可の1つとして登録します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator")));

        // ...
    }
}

クレームを使用してポリシーを定義する

同じようにEmployeeNumberという名前のクレームが存在することを要件とするポリシーは、RequireClaimメソッドを使用して下記のように定義します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber")));

        // ...
    }
}

また、EmployeeNumberというクレームの値が01であることを要件とするポリシーは、下記のように定義します。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("SpecialUserOnly", policy => policy.RequireClaim("EmployeeNumber", "01")));

        // ...
    }
}

その他の方法でポリシーを定義する

ロールやクレーム以外の属性を使用してアクセス認可を行う場合についてですが、この場合は、ポリシーとともに「要件」と「その要件を満たすことを判断するための処理(ハンドラー)」を作成する必要があります。

独自の要件を定義する

例えば、「関東の営業担当であること」と言ったポリシーを作成する場合を考えます。
このような場合は、まず「特定地域の営業担当であること」という要件を作成します。
ここでは「要件」と表現していますが、「アクセス認可の判断基準となる評価軸」という方がより正確かもしれません。「特定地域の営業担当であること」という評価軸を作成しておくことにより、「関東の営業担当であること」だけでなく「北海道の営業担当であること」「九州と沖縄の営業担当であること」なども簡単に作成できるようになります。これは「Administrator」というロール名を指定してポリシーを作成するRequireRoleメソッドと同じような考え方です。

public class SalesAreaRequirement : IAuthorizationRequirement
{
    public string[] SalesAreas { get; protected set; }

    public SalesAreaRequirement(string[] areas)
    {
        this.SalesAreas = areas;
    }
}

次にこの要件を使用したポリシーを作成します。この時、要件の引数に「関東」と設定することで、「関東の営業担当であること」というポリシーになります。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddAuthorization(options => options.AddPolicy("RequireSalesAtKantoArea", policy => policy.Requirements.Add(new SalesAreaRequirement(new string[] { "関東" }))));

        // ...
    }
}

最後に、この要件を実際に確認するハンドラーを作成します。このメソッドの中でcontext.Succeed(requirement)が実行された場合、アクションメソッドに対するアクセスが許可されます。

public class SalesAreaHandler : AuthorizationHandler<SalesAreaRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SalesAreaRequirement requirement)
    {
        // ここでDBなどからログインユーザーの担当地域を取得しておき、salesAreaに格納しておく。

        if (requirement.SalesAreas.Contains(salesArea))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        return Task.CompletedTask;
    }
}

また、このハンドラーはASP.NET Coreのフレームワークによってパイプラインの一部としてインジェクションされるため、その定義も必要となります。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddSingleton<IAuthorizationHandler, SalesAreaHandler>();

        // ...
    }
}

このように、ASP.NET Coreではアクセス認可をポリシーとそのポリシーを満たすための要件として表現することで、容易にわかりやすく定義することができます。
また、ここで作成したポリシーはRazor構文の中でも使用できるため、アクションメソッドのアクセス認可と同じ条件でメニュー等の表示/非表示を制御することもできます。


ということで、今回はASP.NET Coreが持つアクセス認可の定義とその確認方法について紹介してみました。
アクセス認可などは比較的「車輪の再発明」をしがちな分野かと思いますが、用意されているものをうまく利用することで、全体としてより大きな価値が提供できていければ良いなぁと考えています。

*1:Aさんが参照しても良いデータ、参照できないデータの判断もアクセス認可の一部ですが、今回は機能だけに注目して話を進めていますので、メニューでの表現が中心になります。

*2:Rolesに複数のロールを指定すれば「or」ですが、Authorize属性自体を複数つけると「and」になります。

*3:ロールによる判断はポリシーを定義せずともAuthorize属性のRoles引数で可能ですが、「メニューの表示/非表示の制御」のようにRazor構文の中で使用しようとするとポリシーが必要となります。

HoloLensでオブジェクトを回転してみる

こんにちは、情報システム部の高野です。
ちょっと間が空いてしまいましたが
拡大縮小に引き続き、今度は回転です。
今回は、Y軸に対する回転だけやります。
(いまのところそれしか使わなそうなので)

準備

前回の拡大縮小で作成したプロジェクトにそのまま追加していきます。

回転用のハンドルCubeとスクリプトの作成

回転ハンドルを作成する

HierarchyウィンドウのCube内のWireを右クリックし
「3D Obejct」→「Sphere」を選択します。
名称をRotateHandle1に変更します。
サイズと位置を変更します。
Position X=0.5 Y=0 Z=-0.5
Size X/Y/Z=0.05
前回作成したHandle用のマテリアルをアタッチします。

スクリプトを作成する

Projectウィンドウで「C# Script」を作成し
名称をRotateControllerに変更します。
これをRotateHandle1オブジェクトにアタッチします。

RotateController.csをVisualStudioで開き以下の様に修正します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class RotateController : MonoBehaviour, IInputHandler, ISourceStateHandler
{
    private bool isHold;
    private IInputSource currentInputSource;
    private uint currentInputSourceId;
    private Vector3 prevPos;
    private GameObject targetObj;

    // Use this for initialization
    void Start()
    {
        // rootから取れば良かった
        targetObj = transform.root.gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        if (!isHold) return;

        Vector3 handPos;
        currentInputSource.TryGetPosition(currentInputSourceId, out handPos);
        // ワールド空間からローカル空間に変換
        handPos = Camera.main.transform.InverseTransformDirection(handPos);

        var diff = prevPos - handPos;
        prevPos = handPos;

        targetObj.transform.Rotate(0f, diff.x * 360, 0f, Space.World);
    }

    public void OnInputUp(InputEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnInputDown(InputEventData eventData)
    {
        if (!eventData.InputSource.SupportsInputInfo(eventData.SourceId, SupportedInputInfo.Position))
            return;

        if (isHold) return;

        isHold = true;
        InputManager.Instance.PushModalInputHandler(gameObject);

        currentInputSource = eventData.InputSource;
        currentInputSourceId = eventData.SourceId;

        currentInputSource.TryGetPosition(currentInputSourceId, out prevPos);
        prevPos = Camera.main.transform.InverseTransformDirection(prevPos);
    }

    public void OnSourceDetected(SourceStateEventData eventData)
    {
    }

    public void OnSourceLost(SourceStateEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }
}

TransformクラスのInverseTransformDirectionメソッドで
ワールド空間からローカル空間に変換してますが
オブジェクトの正面からだけ操作するのであればこれは必要ありません。
オブジェクトの横や後ろに回って操作すると
これが無いと手の動きと回転が合わなくなります。
下図のような位置関係の場合は、手を横に動かすとZ方向に動かすことになります。
このままだとXの移動距離では無くZの移動距離が必要になります。
f:id:taktak1974:20170620160225p:plain
自分とオブジェクトの位置関係に応じて
Xの移動距離だったりZの移動距離だったりに変更するのは大変なので
カメラのローカル空間に変換して手の横移動は常にXの移動にしました。
これがよくわからずに嵌りました(Unity初心者なので色々、嵌ります)


後は、各辺にハンドルをコピーします。
完成したものがこちらになります。

hololens rotate object


これを作っている間にMRDesignLabs_Unityが公開されて
作る必要なかったなって感じにはなったのですが
勉強にはなったので良かった。
MRDesignLabs_Unityは、HoloToolkitのイベントを使ってないので
やり方は結構違いますけどね。*1


弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
Unityできる人もいるとうれしいな
athome-inc.jp

*1:MRDesignLabs_UnityだとHoloLens以外でも動かせそう

HoloLensでオブジェクトを変倍してみる

こんにちは、情報システム部の高野です。
今回は、HoloLensでオブジェクトの拡大縮小をします。
しかも等倍ではなくXYZ軸自由に拡大縮小できるように作ります。

HoloLensで拡大縮小する方法は、いくつかあります。
・声で指示する方法(以前テレビでやってました)
・HoloLensにプリインストールされているActiongramの様にスライダーのようなもので操作する方法
・下記ブログのようにワイヤーフレームにハンドルを付けて操作する方法
こちらのブログを大分参考にさせていただきました)
blog.d-yama7.com

今回は、変倍ということもあり3番目の方法でやってみます。


準備

まずプロジェクトの準備は、以前と同様です。
HoloToolkitをimportしプロジェクトの設定をします。
HololensCamera・InputManager・DefaultCursorをHierarchyウィンドウに配置します。
f:id:taktak1974:20170606101603p:plain

拡大縮小するオブジェクトとワイヤーフレームの配置

Cubeの配置

まず拡大縮小するオブジェクトを配置します。
これは何でも良いのですが、今回はCubeを配置します。
Hierarchyウィンドウで右クリックし「3D Object」→「Cube」を選択して
Cubeオブジェクトを配置します。
配置されたCubeを選択しInspectorウィンドウで位置と大きさを変更します。
Position Z=1
Scale X/Y/Z=0.2

適当にMaterialを用意しCubeに色を付けておきます。
ワイヤーフレームを緑にするのでそれ以外の色がいいです)

ワイヤーフレームの配置

ワイヤーフレーム用のShaderを作成します。
ProjectウィンドウのAssetsフォルダ上で右クリックし
「Create」→「Shader」→「Standard Surface Shader」を選択します。
作成されたShaderの名前をWireに変更しておきます。
Wireシェーダーを右クリックし「Open C# Project」を選択します。
VisualStudioでWire.shaderを開きこちらを参考に編集します。

次にMaterialを作成します。
ProjectウィンドウのAssetsフォルダ上で右クリックし
「Create」→「Material」を選択します。
Materialの名称をWireに変更します。
Wireマテリアルを選択しInspectorウィンドウの上部にある
Shaderプルダウンから「Custom」→「Wire」を選択します。
LineColorを緑に変更します。
f:id:taktak1974:20170606105725p:plain

HierarchyウィンドウのCubeを右クリックし子オブジェクトとしてCubeを作成します。
名称をWireに変更しておきます。

WireマテリアルをWireオブジェクトに関連づけます。
ここまでで下図のようになります。
f:id:taktak1974:20170606110049p:plain

拡大縮小用のハンドルCubeとスクリプトの作成

リサイズハンドルを作成する

HierarchyウィンドウのWireオブジェクトを右クリックし
「3D Object」→「Cube」を選択します。
名称をResizeHandle1に変更します。
Position X=0.5 Y=0.5 Z=-0.5
Size X/Y/Z=0.05
に変更します。
緑色のマテリアルを作成しResizeHandle1に関連付けます。

これを各頂点にコピーすることになるのですが
動きを付けてからにします。

スクリプトを作成する

Projectウィンドウで「C# Script」を作成し
名称をResizeControllerに変更します。
これをResizeHandle1オブジェクトに関連付けておきます。

ResizeController.csをVisualStudioで開き以下の様に修正します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class ResizeController : MonoBehaviour, IInputHandler, ISourceStateHandler
{
    private GameObject targetObj;
    private bool isHold;
    private IInputSource currentInputSource;
    private uint currentInputSourceId;
    private Vector3 startHandPos;
    private Vector3 axisVect;
    private Vector3 startScale;

    // Use this for initialization
    void Start()
    {
        // 本来は外から設定した方が良いです
        targetObj = transform.parent.parent.gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        if (!isHold) return;

        Vector3 handPos;
        currentInputSource.TryGetPosition(currentInputSourceId, out handPos);

        var diff = handPos - startHandPos;

        // 各軸ごとの方向を取得
        var xVect = axisVect.x >= 0 ? 1 : -1;
        var yVect = axisVect.y >= 0 ? 1 : -1;
        var zVect = axisVect.z >= 0 ? 1 : -1;

        targetObj.transform.localScale = startScale + new Vector3(diff.x * xVect, diff.y * yVect, diff.z * zVect);
    }

    public void OnInputDown(InputEventData eventData)
    {
        if (!eventData.InputSource.SupportsInputInfo(eventData.SourceId, SupportedInputInfo.Position))
            return;

        if (isHold) return;

        isHold = true;
        InputManager.Instance.PushModalInputHandler(gameObject);

        currentInputSource = eventData.InputSource;
        currentInputSourceId = eventData.SourceId;

        currentInputSource.TryGetPosition(currentInputSourceId, out startHandPos);

        startScale = targetObj.transform.localScale;

        axisVect = transform.position - targetObj.transform.position;
    }

    public void OnInputUp(InputEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnSourceDetected(SourceStateEventData eventData)
    {
    }

    public void OnSourceLost(SourceStateEventData eventData)
    {
        if (!isHold) return;

        isHold = false;
        InputManager.Instance.PopModalInputHandler();
    }
}

今回は、以前の移動時と違い手の位置を取れるように
IInputHandler、ISourceStateHandlerを使いました。
こちらの方が簡単でした。

あとは、各頂点にリサイズハンドルをコピーすればOKです。
最終的なHierarchyウィンドウは下図になります。
f:id:taktak1974:20170606141432p:plain

ここまでで拡大縮小が可能です。

対角点を基点にする

上記コードだと中心点が基点になって拡大縮小されますが
対角点を基点にしたいと思います。

以下のようにResizeController.csを変更します。

    void Update()
    {
        if (!isHold) return;

        ・・・

        targetObj.transform.localScale = startScale + new Vector3(diff.x * xVect, diff.y * yVect, diff.z * zVect);

        // 追加
        var scaleDiff = targetObj.transform.localScale - startScale;
        targetObj.transform.position = startPos + new Vector3(scaleDiff.x * xVect, scaleDiff.y * yVect, scaleDiff.z * zVect) / 2;
    }

    public void OnInputDown(InputEventData eventData)
    {
        ・・・

        startScale = targetObj.transform.localScale;
        // 追加
        startPos = targetObj.transform.position;

        axisVect = transform.position - targetObj.transform.position;
    }

拡大縮小しつつ同じ方向に移動しているので基点が対角点に見えます。
(もっと良い方法あるんですかね?)


それで今回作成したものはこんな感じに動きます。

hololens resize object




ワイヤーフレームやハンドルが一緒に拡大縮小しちゃうのが玉に瑕ですね。
課題は残しつつも一応変倍ができるようになりました。
後は回転ができれば基本的なオブジェクトの操作はできるようになりますね


弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
athome-inc.jp

HoloLensでオブジェクトを移動してみる

こんにちは、情報システム部の高野です。
今回は、オブジェクトの移動を試してみます。

前回作ったプロジェクトをそのまま利用します。


HoloLensでオブジェクト移動するために色々と調べてみたところ
いくつかの実装方法があることが分かりました。
今回は、そのうちの2つの方法を試してみます。
1つは、UnityのInteractionManagerのイベントを使う方法
もう1つは、HoloToolkitのIManipulationHandlerを実装する方法です。

InteractionManagerを利用した移動

前回、オブジェクトを生成できるようにしたので
その生成したオブジェクトを移動できるようにしていきます。

移動用のスクリプトを作成する

ProjectウィンドウのScriptsフォルダにGestureControllerというスクリプトを作成します。
作成してGestureControllerHierarchyウィンドウのObjectManager
ドラッグ&ドロップし設定します。

CubeプレハブにTagを設定する

後程、別オブジェクトを作成するので区別できるようにTagを設定しておきます。
ProjectウィンドウのPrefabsフォルダからCubeを選択します。
Inspectorウィンドウの上部にTagプルダウンがあるので「Add Tag」を選択します。
TagsにあるプラスボタンをクリックしTag名をInteractionとし保存します。
もう一度CubeプレハブのInspectorウィンドウを開き
Tagで「Interaction」を選択します。

Tagに設定した状態
f:id:taktak1974:20170522160017p:plain

移動処理を実装する

GestureControllerスクリプトをVisualStudioで開きます。
コードを以下の様に変更します

using HoloToolkit.Unity.InputModule;
using UnityEngine;
using UnityEngine.VR.WSA.Input;

public class GestureController : MonoBehaviour 
{

    private Vector3 prevPos;
    private bool isHold;
    private GameObject focusObj;

    // Use this for initialization
    void Start()
    {
        InteractionManager.SourcePressed += InteractionManager_SourcePressed;
        InteractionManager.SourceReleased += InteractionManager_SourceReleased;
        InteractionManager.SourceLost += InteractionManager_SourceLost;
        InteractionManager.SourceUpdated += InteractionManager_SourceUpdated;
    }

    // Update is called once per frame
    void Update()
    {
        var obj = GazeManager.Instance.HitObject;

        // ホールドしている時は、オブジェクト入れ替えない
        if (obj != null && !isHold)
        {
            // TagがInteractionのものだけを対象とする
            if (obj.tag == "Interaction")
            {
                focusObj = obj;
            }
        }
    }

    private void InteractionManager_SourcePressed(InteractionSourceState state)
    {
        if (focusObj == null) return;

        focusObj.GetComponent<Rigidbody>().useGravity = false;

        Vector3 handPosition;
        if (state.source.kind == InteractionSourceKind.Hand && 
            state.properties.location.TryGetPosition(out handPosition))
        {
            isHold = true;
            prevPos = handPosition;
        }
    }

    private void InteractionManager_SourceReleased(InteractionSourceState state)
    {
        if (focusObj == null) return;

        focusObj.GetComponent<Rigidbody>().useGravity = true;
        isHold = false;
        focusObj = null;
    }

    private void InteractionManager_SourceLost(InteractionSourceState state)
    {
        if (focusObj == null) return;

        focusObj.GetComponent<Rigidbody>().useGravity = true;
        isHold = false;
        focusObj = null;
    }

    private void InteractionManager_SourceUpdated(InteractionSourceState state)
    {
        if (!isHold || focusObj == null) return;

        Vector3 handPosition;
        state.properties.location.TryGetPosition(out handPosition);

        if (state.source.kind == InteractionSourceKind.Hand && 
            state.properties.location.TryGetPosition(out handPosition))
        {
            var moveVector = Vector3.zero;
            moveVector = handPosition - prevPos;

            prevPos = handPosition;

            var handDistance = Vector3.Distance(Camera.main.transform.position, handPosition);
            var objectDistance = Vector3.Distance(Camera.main.transform.position, focusObj.transform.position);

            focusObj.transform.position += (moveVector * (objectDistance / handDistance));
        }
    }
}

InteractionManager_SourcePressedで手の位置を取得しておき
InteractionManager_SourceUpdatedで移動距離を計算して
オブジェクトに新しい位置を割り当てます。
この移動方法は下記の記事を参考にさせていただきました。
qiita.com
もうちょっと複雑な計算をしているものも見かけたのですが
今のところは、この方法で支障がないような気がしています。

IManipulationHandlerを利用した移動

今度はHoloToolkitのIManipulationHandlerインターフェイスを実装する方法です。
HoloToolkitを利用しているので、できればこちらの方法で実装した方が良いかと思います。

新たなCubeプレハブを作成する

元々、用意してあったCubeとは別のオブジェクトを生成して
そちらをIManipulationHandlerで移動するようにしていきます。
前回、作成したようにCubeのプレハブを作成します。
見た目で判別できるように黄色にしておきます。
プレハブ名もYellowCubeにします。
RigidbodyCubeManagerスクリプトも設定しておきます。

YelloCubeを生成できるようにコードを変更する

前回作成したObejectManagerスクリプトのコードを変更します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class ObjectManager : MonoBehaviour, IInputClickHandler
{
    public GameObject obj1;
    public GameObject obj2;
    private bool toggle = true;

    private void Start()
    {
        InputManager.Instance.PushFallbackInputHandler(gameObject);
    }

    public void OnInputClicked(InputClickedEventData eventData)
    {
        var obj = toggle ? obj1 : obj2;
        toggle = !toggle;

        var pos = Camera.main.transform.position;
        var forword = Camera.main.transform.forward;

        Instantiate(obj, pos + forword, new Quaternion());
    }
}

Unityに戻りobj1にCubeプレハブをobj2にYellowCubeプレハブを設定します。

移動処理を実装する

Unityで新しいスクリプトManipulationControllerを作成します。
作成したスクリプトYellowCubeプレハブに設定します。

設定したスクリプトをVisualStudioで開き
下記の様にコードを変更します。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class ManipulationController : MonoBehaviour, IManipulationHandler
{

    Vector3 prevPos;

    public void OnManipulationCanceled(ManipulationEventData eventData)
    {
        GetComponent<Rigidbody>().useGravity = true;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnManipulationCompleted(ManipulationEventData eventData)
    {
        GetComponent<Rigidbody>().useGravity = true;
        InputManager.Instance.PopModalInputHandler();
    }

    public void OnManipulationStarted(ManipulationEventData eventData)
    {
        GetComponent<Rigidbody>().useGravity = false;
        prevPos = eventData.CumulativeDelta;

        // これが無いとオブジェクトにフォーカス時しか操作ができない
        InputManager.Instance.PushModalInputHandler(gameObject);
    }

    public void OnManipulationUpdated(ManipulationEventData eventData)
    {
        Vector3 moveVector = Vector3.zero;
        moveVector = eventData.CumulativeDelta - prevPos;

        prevPos = eventData.CumulativeDelta;

        // 手の位置が取得できないので決め打ちで40cmに
        var handDistance = 0.4f;
        var objectDistance = Vector3.Distance(Camera.main.transform.position, gameObject.transform.position);

        gameObject.transform.position += (moveVector * (objectDistance / handDistance));
    }
}

移動のロジックは、InteractionManagerの方とあまり変わりはありませんが
IManipulationHandlerだと元記事にもありますが手の位置の取得方法が不明です。
私もいろいろと試しましたが今のところ分かっていません。
下記を実行するとfalseが返ってくるので取得できる望みは低いのはないかと思います。

eventData.InputSource.SupportsInputInfo(eventData.SourceId, SupportedInputInfo.Position)

OnManipulationUpdatedメソッドは、InteractionManager.SourceUpdatedイベントと違い
フォーカスが外れるとイベントが飛びません。
それを回避するためにInputManager.Instance.PushModalInputHandlerメソッドに
gameObjectを渡しています。


今回、作成したものはこんな感じで動きます。

HoloLensでオブジェクトを移動する
動画では分かりづらいのですが、
緑のオブジェクト(InteractionManagerを使った方)の方が
手の移動とオブジェクトの移動がしっくりきます。



HoloToolkitを利用してオブジェクトを移動するなら
IManipulationHandlerを実装する方法でやりたいところですが
手の位置の取得方法が分からないのでうーんって感じです。
(やり方分かる方は、こそっと教えてほしいです)
今回は以上になります。次もHoloLensネタになるかと思います。


弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
athome-inc.jp