SObjectModel

Salesforce sObject class library

install

Rubygem

the easiest way

$ gem install sobjectmodel
Bundler

in Gemfile:

gem 'sobjectmodel'

then,

$ bundle install

Generate sObject classes

require 'sobject_model'

# connect with Salesforce using REST API
SObjectModel.connect(:rest, instance_url: url, access_token: token, api_version: 62.0)

# generate Account, Contact and User sObject class
SObjectModel.generate :Account, :Contact, :User

# identify all generated classes
SObjectModel.generated_classes #=> [Account, Contact, User]

What is instance url?

In short, it’s Salesforce host url. You can check it by Salesforce CLI:

$ sf org display
KEY              VALUE
--------------------------------------------------------------
Access Token     <the org's access token>
Api Version      62.0
Connected Status Connected
Instance Url     https://hoge-abc-ed.example.my.salesforce.com

For details, see the command reference.

Main features

Initialize and save a record

c = Contact.new(:Name => "John Smith")
c.Name # "John Smith"
c.save

Find and update a record

c2 = Contact.find(c.id)                  # find by record ID
c2 = Contact.find_by(Name: "John Smith") # find by Name

c2.Name = "Johnny Smith"
c2.save # update

Delete a record

c2 = Contact.find(c.id)
c2.delete

Query and Get a record

contact = Contact.select(:Id, :Name).where(Name: 'Akin Kristen').take
contact # => #<Contact: @Id="0035j00001RW3xbAAD", @Name="Akin Kristen">
contact.Name # Akin Kristen

Contact.select(:Name).where(Name: 'John Smith', LastModifiedDate: :Yesterday).take
Contact.select(:Name).where(Name: 'John Smith').where(LastModifiedDate: :Yesterday).take # same as above

Query and Get records

contacts = Contact.where(LastModifiedDate: :Yesterday).all     # get all contacts who are modified yesterday
accounts = Account.where.not(LastModifiedDate: :Yesterday).all # get all accounts that are *not* modified yesterday

Aggregate functions

Account.where(BillingCountry: 'Japan').count      # count accounts whose billing country is Japan
User.where(country: 'USA').max(:LastModifiedDate) # get the latest LastModifiedDate of users in USA
Case.min(:LastCreatedDate)                        # get the date when the oldest case was created

Child-Parent Relationship

contact = Contact.select(:Id, :Name, "Account.Name").where(Name: 'Akin Kristen').take
contact # <Contact: @Id="0035j00001RW3xbAAD", @Name="Akin Kristen", @Account= #<Account @Name="Aethna Home Products">>
contact.Account.Name # Aethna Home Products

Parent-Children Relationship

 = Account.select(:Id, :Name, "(SELECT Name FROM Contacts)").take
 # <Account @Contacts=[#<Contact @Name="Akin Kristen">], @Id="0015j00001dsDuhAAE", @Name="Aethna Home Products">
.Name                # Aethna Home Products
.Contacts            # [#<Contact @Name="Akin Kristen">]
.Contacts.first.Name # Akin Kristen

Time keywords such as ‘yesterday’ and ‘LAST_N_DAYS:N’ with symbol style

Contact.select(:Name).where(LastModifiedDate: :Yesterday).take       # "SELECT Id, Name FROM Contact WHERE LastModifiedDate = Yesterday" LIMIT 1
Contact.select(:Name).where(LastModifiedDate: :"LAST_N_DAYS:5").take # "SELECT Id, Name FROM Contact WHERE LastModifiedDate = LAST_N_DAYS:5" LIMIT 1

Array for ‘IN’ keyword

Contact.select(:Name).where(Name: ['Jonny Good', 'John Smith']).all # same as "SELECT Name FROM Contact WHERE Name IN ('Jonny Good', 'John Smith')"

Using partial soql directly

Contact.select("Id, Name").where("LastModifiedDate = LAST_N_DAYS:5").all

Ternary style

Contact.select(:Id, :Name).where(:LastModifiedDate, :>=, :"LAST_N_DAYS:5").all # SELECT Id, Name FROM Contact WHERE LastModifiedDate >= LAST_N_DAYS:5
Account.select(:Id, :Name).where(:Name, :LIKE, "%OIL%").all                    # SELECT Id, Name FROM Account WHERE Name LIKE '%OIL%'

Get schema

schema = Account.describe
schema.name                   # Account
schema.field_names            # [Id, Name, ....]
schema.fields.name_and_labels # returns all field name and label pairs

Sometimes it’s better to use raw SOQL when it gets complecated.

accounts =
  SObjectModel.connection.exec_query(
    "SELECT Id, Name FROM Account WHERE Name like '%Hoge' OR (Age <= 30 AND BillingCity IN ['Tokyo', 'NewYork'])",
    model_class: Account
  )

Known Issues

exec_query

SELECT clause missing Id causes model class to force insert a record, not update

irb(main):006> rows = SObjectModel.connection.exec_query "SELECT Name FROM Account LIMIT 1", model_class: Account
=>
[#<Account:0x0000724bada70970
...
irb(main):007> rows.first
=>
#<Account:0x0000724bada70970
  @Name="Express Logistics and Transport2",
  @current_attributes={:Name=>"Express Logistics and Transport2"},
  @original_attributes={:Name=>"Express Logistics and Transport2"},
  @updated_attributes={:Name=>nil}>

 => rows.first.save  #=> this does NEW RECORD INSERT, not Update because its Id is nil

Child relationship in SELECT clause

child relationsip that doesn’t specify Id in SELECT clause causes model class to force insert a record, not update

irb(main):016> acc = Account.select(:Name, '(SELECT Name FROM Contacts)').where(Id: Contact.where.not(AccountId: nil).pluck(:AccountId)).take
=>
#<Account:0x0000724bae18f4b8
...
irb(main):017> acc
=>
#<Account:0x0000724bae18f4b8
 @Contacts=
  [#<Contact:0x0000724bacf411a8 @Name="Gonzalez Rose", @current_attributes={:Name=>"Gonzalez Rose"}, @original_attributes={:Name=>"Gonzalez Rose"}, @updated_attributes={:Name=>nil}>,
   #<Contact:0x0000724bae18f468 @Name="Forbes Sean", @current_attributes={:Name=>"Forbes Sean"}, @original_attributes={:Name=>"Forbes Sean"}, @updated_attributes={:Name=>nil}>],
 @Id="0015j00001U2XvEAAV",
 @Name="Edge Communications",
 @current_attributes={:Name=>"Edge Communications"},
 @original_attributes={:Name=>"Edge Communications", :Id=>"0015j00001U2XvEAAV"},
 @updated_attributes={:Name=>nil}>

irb(main):018> acc.Contacts.first.save #=> this does NEW RECORD INSERT, not Update because its Id is nil

Development Note

1. To develop, install libraries at first.

$ bundle install

2. This project adopts TDD (Test Driven Development)

# rspec
$ bundle install rspec

# cucumber
$ SF_TARGET_ORG=your_target_org bundle exec cucumber

3. For debug, use IRB

$ bundle exec rake irb[your_target_org]
> gen :Account, :Contact  #=> shortcut to generate classes

The word “target org” is a technical term of Salesforce CLI. It means the alias of Salesforce org. You can check it like bellow:

$ sf org list
Type   Alias      Username                          Org ID             Status    Expires
------------------------------------------------------------------------------------------
DevHub dev        hoge@example.sandbox              00D5j00000DiuxmEAB Connected
       trailhead  hoge@resourceful-hoge-bar-baz.com 00DIS000002BinI2AS Connected

See also the command reference.