APIリクエスト数を消費せずにVisualforce Pageから安全にSOQLクエリを実行する

  • 24
    Like
  • 0
    Comment
More than 1 year has passed since last update.

Salesforceにおけるデータクエリ方法

Salesforceに保存されているデータを検索(クエリ)するには、SOQLというクエリ言語を用いてデータ取得方法を記述します。外部からの連携用にREST/SOAPでのAPIアクセスが開放されており、SOQL文をAPIリクエストに乗せて送信することで任意のSalesforce内のオブジェクトに格納されたレコード情報を取得することができます。

JavaScriptで作成されたHTML5/SPA(Single Page Application)からデータにアクセスする際にもAPIは利用できます。ただしブラウザJavaScriptにはSame Origin Policyが適用されますので、もしSalesforce以外のドメインにホストされているWebアプリからリクエストする場合は適切にCORS設定を施しておく必要がありますが、Visualforceからのアクセスの場合には同一ドメイン内にAPIエンドポイントが存在しますので、CORS設定は必要ありません。

API実行に関する制約と迂回策

さて、そのSalesforceのAPIについてですが、開発にあたり念頭に入れておくべき制約があります。それは、Salesforceのインスタンス(組織)ごとに一日に実行できるAPIリクエスト数が決まっている、ということです。ざっくり言うとユーザライセンス数に比例して上限が決まっています。そのため、あまりAPIに頻繁にアクセスするWebアプリケーションを作成すると、場合によっては枯渇してしまいます。APIリクエスト数は他のアプリや連携プログラムでも共有して消費する資源ですので、他のシステムに影響を与える可能性は最小限になるようにしたいものです。

JavaScript Remoting

ただ、もしあなたがVisualforce Page上でSPAを作成しようとしている場合には、実は抜け道があります。それが JavaScript Remoting です。

JavaScript Remotingとは、サーバ側にApexクラスとしてstaticメソッドをあらかじめ作成して公開しておくことにより、Visualforceから該当のメソッドをJavaScriptの非同期関数コールとして呼び出せるようになる、というものです。これにより、Salesforceのデータを検索して取得することができますが、SOQLをAPI経由で利用するのとは違い、クエリの内容はApex(サーバ)側で決めておくことになります。

サーバ(Apex)
public class RemoteController {    
    @RemoteAction
    public static List<Account> searchAccount(String name) {
        name = name+'%';
        return [SELECT Id, Name, Type FROM Account WHERE Name LIKE :name];
    }
}
クライアント(JavaScript)
RemoteController.searchAccount('ACME', function(accounts, event) {
    if (event.status) {
        console.log(accounts);
    } else if (event.type === 'exception') {
        console.error(event.message, event.where); 
    )
});

RemoteTK

このJavaScript Remotingの仕組みを利用して、Salesforce の Pat Pattersonが習作的に公開したライブラリとして RemoteTK というものがありました。

これは、ざっくり言うと、SOQL文をそのままパラメータとして受け取れるようにしたJavaScript Remoting用のメソッドを作成しておいて、それをそのままクエリ実行関数に渡してして結果を取得するという、かなり強引な技です。

@remoteAction       
public static String query(String soql) {       
    List<sObject> records;      
    try {       
        records = Database.query(soql);     
    } catch (QueryException qe) {       
        return makeError(qe.getMessage(), 'INVALID_QUERY');     
    }       

    Map<String, Object> result = new Map<String, Object>();     
    result.put('records', records);     
    result.put('totalSize', records.size());        
    result.put('done', true);       

    return JSON.serialize(result);      
}

あとで述べるように、これはやはり問題を抱えていました。以前はForce.com JavaScript Toolkit の中にコードが公開されていましたが、現在は削除されています。

Visualforce Remote Object

もうひとつ、最近できた迂回方法があります。Winter'15 でGAとなったVisualforce Remote Objectsです。こちらも同様にAPIリクエスト数の消費を行わずに、Visualforceから非同期にデータを取得できます。

   <apex:remoteObjects >
        <apex:remoteObjectModel name="Warehouse__c" jsShorthand="Warehouse" 
            fields="Name,Id">
            <apex:remoteObjectField name="Phone__c" jsShorthand="Phone"/>
        </apex:remoteObjectModel>
    </apex:remoteObjects>

    <!-- JavaScript to make Remote Objects calls -->
    <script>
        var fetchWarehouses = function(){
            // Create a new Remote Object
            var wh = new SObjectModel.Warehouse();

            // Use the Remote Object to query for 10 warehouse records
            wh.retrieve({ limit: 10 }, function(err, records, event){
                if(err) {
                    alert(err.message);
                }
                else {
                    var ul = document.getElementById("warehousesList");
                    records.forEach(function(record) {
                        // Build the text for a warehouse line item
                        var whText = record.get("Name");
                        whText += " -- ";
                        whText += record.get("Phone");

                        // Add the line item to the warehouses list
                        var li = document.createElement("li");
                        li.appendChild(document.createTextNode(whText));
                        ul.appendChild(li);
                    });
                }
            });
        };
    </script>

それぞれの問題点

以上の方法は、残念ながらAPIによるSOQLの実行を完全に代替できるところまでは行きません。以下に理由を述べます。

1. JavaScript Remotingでは自由にクエリを記述できない

通常、JavaScript Remoting を利用する場合には、クエリの構築は常にサーバサイドになります。クライアント側でSOQLのように自由に指定することは難しいです。パラメータを受け取ったり、動的にSOQLを構築することももちろん可能ですが、限界があります。もちろん、あらかじめデータアクセスのパターンが決まっている場合にはそれでも問題ないでしょう。

2. RemoteTK はセキュアでない

Salesforce API経由でSOQLを実行した場合、データアクセスは常にAPIアクセス承認されたユーザの権限での実行となります。つまり、共有ルールやオブジェクト/項目へのアクセス権限の設定でアクセスが許可されていないデータにはアクセスできないようになっており、安全といえます。言い換えれば、どんなに自由にクエリを指定したとしても、取得できるデータはWebブラウザからログインして見られる情報と同じであることが保証されている、ということです。

しかしApex上でクエリ実行する場合は、システム権限での動作が基本となります。レコードレベルのアクセス制御については、Apexクラスの定義の際にwith sharingキーワードを指定することで、クラス内で実行されるメソッドから共有されていないレコードにはアクセスできないように保証することが可能ですが、オブジェクトや項目へのアクセスの制御(Field Level Security = FLS)については、例え実行ユーザに対してアクセス不可能と設定されていてもすべてクエリ可能となってしまいます。

RemoteTKではクライアントから受け取ったSOQLをそのままクエリ実行に渡してしまっていたので、簡単に権限昇格が発生してしまうのが致命的でした(このため現在は公開が停止されています)。

3. Visualforce Remote Objects でもまだ自由度が足りない

Visualforce Remote Objectsでおこなうクエリは自動的にアクセスしているユーザの権限下での実行になるため、上記で指摘した権限昇格の問題は解消されています。クエリにはフィルタやソート条件などもJavaScript側で指定できるため、柔軟性は高そうです。しかし、Remote Objectsでもまだ自由度に制限があります。

まず、Visualforce Page上にあらかじめタグを作成し、取得するオブジェクト/項目を宣言しておかなければならない、というところです。アプリケーションの種類によっては、取得するオブジェクト・項目自体もJavaScriptプログラム上で動的に決定したいという場合は多いでしょう。API経由でSOQLを利用する場合はこれが可能です。

さらに、項目として指定できるものは検索対象のオブジェクトに存在しているもののみ、という制約があります。つまり、親であれ子であれ、リレーションをたどったクエリについてはサポートされていません。

Salesforceデータクエリのための理想的な要件

一旦ここで、Visualforce Page上でSalesforceのデータを閲覧するSPAを開発するにあたり、私が理想的と考えているデータクエリ方式の要件をもう一度提示します。

  1. 動的なクエリ作成をクライアント(JavaScript)側に完全に委ねることができる自由度がある
  2. Salesforceで設定されたアクセス権限を順守している
  3. APIリクエスト数を消費しない

RemoteTKは1.および3.を実現していましたが、2.を実現することは本質的に無理がありました。FLSをサポートするにはパラメータで渡ってくるSOQLを解析(パース)してあげなければならず、Apex上でそれを行うのは単に難しいだけでなく不毛なものです。Remote Objectsはよい線まで行きましたが、求めるものには少し隔たりがあります。

ただ、Remote Objectsが実際にはJavaScript Remotingと同等のことを実現していると考えると、自前である程度同じことは実装できるのではないでしょうか。

解決策

RemoteTKの問題は、クエリ情報がパースされていないため、FLSのチェックすることができないことでした。これがもしパースされた状態で渡ってくるのであれば話は別です。Apexには既存のオブジェクトや項目情報をDescribeして、メタデータおよびアクセス権限情報を取得する機能があります。これを利用して、クエリ対象オブジェクトおよび項目に対して全てチェックすればOKでしょう。

RemoteTKとは異なり、SOQL文ではなく今度はJSONでクエリを記述するようにします。この辺りはRemote Objectsと若干似ていますが、違うのはフィルタやソート条件だけでなく、取得する項目や対象のオブジェクトもJSONオブジェクトで指定することです。

クライアント(JavaScript)
// JSON形式でクエリを定義
var queryConfig = {
  "fields": [ "Id", "Name", "Account.Name" ],
  "table": "Contact",
  "condition": {
    "operator": "AND",
    "conditions": [{
       "field": "CloseDate",
       "operator": "=",
       "value": { "type": "date", "value": "THIS_MONTH" }
    }, {
       "field": "Amount",
       "operator": ">",
       "value": 20000
    }]
  },
  "sortInfo": [{
    "field": "CloseDate",
    "direction": "ASC"
  }],
  "limit": 10000
};
// JavaScript Remotingを介して実行。JSONはシリアライズする
MyRemoteController.query(JSON.stringify(queryConfig), function(records, event) {
  if (event.status) {
    console.log(records);
  } else {
    console.error(event.message + ': ' + event.where);
  }
});
サーバ(Apex)
public with sharing class MyRemoteController {    
    @RemoteAction
    public static List<SObject> query(String queryJSON) {
        Map<String, Object> qconfig = (Map<String, Object>) JSON.deserializeUntyped(queryJSON);
        Query query = new Query(qconfig);
        query.validate(); // ここでFLSなどアクセス権限のチェック
        return Database.query(query.toSOQL());
    }
}

RemoteTKと同様にクエリ自体はApexでの実行になるので、Apexでの制限がそのまま適用されます(レコード取得数など)。なお、Remote Objectsの場合は、開発者ガイド上では一度に100件が上限値で、OFFSET値は無限に指定できる、となっています。

ソースコード

上記で用いているApexクラスは、GitHub上に公開しています。

https://github.com/stomita/soql-secure

また、こちらにForce.com Site上で動いているデモもあります。

https://soql-remoting-demo-developer-edition.ap2.force.com/

なおSOQLを動的に生成するという意味では、Apex Commonsで提供されているSOQL Builderというものもありますが、soql-secureはこちらとは異なり、JSON形式のクエリ定義からFLSチェック済みのSOQLを生成するものです。

まとめおよび私見

今回開発したsoql-secureはまだ実証実験段階であり、容易にプロダクションに用いるべきではないかもしれません。今後の方向性として考えているのは、私が別途携わっているJSforceでのクエリ実行について、soql-secureを利用できる環境であればAPIをバイパスしてデータクエリを透過的に利用できるような仕組みについて構想しています。

今回評価した段階でのRemote ObjectsについてはまだGAとなったばかりであり、いわば発展途上ですので、いずれクエリの記述自由度もアップして、API経由でのSOQLクエリと同等のことが実現できるかもしれません。

ただ、もしそうなったとして、ではなぜ同等の機能をAPIでのアクセスではリクエスト数で制限するようにしているのか、よくわからなくなってきます。

海外のフォーラムやブログなどを見ていると、Remote Objectsバンザイみたいな感想を表明しているエンジニアもいるのですが、上記の理由により、私自身はあまり手放しで喜べない機能です。もし制限を意識することなくAPIを利用できるのであれば、このような機能自体が必要なかったはずです。あと、細かい点ですが、勝手にRemoting用のオブジェクトをグローバルに定義するJavaScriptコードをinjectしたりといったおせっかいにも正直違和感を感じます。

Salesforceが向かうべき正しい道は、APIリクエスト制限の撤廃、あるいは大幅な緩和であるはずです。API Firstはどこにいったのでしょうか。あれはスローガンだけなのでしょうか。