[Salesforce] equalsメソッドのOverrideでSObjectの自己結合をエミュレートする

SOQLでは自己結合ができないという制限があります。そのため、SObjectで自己結合を行いたい場合はSOQLだけでは実現できません。 DTOとそのequalsメソッドのOverrideで自己結合をエミュレートする方法について記載しました。
2021.05.13

SOQLとSQLを比較した時に、SOQLでは自己結合ができないという制限があります1
例えば、従業員テーブルが次のようにあるとします。

従業員ID 氏名 年棒
00001 菅 裕行 450
00001 菅 裕行 -100
00002 宮沢 美紀 600
00003 河野 雅 -75
00002 宮沢 美紀 -150
00003 河野 雅 400

このテーブルのDDL例は次の通りです。

create table 従業員テーブル (
  従業員ID varchar(15),
  氏名 varchar(255),
  年棒 decimal(10,2)
);
INSERT INTO 従業員テーブル (従業員ID, 氏名, 年棒) VALUES ('00001', '菅 裕行', 450);
INSERT INTO 従業員テーブル (従業員ID, 氏名, 年棒) VALUES ('00001', '菅 裕行', -100);
INSERT INTO 従業員テーブル (従業員ID, 氏名, 年棒) VALUES ('00002', '宮沢 美紀', 600);
INSERT INTO 従業員テーブル (従業員ID, 氏名, 年棒) VALUES ('00003', '河野 雅', -75);
INSERT INTO 従業員テーブル (従業員ID, 氏名, 年棒) VALUES ('00002', '宮沢 美紀', -150);
INSERT INTO 従業員テーブル (従業員ID, 氏名, 年棒) VALUES ('00003', '河野 雅', 400);

諸般の事情により(?)、従業員テーブルには正の値(年棒)と控除額(税など)が別レコードで記録されています。
この時に各従業員の控除後の年棒を出したい場合、SQLでは次のようにできます。

SELECT
    DISTINCT A.従業員ID,
    A.氏名,
    A.年棒 + B.年棒 AS 年棒(控除後)
FROM
    従業員テーブル A, 従業員テーブル B
WHERE
    A.従業員ID = B.従業員ID
AND
    A.年棒 > 0
AND
    B.年棒 < 0
ORDER BY
    従業員ID ASC

結果は

従業員ID 氏名 年棒(控除後)
00001 菅 裕行 350
00002 宮沢 美紀 450
00003 河野 雅 325

となります。

これを、従業員というカスタムオブジェクト(SObject型のEmployee__c)で行いたい場合、先述の通りSOQLで自己結合は使えませんので工夫が必要です。 具体的には、自己結合のAとBにあたるレコードセットを表現するDTOを定義し、そのDTOのequalsメソッドにて結合条件を設定します。

DTOの定義

DTOを次のように定義します。

global class EmployeeDto implements Comparable {
    // 従業員ID
    public String employeeId {get;set;}

    // 氏名
    public String Name {get;set;}

    // 年棒
    public Decimal annualSalary {get;set;}

    global EmployeeDto(String employeeId, String Name, Decimal annualSalary) {
        this.employeeId = employeeId;
        this.Name = Name;
        this.annualSalary = annualSalary;
    }

    // ソート(従業員ID↑)
    global Integer compareTo(Object obj) {
        EmployeeDto dto = (EmployeeDto) obj;

        // ソートキーは従業員ID
        if ( this.employeeId != null && dto.employeeId != null ) {
            Integer ret = this.employeeId.compareTo(dto.employeeId);
            if ( ret != 0 ) {
                return ret;
            }
        }
        return 0;
    }

    public Boolean equals(Object obj) {
        if ( obj != null && obj instanceOf EmployeeDto ){
            EmployeeDto dto = (EmployeeDto) obj;
            if (
              (
                ( employeeId != null && dto.employeeId != null ) &&
                ( employeeId == dto.employeeId )
              )
            ) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    public Integer hashCode() {
        Integer result = 17;
        final Integer prime = 31;
        result = prime * result + System.hashCode(employeeId);
        return result;
    }
}

compareTohashCode メソッドに関しては説明を省略します2
equals メソッドを Override することでこのDTO(EmployeeDto)同士の一致条件(一意性)をカスタマイズすることができます

    public Boolean equals(Object obj) {
        if ( obj != null && obj instanceOf EmployeeDto ){
            EmployeeDto dto = (EmployeeDto) obj;
            if (
              (
                ( employeeId != null && dto.employeeId != null ) &&
                ( employeeId == dto.employeeId )
              )
            ) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

if文で employeeId が等しい時に x.equals(y) (※ x、yは共にEmployeeDtoオブジェクト)がTrueになるように実装しています。
これによって先のSQL例の A.従業員ID = B.従業員ID を表現しています。

自己結合のエミュレート

DTOの準備ができたら、次のようなApexコードで自己結合をエミュレートできます。

// 正の年棒を持つEmployee__cを保持するList
List<Employee__c> plusAnnualSalaryEmployees = [SELECT employeeId__c, Name, annualSalary__c FROM Employee__c WHERE annualSalary__c > 0];
// 負の年棒を持つEmployee__cを保持するList
List<Employee__c> minusAnnualSalaryEmployees = [SELECT employeeId__c, Name, annualSalary__c FROM Employee__c WHERE annualSalary__c < 0];

/* DTOのListを作る */
List<EmployeeDto> plusDtos = new List<EmployeeDto>();
for ( Employee__c employee : plusAnnualSalaryEmployees ) {
    EmployeeDto plusDto = new EmployeeDto(employee.employeeId__c, employee.Name, employee.annualSalary__c);
    plusDtos.add(plusDto);
}
List<EmployeeDto> minusDtos = new List<EmployeeDto>();
for ( Employee__c employee : minusAnnualSalaryEmployees ) {
    EmployeeDto minusDto = new EmployeeDto(employee.employeeId__c, employee.Name, employee.annualSalary__c);
    minusDtos.add(minusDto);
}

/* 結果生成 */
for ( EmployeeDto plusDto : plusDtos ) {
    for ( EmployeeDto minusDto : minusDtos ) {
        if ( plusDto.equals(minusDto) ) {
            // 一致すると判断されたら各項目を出力する
            System.debug('従業員ID: ' + plusDto.employeeId);
            System.debug('氏名: ' + plusDto.Name);
            System.debug('年棒(控除後): ' + (plusDto.annualSalary + minusDto.annualSalary));
        }
    }
}

もし、自己結合の結合条件を変えたければ、EmployeeDtoのequalsメソッドを改修すればOKです。

まとめ

SObjectで自己結合したい場合に、DTOを定義しequalsメソッドをOverrideすることでエミュレートできることをみてきました。
思いのほか簡単に実現できることがお分かり頂けたかと思います。
自己結合しなくて済むようなテーブル(オブジェクト)設計ができれば一番良いのですが、様々な理由によりそうもいかない場合もあるかと思います。
SObjectで自己結合したい!という状況に直面したらこの方法を参考にしていただければと思います。


  1. 他にもたくさん制限があります。 
  2. compareToは Comparable インターフェース、hashCodeは Javaを陰から支えるhashCode、その仕組みと実装方法を基礎から紹介 が参考になります。