Dao実装時にもう一手間
この記事はForce.com Advend Calender 2013 15日目エントリーです。
さて、ほぼ1年間放置してたわけですが、この場所はadvent calendarを書くための場所となりましたw
今年のadvent calendarも質の良い記事が沢山投稿されてますね。でもちょっと難しいな、と感じている初級者の方のために(多分)明日からでも使えるApexのTipsを一つ。(本当はWinter14のパイロット機能を紹介しようとしたら、一つは未実装。二つ目は有効化に間に合わなくてこうなったのは内緒ですorz)
オブジェクトにアクセスするためにはDaoクラスを用意するところは多いと思います。DaoとはData access Object つまりオブジェクトにアクセスすることに特化したクラスというわけです。
例えば取引先のDaoだと以下の感じでしょうか。
- /**
- * 取引先のデータ・アクセスオブジェクトクラスです
- */
- public with sharing class AccountDao {
- /**
- * IDをキーに取引先オブジェクトを取得します
- * @param id 取引先ID
- * @return 取得した取引先オブジェクト
- */
- public static Account selectById(ID id) {
- List<Account> accList = [select Id, Name from Account where id = :id];
- if (accList.size() > 0) {
- return accList.get(0);
- }
- return null;
- }
- }
上記は取引先IDをキーに取引先を取得する例ですね。
[select Id, Name from Account where id = :id]の返り値はSObjectクラスも返してくれるのでreturn [select Id, Name from Account where id = :id];でもよさそうですがなぜそうしないかというと、SObjectクラスで受け取る場合、select結果が0件の場合は例外が発生してしまいます。例外をtry/catchするよりはnullを返したほうが色々と便利なので、1件しか返さない前提の場合はこういう書き方をする事が多いですね。
話を元に戻して、salesforceにはアクセス権があります。この場合はクラスにwith sharingをしているのでユーザ権限ですね。実行者のユーザ権限において参照できる取引先オブジェクトが取得できます。知らない方はフレクトさんが分かりやすくまとめられていますので、URLをご紹介しておきます。
http://blog.flect.co.jp/salesforce/2010/11/with-sharingwit.html
では同じSOQLをシステム管理者権限で実行したい場合はどうすればよいでしょうか? 結論として、このままではどうやってもシステム管理者権限で実行できません。新しくwithout sharingを付けたシステム管理者権限用のDaoクラスを作成し、同じロジックを記述しなくてはならないのです。同じロジックが別のクラスに分散するのはちょっと嫌ですよね? じゃあどうするか。
ここで思い出して欲しいのは、with sharingやwithout sharingを付けないクラスは呼び出し元の設定が適用されるということです。つまり、ロジックの記述されているクラスが指定なしで、呼び出し元がwith sharingならユーザ実行権限で実行されることになります。
以上を踏まえた上でDaoを実装すると以下になります。
AccountDaoクラス
- /**
- * 取引先のデータ・アクセスオブジェクトクラスです
- */
- public class AccountDao {
- /**
- * IDをキーに取引先オブジェクトを取得します
- * @param id 取引先ID
- * @return 取得した取引先オブジェクト
- */
- public static Account selectById(ID id) {
- List<Account> accList = [select Id, Name from Account where id = :id];
- if (accList.size() > 0) {
- return accList.get(0);
- }
- return null;
- }
- }
WS_AccountDaoクラス
- /**
- * 取引先のデータ・アクセスオブジェクトクラスです(ユーザ権限実行)
- */
- public with sharing class WS_AccountDao {
- /**
- * IDをキーに取引先オブジェクトを取得します
- * @param id 取引先ID
- * @return 取得した取引先オブジェクト
- */
- public static Account selectById(ID id) {
- return AccountDao.selectById(id);
- }
- }
WOS_AccountDaoクラス
- /**
- * 取引先のデータ・アクセスオブジェクトクラスです(システム管理者権限実行)
- */
- public without sharing class WOS_AccountDao {
- /**
- * IDをキーに取引先オブジェクトを取得します
- * @param id 取引先ID
- * @return 取得した取引先オブジェクト
- */
- public static Account selectById(ID id) {
- return AccountDao.selectById(id);
- }
- }
どうでしょうか。実装はAccountDaoに任せ、WS_AccountDaoとWOS_AccountDaoはAccountDaoのメソッドを呼び出しているだけです。これでユーザ権限で実行したい場合はWS_AccountDao、システム管理者権限で実行したい場合はWOS_AccountDaoを使えばよいのです。テストはAccountDaoは使わないという取り決めさえあればWS_AccountDaoとWOS_AccountDaoの2つをテストすればよいでしょう。
これでロジックは1つのクラスにまとめたまま、ユーザ権限実行とシステム管理者権限実行ができるようになりました。しかし、その分ユーザ権限実行のみをしたい場合でも2つのクラスが必要になってしまいました。これはちょっと、と考える人もいるでしょう。これを解決する方法はあるのかというと一つあります。
拡張コントローラやカスタムコントローラなどのクラスを自作する場合、force.com IDEから自動生成するとwith sharingで作られますよね? まあ他にもglobalクラスとか例外はありますが、これをわざわざ外して作っているところはほとんど見たことはありません。つまり、コントローラクラスも権限指定がされているのでこれを使えばよいのです。
呼び出し元クラスが必ずwith sharingであるという取り決めさえあれば、WS_AccountDaoは必要ありません。AccountDaoのメソッドをコントローラクラスから呼び出せば、それはユーザ管理権限実行になります。その中でシステム管理者権限で取得したい場合はWOS_AccountDaoを使えばよいのです。これで実装するクラスは増やさずに、実装を1つのクラスにまとめることができました。AccountDaoを使うことになるので、ちゃんとこのクラスのテストも行うことを忘れずに。
ただし一つ注意点があります。AccountDaoは呼び出し元クラスの設定になるので、指定なしクラスやwithout sharingのクラスではシステム管理者権限実行となってしまいます。その場合は素直にWS_AccountDaoを実装しましょう。それしか方法はありません。
いかがだったでしょうか? 今まで自分が関わってきたプロジェクトだとwith sharingでとりあえずDaoを作っておいて、必要になったらwithout sharingのDaoをもう一つ作るという事がほとんどだったのですが、ロジックの分散がいつも気になってどうにかならないかと考えてみた方法がこれです。継承を使えばもっとうまくいけそうな気もするんですが、どうにもうまくいかず結局この方法落ち着きました。もっといい方法があれば教えてください凄い人! まあ、手間の割にそれほどメリットはないかもですが、個人的にはそれなりに気に入っていますw これを読んで少しでも参考になればこれ幸いです。
※追記
まるで自分1人で考えたかのように書いてしまいましたが、事の発端はtwitter上で@i_sanukiさんにwith sharing版とwithout sharing版の2つのDaoを作っていますと言ったら、もっと良い方法があるよとアドバイスを貰ったのがきっかけでした。結構昔の事なのですっかり忘れてました・・・。ここで改めて訂正を。
XXX年後の世界
※この内容はフィクションです。実際の団体及び人物には何の関係もありません。
※稚拙かつ厨二成分が多大に盛り込まれていますので、苦手な方は読まないことをお勧めします。
※この記事はforce.com Advent Calendarに参加していますが、force.comには一切触れていません(書いた後に気付いた……)。ご了承ください。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
そこに一つの都市があった。天を貫くほどの高層ビルや数多のテーマパークは絢爛に輝き、初詣の参拝客さながらに人々がごった返している様は、東京やニューヨークなどといった世界の大都市と比較しても遜色はない。往来を行く人々の人種は様々で、まるで坩堝(るつぼ)のように一貫性が無かった。巨大な都市は遥か地平の彼方まで続き、白夜のように地平線を明るく照らしている。
その光景を遙か上空から見下ろす一人の男がいた。ネクタイも締めずにスーツをだらしなく着崩し、無造作に伸ばした長髪は申し訳程度に後ろで縛られている。体躯はすらっとした長身で、格好から見ようによってはホストのようにも見える。
背後で軽快な電子音が鳴る。男は振り返らずに声をかけた。
「指定時間ジャスト。相変わらずきっちりしてんな、桐島」
「響さん、お久しぶりです。あなたがIACTO(International Anti-CyberTerrorism Organization)を抜けて一年ぶりですね」
響の背後に現れたのは若い女性だった。服装はダークパープルのジャケットにタイトスカート。アップに纏めたブロンドは眩しく、すっきりとした端正な顔立ちも合わせて、人通りの中を歩けばよく人の目に止まるだろう。ある意味、響とは対照的とも言える。
「ここに来るのは誰にも知られてないだろうな?」
「はい」
「上等」
にっと口端を上げ、満足そうに響は頷いた。
桐島はゆっくり歩き、響の横に立つ。
「正直驚きました。IACTOから消えて一年。誰にも消息を掴ませなかったあなたが突然、ただの一部下に過ぎなかった私に連絡を取ってきたんですから」
「もっと自信を持て。俺が知る限り、IACTOの中じゃ一等に優秀だったよ」
響が褒めても、逆に桐島は口を固く結んで、複雑な表情を浮かべていた。響の言った事をお世辞だと思い恐縮しているのだろう。響はばつが悪くなったように自分の頬を掻く。
桐島は気分を切り替えるように大きく息を吐き、きっと響を見つめる。
「……本題に入りましょう。なぜ私をここに呼んだんです?」
「足元に広がっているこいつについて話したくてな」
「ここは……salesforceですね。相変わらず、なんて広大」
眼下に地平の彼方まで広がっている巨大な都市。これはsalesforce全体のシステムをVR(Virtual Reality)システムで視覚化させた姿だった。
今から5年前。鳴り物入りに公開されたVRシステムの実用化を基に、salesforceは大きな改革を先だって行った。salesforceのVRシステムへの対応と個人向けのライセンス販売開始である。
salesforceがあらかじめ用意したシステムを利用して個人でも簡単に様々な商売を開始でき、さらにVRシステムによるサイバースペース内では、まるで現実世界のようなリアリティで店を構えたり、直に商品を触ったり、客とコミュニケーションを取る事が可能だった。
目新しさから次々にユーザを獲得し、さらに世界中の公的機関や大手企業の参入。アミューズメントパークやカジノなどの娯楽施設も次々に出来たことで、利用者数とシステムの規模は急速に膨れ上がっていった。結果、わずか5年で世界最大の仮想商業都市とも言うべき物が出来上がった。
「ここまで巨大だと都市と言うよりも多国籍国家と言った方がしっくりきますね。……まさか?」
桐島の問いに、響は小さく頷いて答える。
「一年ほど前からここに妙なアタックを仕掛けている奴がいるらしくてな。やり方はお粗末でtrustにも載らない些細な事だが、一向に犯人が割れないのがどうにも気にかかる。まるで石壁を叩いて、脆い所を探っているような」
「だったらIACTOに……!」
響は首を横に振る。
「結局はただの俺の勘だ。大体この程度じゃIACTOは動かんし、他の事でもう手一杯だろう。だから俺はIACTOを抜けて独自に調査していた。しかし、俺一人ではちょっと限界が見えてきてな」
「それで、私を?」
響は桐島の方へ向き直り、じっと正面を見据えた。視線は真摯な熱を帯び、気圧されるように桐島は半歩後ろへ下がる。
「さっき言っただろう? お前はIACTOの中でも特に優秀だと。今回の件は結局ただの杞憂に終わるかもしれん。だがもしsalesforce全体がクラックされて乗っ取られる事態にでもなれば、世界はとんでもない大混乱に陥る事になる。それを防ぐために、まずはIACTOが大手を振って動けるような確証が欲しい。桐島、手伝ってくれないか?」
そう言って、響は右手を桐島の前に差し出した。しかし桐島はその手を取ろうとしない。
「……終わったらIACTOに帰ってきてくれますか?」
「む……。それは向こうが許してくれんだろう? ほとんど無断で抜けたようなものだしな……」
「私が何とかします! どんな手を使っても、響さんをIACTOに戻してみせます!」
今度は逆に響が桐島に詰め寄られ、響は数歩後ろに下がる。しばらく響は目を泳がせていたが、やがて覚悟を決めたように後頭部を思い切り掻き毟り、大きく息を吐いた。
「全く、なんでいつの間にかこっちがお願いされる立場になってるんだか……。分かった分かった! 全部終わったら何でもお前の言う事を聞いてやるよ」
「ありがとうございます!」
桐島は目を輝かせ、両手で差し出されていた響の右手を勢い良く掴んだ。一方の響は何ともばつが悪そうに桐島を直視できず、あさっての方向を向いている。
「だから、頼んでるのはこっちだってんだろ……。はあ。まあ何はともあれ、改めて頼むぞ。桐島」
「一年ぶりのコンビ再結成ですね。頑張りましょう!」
二人は改めて眼下に広がる黄金都市を見下ろす。現実には存在しない。しかし確かに世界に存在するこの場所を守るため、二人は決意を新たにするのだった。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
駄文をここまで読んでいただいた方、ありがとうございました。またつまらぬ黒歴史を書いてしまった……。恥ずかしさで悶え死にそうです。前回といい、ほんとにこんなものばっかですみません……。
本当は上記を掌編か短編ぐらいで完結させようかと思っていたのですがとても収まり切らないので、時間も無く起承転結の起だけ書いてみました。昔取った杵柄で久しぶりにラノベっぽく書いてみたのですが、言葉が全然出てこずに苦労しました。語彙が相当落ちてますね。
舞台は数百年後、ネットやsalesforceがどんな事になっているかを妄想して書いたものです。言ってしまえば攻○機動隊とサマー○ォーズを足して100ぐらいで割った感じですかねw 個人的にsalesforceがこんな風になっていたらいいなあという思いで書いてみました。今は一般の人達には馴染みが薄い印象があるので、普通の人達にも使われたら、最終的にこんな事になるんじゃないかなあ、と。
設定や伏線で煽るだけ煽ってますが続きは書きませんw 自分の浅はかな知識ではSF物を書いても薄っぺらな内容になるのは間違い無いですし……。
これにてAdventCalendarの自分の担当は終了。こっちのブログをもっと弄りたいのですが時間が取れず。身動きが取れるようになるまでまだ時間がかかりそうです……。
四捨五入(round)の罠
ブログを立ち上げてグダグダやってる間にForce.com AdventCalendarの順番が回ってきてしまった……。
ということで、しょっぱいネタですがapexで四捨五入にはどうすれば良いのかちょっと書いてみたり。すでに日本語で詳しく解説してあるところがすでに存在してたりしますし常識かもですが、こんなのあったなあと頭の片隅にでも置いてもらえれば幸いです。
例えばjavaでdoubleやfloatを四捨五入する場合、下記のコードが一般的だと思います。
- System.out.println(Math.round(11.4));
- System.out.println(Math.round(11.5));
上記の結果は1が11、2は12が返ってきます。順当な結果ですね。
apexのMathクラスにもroundがあります。これを使って同じ事をしてみましょう。
- System.debug(Math.round(11.4));
- System.debug(Math.round(11.5));
この結果も1が11、2は12が返ってきます。これで四捨五入は完成! と思い込んでしまうと、潜在バグの完成です……。
今度は引数の値を変えてやってみます。
- System.debug(Math.round(10.4));
- System.debug(Math.round(10.5));
1の結果は10。これは問題無いですね。しかし2の結果を見ると、11になってほしいはずが10が返ってきます。日本の四捨五入は基本的に4以下は切り捨て、5以上は切り上げなので、大抵これは期待していた値ではありません。
さて、なぜこんな値になってしまうかというと、デフォルトの丸めモードが両言語で違うからです。
javaの丸めモードはHALF_UPといい、日本で言う四捨五入と同一の動きをします。
対してapexの丸めモードはHALF_EVEN。これは基本的に四捨五入と同じ動きをするのですが、結果をなるべく偶数に寄らせるという特性をもっています。
先ほどの例では、10.5を四捨五入すると11になるのですが、切り上げでは奇数になるので、切り捨てにして偶数の10に寄ってしまったために起こってしまったのです。
さて、では単純に四捨五入するにはどうすればよいかというと、以下のコードでHALF_UPが指定できます。
- Decimal rnd = 10.5;
- System.debug(rnd.setScale(0, RoundingMode.HALF_UP));
上記を実行すると、期待した値の11が返ってきます。
HALF_EVENの厄介なところは、テストデータによって一見正常に動作しているように見えるところです。自分も最初はこれで良いと思い込んでいて、ギリギリになって気付いてヒヤッとした覚えがあります……。四捨五入が必要な部分は金額が多いので、そこが狂ってしまうと大問題になりかねません。HALF_EVENで問題なければ良いのですが。
さて、次はどうするかな。本気でネタがないので小説でも書いてみるかもw