sObjects, SOQL and SOSL · Lesson 14 of 41
sObjects in Apex: Working with Records
Every Salesforce record you touch in Apex is an sObject: an Account, a Contact, a custom Invoice__c. Each object in your org automatically becomes an Apex type, with its fields as properties. That's what makes Apex feel so direct – and understanding sObjects (including the generic SObject type) is the foundation for SOQL, DML and triggers.
In this lesson
- Specific sObject types and their fields
- Lookup fields vs relationship fields
- The generic SObject type with get and put
- Schema describe: learning about objects at runtime
Creating sObjects
// three ways to set fields
Account acc = new Account(Name = 'Acme Corp', Industry = 'Energy'); // name = value pairs
acc.AnnualRevenue = 5000000; // dot notation
acc.put('Rating', 'Hot'); // generic put by name
System.debug(acc);
System.debug('Saved yet? ' + (acc.Id != null));
insert acc; // saving fills in the Id
System.debug('Saved now? ' + (acc.Id != null) + ', prefix ' + String.valueOf(acc.Id).left(3));
// a field you never set or queried is simply null
System.debug('Phone: ' + acc.Phone);
Account:{Name=Acme Corp, Industry=Energy, AnnualRevenue=5000000, Rating=Hot}
Saved yet? false
Saved now? true, prefix 001
Phone: nullAn sObject in memory is just data until you save it with DML. Custom objects and fields work the same way, with their API names: new Invoice__c(Amount__c = 500).
Lookup fields and relationship fields
A lookup has two faces. The Id field (AccountId, Customer__c) holds the parent's Id and is what you set when saving. The relationship field (Account, Customer__r) holds the parent record itself – it's only filled when you query it:
Account parent = new Account(Name = 'Globex');
insert parent;
Contact c = new Contact(LastName = 'Rao', AccountId = parent.Id); // set the Id field to link
insert c;
Contact loaded = [SELECT LastName, AccountId, Account.Name, Account.CreatedDate FROM Contact WHERE Id = :c.Id];
System.debug('AccountId set: ' + (loaded.AccountId == parent.Id));
System.debug('Parent name via relationship: ' + loaded.Account.Name);
Contact notQueried = [SELECT LastName FROM Contact WHERE Id = :c.Id];
try {
System.debug(notQueried.Account.Name); // relationship not in the SELECT
} catch (SObjectException e) {
System.debug(e.getMessage());
}
AccountId set: true Parent name via relationship: Globex SObject row was retrieved via SOQL without querying the requested field: Contact.Account
The last line is worth remembering: reading a field – or a relationship – that wasn't in the query's SELECT list doesn't give you null, it throws an SObjectException. When code fails with "SObject row was retrieved via SOQL without querying the requested field", add the field to the query.
| Standard object | Custom lookup | |
|---|---|---|
| Id field (set this to link) | Contact.AccountId | Invoice__c.Customer__c |
| Relationship field (query this to read the parent) | Contact.Account.Name | Invoice__c.Customer__r.Name |
| Child relationship (subquery from the parent) | Account.Contacts | Account.Invoices__r |
The generic SObject type
SObject can hold any record. You lose compile-time field checks, but gain the ability to write code that works with any object – a field-copy utility, a generic logger, a configurable mapping:
SObject rec = Schema.getGlobalDescribe().get('Lead').newSObject(); // create by object name
rec.put('LastName', 'Shah');
rec.put('Company', 'Initech');
rec.put('NumberOfEmployees', 85);
System.debug('Type: ' + rec.getSObjectType());
System.debug('Company: ' + rec.get('Company'));
// copy several fields by name – the kind of code that drives field mappings
Map<String, String> mapping = new Map<String, String>{ 'Company' => 'Name', 'NumberOfEmployees' => 'NumberOfEmployees' };
Account target = new Account();
for (String src : mapping.keySet()) {
target.put(mapping.get(src), rec.get(src));
}
System.debug(target);
// get() returns Object – cast to use it
Integer employees = (Integer) rec.get('NumberOfEmployees');
System.debug(employees + 15);
Type: Lead
Company: Initech
Account:{Name=Initech, NumberOfEmployees=85}
100Schema describe: learn about objects at runtime
Schema.DescribeSObjectResult accDescribe = Account.SObjectType.getDescribe();
System.debug('Label: ' + accDescribe.getLabel() + ' / ' + accDescribe.getLabelPlural());
System.debug('Key prefix: ' + accDescribe.getKeyPrefix());
System.debug('Can the user create Accounts? ' + accDescribe.isCreateable());
Schema.DescribeFieldResult ratingField = Account.Rating.getDescribe();
System.debug('Rating type: ' + ratingField.getType());
List<String> values = new List<String>();
for (Schema.PicklistEntry pe : ratingField.getPicklistValues()) values.add(pe.getValue());
System.debug('Rating values: ' + values);
Map<String, Schema.SObjectField> fields = Schema.SObjectType.Contact.fields.getMap();
System.debug('Contact has an Email field? ' + fields.containsKey('email'));
System.debug('Email is ' + fields.get('email').getDescribe().getType());
Label: Account / Accounts Key prefix: 001 Can the user create Accounts? true Rating type: PICKLIST Rating values: (Hot, Warm, Cold) Contact has an Email field? true Email is EMAIL
Describe calls power dynamic UIs (picklist options for a component), security checks (isAccessible(), isUpdateable()) and generic code. Field map keys are lower-case, as the example shows. Describe results are cached by the platform, but in loops prefer calling them once and storing the result.
Practice
Try it yourself
You set
contact.Account = new Account(Name = 'X');and insert the contact, but it isn't linked to any account. Why?Show answer
Linking uses the Id field: set
contact.AccountIdto an existing account's Id. The relationship field is for reading queried data (or for upserting by an external Id, a special case).Write a method that returns the picklist values of any field, given the object and field names as Strings.
Show answer
Schema.SObjectField f = Schema.getGlobalDescribe().get(objName).getDescribe().fields.getMap().get(fieldName); List<String> out = new List<String>(); for (Schema.PicklistEntry pe : f.getDescribe().getPicklistValues()) out.add(pe.getValue()); return out;
Quick check
What does a custom lookup's relationship field end with?
__r (the Id field ends with __c).
When is acc.Id filled in?
After the record is inserted (or when it's queried).
What does SObject.get() return?
An Object – cast it to the field's type.
Summary
- Every object is an Apex type; fields are properties with API names.
- Set the Id field to link records; query the relationship field to read the parent.
- The generic SObject type with get/put enables code that works with any object.
- Schema describe gives labels, field types, picklist values and permissions at runtime.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.