Appirio's Tech Blog

2011年5月10日火曜日

ApexプログラミングTips: オブジェクトのメタデータを活用する

Apexは、Force.comプラットフォームにおける開発言語のひとつで、主にアプリケーションロジックの記述に使われます。文法上はJavaに似た言語ですが、Force.comプラットフォームを操作する機能が組み込まれており、Force.comデータベースやプレゼンテーション層の技術であるVisualforceとシームレスに連携します。

今回は、ApexによるコーディングにおけるちょっとしたTipsを紹介してみたいと思います。

オブジェクトによるデータ操作

Force.comデータベースを操作するためのコンポーネントに、標準オブジェクトとカスタム・オブジェクト(以降、合わせて"オブジェクト"と呼びます)があります。これらオブジェクトを使って、データベース上のデータ操作を簡単に記述することが可能です。

例えば、標準オブジェクトの取引先オブジェクトのデータ操作には、Account(API参照名)を使いますが、以下のように記述することができます。
  //【リスト1】

  // オブジェクト作成
  Account acc = new Account();
  acc.Name = 'My Account';
  insert acc;
  // 更新
  acc.Phone = '03-3333-3333';
  update acc;
  // 検索
  acc = [SELECT Id, Name, Phone FROM Account WHERE ID = :acc.id limit 1];
  System.debug('Account: ' + acc.Name);
  // 削除
  delete acc;
標準でO/Rマッピング機能が装備されており、構文レベルでオブジェクトの問い合わせや更新をサポートしています。JavaにおけるJDBCプログラミングのような、煩わしい文字列ベースのSQL文の作成や型変換や、面倒なO/Rマッピング設定などを行うことなく、データ操作処理を記述することができます。

オブジェクトの便利な特徴

さて、このオブジェクトですが、Apexで通常のクラスと同じように扱うことができますが、クラスと違った特徴があります。まず、オブジェクトは、sObjectという総称的な型として扱うことができます。
 //【リスト2】

 // sObject型
 sObject sobj = new Account();
 Account acc = (Account)sobj;
このsObjectには、いくつか便利な機能が備わっています。まず、オブジェクトが保持する値を、Mapのように、フィールド名を文字列で指定して値の出し入れをするメソッドが用意されています。

Object get(String fieldName)
- Returns the value for the field specified by fieldName, such as AccountNumber.

Object put(String fieldName, Object value)
- Sets the value for the field specified by fieldName and returns the previous value for the field.

関連するsalesforce.comドキュメントへ

使用例:
 //【リスト3】
 
 //sObject
 SObject sobj = [SELECT ID, Name, Phone FROM Account WHERE Phone != null LIMIT 1];
 // フィールド"Phone"の値を参照
 String phoneValue = (String)sobj.get('Phone');
 // 値を書き換える
 sobj.put('Phone', phoneValue.replace('-', '')); 
 // 更新
 update sobj;                                    
次に、オブジェクトのメタデータ(定義情報)を参照する仕組みが用意されています。

オブジェクトのメタデータには、Describe ResultというAPIを使うことでアクセスできます。Describe Resultには、sObject Describe ResultとDescribe Field Resultがあります。前者はオブジェクトそのものの定義情報、後者はフィールドの定義情報へのアクセスを提供します。

APIの詳細は割愛しますが、以下のApex コードでは、オブジェクトのフィールドの定義情報を参照しています。
    //【リスト4】

    //sObject
    SObject sobj = [SELECT ID FROM Account LIMIT 1]; 
    Map<String, Schema.SObjectField> fmap = sobj.getSObjectType().getDescribe().fields.getMap();
    for(Schema.sObjectField f : fmap.values()) {
        // フィールドの名前と型情報
        Schema.DescribeFieldResult fd = f.getDescribe();
        System.debug('name: '+fd.getName()+', type: '+fd.getType()); 
    }
Describe Resultについての詳細は、ドキュメントを参照してください。(†1)
また、Describeの使用には、ガバナ制限がありますのでご注意ください。(†2)

これらのAPIを使用して、オブジェクトのどのフィールドをどのように操作するかを実行時に決めることができます。ApexクラスにはJavaのリフレクションのような機能は備わっていないためこれができないのですが、オブジェクトでは可能です。

これらの機能は、ある特定のオブジェクトだけを対象としない汎用的なロジックを組むのに、非常に便利です。

汎用的なロジックへの展開

ひとつ利用例を考えてみたいと思います。

Force.comでは、プロダクション環境にコードをデプロイする際に、テストクラスが必要とされます。テストクラスでは、その名の通りApexコードをテストするためのクラスですが、テストの成功とコードカバレッジがプロダクション環境へのデプロイの条件になります。

適切なテストを行うために、事前にデータを必要とする場合も多いと思われます(例えば、マスタデータなど)が、そのようなテストを実行するにあたり、必要なデータがすでにデータベースにあることを前提とすべきではありません。開発環境でのテストは問題なく実行されても、いざ別の環境にデプロイしたときに開発環境では発生しなかったエラーを引き起こす、といったことが起きるからです。Force.comでの開発に限った話しではありませんが、テストコードは可能な限り環境への依存性を排除して作成すべきです。

ひとつの対処方法としては、テスト(テストメソッド)のたびに、テストコードより前のステップで、必要となるデータを作成します。テストメソッドのトランザクションはコミットされませんので、テストメソッド毎にデータ設定が必要になりますが、データベースにゴミを残すこともありません。

冒頭で紹介したように、Apexでは、オブジェクトを使用して手軽にコードからデータを作成することが出来ますが、それでも逐一コードを書くのはなかなか面倒です。 なるべく冗長なコードを無くして手軽に書けるようにして、テストコードを書くモチベーションをキープしたいところです。

そこで、任意のオブジェクトに対して、CSV文字列データからデータベースにレコードを作成するユーティリティクラスを考えてみたいと思います。 単純な登録処理であれば、オブジェクト操作のコードを書かずに、定義だけでデータを作成できるはずです。 開発時にデータベースに登録されているデータをExportしてそのままコピーして使う、であるとか、既存のCSVデータをコピーして使う、といったようなことが出来ればテストクラスの作成も随分ラクになるのではないかと思います。

個々のオブジェクトに対して、このような処理を実装することは容易ですが、メタデータを利用することで汎用的なロジックにできたら良いと考えています。

CSVユーティリティクラスの使用イメージ

例として、以下のようなカスタムオブジェクトがあるとします。
Test__c:
    Name テキスト
    URL1__c URL
    Text1__c テキスト
    Boolean1__c チェックボックス
    Number1__c 数値
    Currency1__c 通貨
    Percent1__c パーセント
    Date1__c 日付
    DateTime1__c 日付時刻

以下のような感じに、フィールドの並び順と、作成するデータを文字列で定義します。

ヘッダ文字列:
    //【リスト5】

    // CSVのヘッダ
    String csvHeader = 'Name,URL1__c,Text1__c,Boolean1__c,Number1__c,Currency1__c,Percent1__c,Date1__c,DateTime1__c';
データ文字列:
    //【リスト6】

    // CSVデータ
    String[] csvData = new String[]{
        'テスト1,http://www.appirio.com,test1,true,300.0,5050,33.3,2011-04-25,2011-04-26 11:57:36',
        'テスト2,http://www.salesforce.com,test2,false,200.0,35,15.3,2011-04-21,2011-05-21 12:25:31'
    };
これを使って、実際にデータを作成します。コードは、以下のような感じです。
 //【リスト7】

 // ユーティリティクラスを作成。引数に、対象とするオブジェクトの型情報(Schema.sObjectType)を渡す。
 // (ネーミングに難がある気がしますが、いいのが思いつかないのでご勘弁 ^^;)
 Utils.ObjectBuilder builder = new Utils.ObjectBuilder(Test__c.sObjectType);
 // ヘッダの定義をセット
 builder.setFieldNames(csvHeader);
 // CSV文字列からデータをデータベースに作成。結果を受け取る。
 List<Test__c> tests = builder.create(csvData);

CSVユーティリティクラスの実装

前述のsObjectのメソッド、put(String fieldName, Object value) を使えば、特定のオブジェクトのフィールド名をハードコードせずに、オブジェクトに値をセットできることは、もうお分かりかと思います。あとは、CSV文字列をカンマで分割して指定されたフィールドに設定するだけですが、幾つか課題があります。

主な課題は、以下の2点です。

(A) オブジェクトのインスタンスの作成(new)
 … sObjectは、new演算子でインスタンスを作成することができません。
(B) 文字列からフィールドの型へのデータ変換
 … CSVから得られるのは文字列ですが、数値型フィールド等へ値を設定するには、型の変換が必要になります。

(A)については、Schema.sObjectTypeを利用します。Schema.sObjectType#newSObject()は、オブジェクトのインスタンスを作成して返します。※1

sObject newSObject()
- Constructs a new sObject of this type.


関連するsalesforce.comドキュメントへ

Schema.sObjectType#newSObject()の使用例:
 //【リスト8】

 Schema.sObjectType sobjType = Test__c.sObjectType;
 sObject sobj = sobjType.newSObject();
 Test__c test = (Test__c)sobj;
(B)については、【リスト4】の例で出てきたSchema.DescribeFieldResult#getType()から型情報(Schema.DisplayType)が得られるので、ここからフィールドの型を判定して適当な変換を行うようにしてみましょう。

Schema.DisplayType getType()
- Returns one of the DisplayType enum values, depending on the type of field.


関連するsalesforce.comドキュメントへ

後で参照できるように、対象とするオブジェクトの型情報(Schema.sObjectType)から、各フィールドの型情報(Schema.DisplayType)を取得して、フィールド名をキーにマップに登録するメソッドを用意します。
    //【リスト9】

    protected virtual void buildFieldTypeMap() {
        if(this.sType==null) //sType: Schema.sObjectType
            return;
        Map<String, Schema.SObjectField> fmap = sType.getDescribe().fields.getMap();
        for(Schema.Sobjectfield f : fmap.values()) {
            Schema.DescribeFieldResult fd = f.getDescribe();
            fieldTypeMap.put(fd.getName(), fd.getType());
        }
    }
フィールドの型を表すSchema.DisplayTypeは、列挙型で以下のような項目が定義されています。
anytype, base64, Boolean, Combobox, Currency, Date, DateTime, Double, Email, EncryptedString, ID, Integer, MultiPicklist, Percent, Phone, Picklist, Reference, String, TextArea, Time, URL

関連するsalesforce.comドキュメントへ

結構たくさんありますが、ブール値、数値、日付、日付時刻を変換して、あとはテキストとしてそのまま扱うことにします。 ユーティリティクラスに、以下のような、型ごとに場合分けした変換メソッドを用意します。
    //【リスト10】

    protected virtual Object conv(String value, Schema.DisplayType fieldType) {
        if(value==null)
            return null;
        
        if(fieldType == Schema.DisplayType.Integer ||
            fieldType == Schema.DisplayType.Double ||
            fieldType == Schema.DisplayType.Percent ||
            fieldType == Schema.DisplayType.Currency ) {
            // Decimal
            return value.trim()!='' ? Decimal.valueOf(value) : null;
        }
        else if(fieldType == Schema.DisplayType.Date) {
            // Date
            return value.trim()!='' ? Date.valueOf(value) : null;
        }
        else if(fieldType == Schema.DisplayType.DateTime) {
            // DateTime
            return value.trim()!='' ? DateTime.valueOf(value) : null;
        }
        else if(fieldType == Schema.DisplayType.Boolean) {
            // Boolean
            return value.trim()!='' ? Boolean.valueOf(value) : null;
        }
        //String
        return value;
    }
その他の部分を含めたコード全体像は【リスト14】に掲載します。

【リスト11】は、今回作成したユーティリティクラスを使って、CSV文字列からテスト用のオブジェクト(Test__c)を作成するコード例です。
    //【リスト11】

    //CSVヘッダ
    String csvHeader = 'Name,URL1__c,Text1__c,Boolean1__c,Number1__c,Currency1__c,Percent1__c,Date1__c,DateTime1__c';
    //CSVデータ
    String[] csvData = new String[]{
        'てすと1,http://www.appirio.com,test1,true,300.0,5050,33.3,2011-04-25,2011-04-26 11:57:36',
        'てすと2,http://www.salesforce.com,test2,false,200.0,35,15.3,2011-04-21,2011-05-21 12:25:31'
    };
    //データ作成
    Utils.ObjectBuilder builder = new Utils.ObjectBuilder(Test__c.sObjectType);
    builder.setFieldNames(csvHeader);
    List<Test__c> tests = builder.create(csvData);
    //結果print
    for(Test__c t : tests) {
        System.debug('ID:'+t.ID+', Name:'+t.Name+', URL1:'+t.URL1__c+', Text1:'+t.Text1__c+', Boolean1:'+t.Boolean1__c+
        ', Number1:'+t.Number1__c+', Currency1:'+t.Currency1__c+', Percent1:'+t.Percent1__c+', Date1:'+t.Date1__c+', DateTime1:'+t.DateTime1__c);
    }
【出力例】
ID:a00G0000007O9HXIA0, Name:てすと1, URL1:http://www.appirio.com, Text1:test1, Boolean1:true, Number1:300.0, Currency1:5050, Percent1:33.3, Date1:2011-04-25 00:00:00, DateTime1:2011-04-26 02:57:36
ID:a00G0000007O9HYIA0, Name:てすと2, URL1:http://www.salesforce.com, Text1:test2, Boolean1:false, Number1:200.0, Currency1:35, Percent1:15.3, Date1:2011-04-21 00:00:00, DateTime1:2011-05-21 03:25:31
※日付時刻型の時間がずれているのは、タイムゾーンの違い(入力:JST、出力:UTC)によるものです。


このユーティリティクラスは、別のオブジェクトに対しても同様に適用できます。

【リスト12】は、標準オブジェクトの取引先(Account)を作成するコード例です。 ユーティリティのコードを変更することなく使用できます。
    //【リスト12】
    
    //フィールド
    String csvHeader = 'Name,Phone,Fax';
    //データ
    String[] csvData = new String[]{
        '取引先a,03-3333-3333,03-3333-3334',
        '取引先b,,03-3333-3335',
        '取引先c'
    };
    //データ作成
    Utils.ObjectBuilder builder = new Utils.ObjectBuilder(Account.sObjectType);
    builder.setFieldNames(csvHeader);
    List<Account> accounts = builder.create(csvData);
    //結果print
    for(Account acc : accounts) {
        System.debug('ID:'+acc.ID+', Name:'+acc.Name+', Phone:'+acc.Phone+', Fax:'+acc.Fax);
    }
【出力例】
ID:001G000000fJNpCIAW, Name:取引先a, Phone:03-3333-3333, Fax:03-3333-3334
ID:001G000000fJNpDIAW, Name:取引先b, Phone:, Fax:03-3333-3335
ID:001G000000fJNpEIAW, Name:取引先c, Phone:null, Fax:null


【リスト13】は、このユーティリティを使ったテストクラスの例です。 テストデータの作成処理をメソッドにまとめてしまって、各テストメソッドの冒頭でそれを呼び出すようにしています。
//【リスト13】

@isTest
private class MyTestExample {

    //CSVヘッダ
    static String csvHeader = 'Name,URL1__c,Text1__c,Boolean1__c,Number1__c,Currency1__c,Percent1__c,Date1__c,DateTime1__c';
    //CSVデータ
    static String[] csvData = new String[]{
        'てすと1,http://www.appirio.com,test1,true,300.0,5050,33.3,2011-04-25,2011-04-26 11:57:36',
        'てすと2,http://www.salesforce.com,test2,false,200.0,35,15.3,2011-04-21,2011-05-21 12:25:31'
    };
    
    static void setupData() {
        //データ作成
        Utils.ObjectBuilder builder = new Utils.ObjectBuilder(Test__c.sObjectType);
        builder.setFieldNames(csvHeader);
        List<Test__c> tests = builder.create(csvData);
        System.assertEquals(csvData.size(), tests.size());
    }
    
    static testMethod void test1() {
        // テストデータ作成
        setupData();
        /*  この後にテストコードを記述 */
    }
    
    static testMethod void test2() {
        // テストデータ作成
        setupData();
        /*  この後にテストコードを記述 */
    }
以上、例を通してオブジェクトの便利な特徴を紹介してみました。

今回、取り上げたものは、Apexからアクセスできるメタデータのほんの一部です。 日本語のリソースが少なく、ドキュメントも少し分かりにくい気がしますが、いろいろと使いみちがあると思いますので、本記事がその取っ掛かりになれば幸いです。

ユーティリティクラスのコード全体:
//【リスト14】

Utils.cls:

public class Utils {

    /**
     * CSV文字列からオブジェクトを作成するユーティリティクラス
     * 使用例:
     * ObjectBuilder builder = new ObjectBuilder(Account.getSObjectType());
     * builder.setFieldNames('Name,Phone,Fax');
     * Account acc = (Account)builder.create('Appirio,xxx-xxxx-xxxx,zzz-zzzz-zzzz');
     */
    public virtual class ObjectBuilder {
        /** sObjectType */
        protected Schema.sObjectType sType;
        /** インスタンスを作成する */
        public virtual SObject newInstance() {
            return sType.newSObject();
        }
        /**
         * コンストラクタ
         * @param sType 対象オブジェクトのsObjectType
         */
        public ObjectBuilder(Schema.sObjectType sType) {
            this.sType = sType;
            buildFieldTypeMap();
        }
        
        /** フィールド名と並び順を記録 */
        public List<String> fields = new List<String>();
        /** フィールド名と型情報(Schema.DisplayType)のマッピング */
        public Map<String, Schema.DisplayType> fieldTypeMap = new Map<String, Schema.DisplayType>();
        
        /**
         * CSVのヘッダを設定
         */
        public void setFieldNames(String headerCSV) {
            if(headerCSV==null || headerCSV=='')
                return;
            List<String> headers = headerCSV.split(',');
            for(String h : headers) {
                fields.add(h.trim());
            }
        }
        
        /**
         * CSV文字列からオブジェクトを作成し、データベースに保存して返す.
         * テストクラス用途なので、upsertを行っています。また、エラー発生時はそのまま例外を送出します。
         * (この部分は、用途に応じて改善の余地があります)
         * @param csv
         * @returns 保存後のオブジェクト
         */
        public SObject create(String csv) {
            if(csv==null)
                return null;
            SObject sobj = toObject(csv);
            upsert sobj;
            return sobj;
        }
        
        /**
         * CSV文字列からオブジェクトを作成し、データベースに保存して返す.
         * テストクラス用途なので、upsertを行っています。また、エラー発生時はそのまま例外を送出します。
         * (この部分は、用途に応じて改善の余地があります)
         * @param csvlist
         * @returns 保存後のオブジェクト
         */
        public List<SObject> create(String[] csvlist) {
            //具体的なオブジェクト名でリストを作成できないため、バッチアップデートできない。
            //できそうな気もするが..
            List<SObject> sobjs = new List<SObject>();
            if(csvlist==null || csvlist.size()==0)
                return sobjs;
            for(String csv: csvlist) {
                SObject sobj = toObject(csv);
                if(sobj!=null) {
                    sobjs.add(sobj);
                    upsert sobj;
                }
            }
            return sobjs;
        }

        /**
         * フィールド名と型情報のマッピングを作る
         */
        protected virtual void buildFieldTypeMap() {
            if(this.sType==null)
                return;
            Map<String, Schema.SObjectField> fmap = sType.getDescribe().fields.getMap();
            for(Schema.Sobjectfield f : fmap.values()) {
                Schema.DescribeFieldResult fd = f.getDescribe();
                fieldTypeMap.put(fd.getName(), fd.getType());
            }
        }
        
        /**
         * オブジェクトのインスタンスを作成し、CSVの値をセットして返す.
         * @param values CSVの値
         * @returns オブジェクトのインスタンス
         */
        protected SObject toObject(String csv) {
            if(csv==null)
                return null;
            return toObject(csv.split(','));
        }
        
        /**
         * オブジェクトのインスタンスを作成し、CSVの値をセットして返す.
         * @param values CSVの値
         * @returns オブジェクトのインスタンス
         */
        protected virtual SObject toObject(List<String> values) {
            if(values==null || values.size()==0)
                return null;
            
            SObject sobj = newInstance();
            if(sobj==null)
                return null;
            
            Integer i=0;
            for(String f : fields) {
                if(i >= values.size())
                    break;
                sobj.put(f, conv(values.get(i), fieldTypeMap.get(f)));
                i++;
            }
            return sobj;
        }
        /**
         * 型情報に合わせて、文字列データを変換して返す.
         * @param value データ
         * @param fieldType 型情報
         * @returns 変換後のデータ
         */
        protected virtual Object conv(String value, Schema.DisplayType fieldType) {
            if(value==null)
                return null;

            if(fieldType == Schema.DisplayType.Integer ||
                fieldType == Schema.DisplayType.Double ||
                fieldType == Schema.DisplayType.Percent ||
                fieldType == Schema.DisplayType.Currency ) {
                // Decimal
                return value.trim()!='' ? Decimal.valueOf(value) : null;
            }
            else if(fieldType == Schema.DisplayType.Date) {
                //Date
                return value.trim()!='' ? Date.valueOf(value) : null;
            }
            else if(fieldType == Schema.DisplayType.DateTime) {
                //DateTime
                return value.trim()!='' ? DateTime.valueOf(value) : null;
            }
            else if(fieldType == Schema.DisplayType.Boolean) {
                //Boolean
                return value.trim()!='' ? Boolean.valueOf(value) : null;
            }
            //String
            return value;
        }
    }
}

※1 sObjectTypeの取得方法について:この例では、具体的なオブジェクトを指定して取得していますが、Schema#getGlobalDescribe()を使用することで、sObjectTypeを検索することも可能です。

†1
「Understanding Apex Describe Information」
「sObject Describe Result Methods」
「Describe Field Result Methods」

†2
「Understanding Execution Governors and Limits」


Posted by Kohata Yoshifumi

0 コメント:

コメントを投稿

 
2006-2011 Appirio Inc. All rights reserved.
アピリオ | リソースセンター(英語) | お問い合わせ先 | 採用情報 | プライバシーポリシー(英語)