Attempt to de-reference null object when using maps to build parent-child relationship for visualforce page

apexcontrollermapnullvisualforce

Products Products__c

Name Label Data Type Sample Data Sample Data 2 Sample Data 3
PDC PDC__c Text ACA ACG TBC
PDC Desc PDC_Desc__c Text Tire Size Rim / Wheel Disc Size Type of Toolbox
Value Desc Value_Desc__c Text 255/70R 22.5 22.5×8.25 None
Prod Order Prod_Order__c Lookup a1C2f000001jCyYEAU a1C2f000001jCyYEAU a1C2f000001jCyYEAU

Product Groups Products_Groups__c

Name Label Type Sample Data Sample Data 2 Sample Data 3
Product Code Product_Code__c Text ACA ACG TBC
Product Group Product_Group__c Text Tires / Wheels / Hubs and Drums Tires / Wheels / Hubs and Drums Tool Boxes
Product Group Seq Product_Group_Seq__c Number 2 2 5
Product Seq Product_Seq__c Number 1 2 5

Desired VFP Results based on sample data. Sorted first by Product_Group_Seq__c then Product_Seq__c:

Tires / Wheels / Hubs and Drums
Tire Size 255/70R 22.5
Rim / Wheel Disc Size 22.5×8.25
Tool Boxes
Type of Toolbox None

Below is my standard controller.

I am trying to get this into a parent-child map format where I can display on visualforce page with the Product_Group__c from Product_Groups__c being a header value showing all Product_Code__c and their child value Value_Desc__c from Products__c below in repeating format.

I am receiving attempt to de-reference null object

and I can see in debug it says AdjacentGroup|null

11:07:44:377 EXCEPTION_THROWN [23]|System.NullPointerException: Attempt to de-reference a null object

public with sharing class Productsv2Controller {
    
    public String OrderId {get;set;}
    public Map<String, Products_Groups__c> groupsMap {get;set;}
    public Productsv2Controller() {
        
        OrderId = ' ';
        
        OrderId=System.currentPageReference().getParameters().get('id');
        
        System.debug('...................OrderId=='+OrderId);
        
        Id i= Id.valueOf(OrderId);
        
        Map<String, Products_Groups__c> groupsMap = new Map<String, Products_Groups__c>();
        for(Products_Groups__c og :[SELECT Id,Name, Product_Code__c,Product_Group_Seq__c,Product_Group__c,Product_Priority_Level__c,Product_Seq__c FROM Products_Groups__c]){
            groupsMap.put(og.Product_Code__c, og);
        }
        
        for(Products__c o :[SELECT Id,PDC__c, PDC_Desc__c, Value_Desc__c from Products__c WHERE Prod_Order__c = :i]){
            Products_Groups__c adjacentGroup = groupsMap.get(o.PDC__c);
            
            system.debug('Group: '+adjacentGroup.Product_Group__c+' Code: '+adjacentGroup.Product_Code__c+' PDC: '+o.PDC__c+' Value: '+o.Value_Desc__c);
        }
        
    }
}

Best Answer

I would restructure this controller to use an inner class (sometimes called a wrapper class so your data model is easier to handle in the VF page

public inherited sharing class Productsv2Controller {
    
    public String orderId { // orderId lazy loader
       get {
         if (orderId == null) {
            orderId = System.currentPageReference().getParameters().get('id');
         } set;
    }

    // inner class that represents the parent-children for a single parent
    public class ProductGroup { 
       public Products_Groups__c pg {get; private set;}
       public List<Product2> products {get; private set;}

       ProductGroup withProductGroup(Products_Groups__c val) {this.pg = val; return this;}
       ProductGroup withProducts(Products__c[] val) {this.products = val; return this;}
    }

    // Map used by VF page to display all parents and children
    public Map<String, ProductGroup> productGroupsByProductCode {
       get {
          if (productGroupsByProductCode == null) { // lazy load

            // Pivot order's Products__c by PDC
            productGroupsByProductCode = new Map<String,ProductGroup>();
            Map<String,Products__c[]> productsByPDC = new Map<String,Products__c[]>();
            
            for(Products__c p :[SELECT Id,PDC__c, PDC_Desc__c, Value_Desc__c 
                                  FROM Products__c WHERE Prod_Order__c = :this.orderId]) {
                String pdcLower = p.Pdc__c.toLowerCase();
                if (!productsByPDC.containsKey(pdcLower)) {
                  productsByPDC.put(pdcLower,new List<Products__c>())
                }
                productsByPDC.get(pdcLower).add(p); // build list
            }

            // Construct the parent-child
            for (Products_Groups__c pg: [SELECT Id, Product_Code__c, ...
                                            FROM Products_Groups__c]
                                            ) {
                productGroupsByProductCode.put(pg.Product_Code__c, new ProductGroup()
                  .withProductGroup(pg)
                  .withProducts(productsByPDC.containsKey(pg.Product_Code__c.toLowerCase()
                    ? productsByPDC.get(pg.Product_Code__c.toLowerCase())
                    : new List<Products__c>());                                
            }

          }
       } private set;
    }
    public Productsv2Controller() {}
        
        
}

VF markup (based on VF doc) snippet

<apex:repeat value="{!productGroupsByProductCode}" var="productCode">
   <apex:outputText value="{!productGroupsByProductCode[productCode].pg.XXX__c}"/>
   
   <apex:repeat value="{!productGroupsByProductCode[productCode].products" var="prod">
      <apex:outputText value="{!prod.PDC_Desc__c}"/> // or whatever field(s) you want from Products__c object
   </apex:repeat>
</apex:repeat> 

Some notes

  1. SObject names with plurals are a really bad idea. If this is de novo development, I'd really encourage you to singularize the SObject names
  2. I renamed your variables and changed to camelCase
  3. The inner class uses a fluent pattern (sometimes called a builder pattern) to set its properties. This is not strictly necessary but useful to learn for a variety of reasons
  4. Everything is done with collections which is a good way to avoid NPE;
  5. I agree with Derek's advice about map keys which are case sensitive
  6. VF controllers should ideally have empty or near empty constructors and where all the logic is either in properties/methods referenced by the page's merge fields or page action methods for action="{!doSomething} on the page.
  7. I didn't test this so YMMV
Related Topic