Skip to content

Customer CRUD with child persistence

clsCustomer owns the root COMPANY row and lazily exposes its address, email, phone, customer-contact relationship, and long-note children as properties. A single Save() call persists the root row first, then cascades to every child collection in a fixed order so link rows always reference an existing parent.

Field Value
Operation ID OP-CC-02
Trigger clsCustomer.NewCustomer(dpi) or clsCustomer.GetCustomer(dpi, customerID), followed by property/child edits and Save()
Inputs Root customer fields (name, type code, tax ID, active flag, internal reference ID), LongNoteText, and any of the Addresses, Emails, Phones, Contacts child collections
Outputs Persisted COMPANY row; persisted child address/email/phone/contact-relationship/note rows; InsertError/UpdateError/DeleteError broken rules on contention
Primary components clsCustomer.Create, clsCustomer.GetCustomer, clsCustomer.OnSave, clsCustomer.OnSaveComplete, clsCustomer.BeforeInsert, clsCustomer.BeforeUpdate, clsCustomer.BeforeDelete
Entry point Business\SAIF.Business.CustomerContact\Customer\clsCustomer.vb

Steps

  1. Create a new customer. Create() assigns a new CustomerID from GetNextSeqNum("COMPANY_SEQ") on the data-portal info.
  2. Retrieve an existing customer. GetCustomer(dpi, customerID) runs the BOF retrieve pipeline (BeforeRetrieve/AfterRetrieve) against COMPANY (SQL-035, COMPANY); the Addresses, Emails, Phones, Contacts, and CustomerNote child properties are lazy-loaded the first time they are accessed rather than during retrieve.
  3. Save pre-processing (OnSave).
  4. If the customer IsDeleted: clear Emails, Phones, and Addresses in memory, call CustomerNote.Delete(), and immediately save those four child objects before the root delete proceeds.
  5. Otherwise: set _UpdateDate = Now and _UpdateUser from the data-portal info user ID.
  6. If the existing CustomerNote is not new and its NoteText is blank, call CustomerNote.Delete() so the note row is removed rather than stored empty (BR-009).
  7. Persist the root row.
  8. Insert: BeforeInsert binds all root COMPANY fields (SQL-008, COMPANY).
    • On failure (rows affected ≠ 1): add the InsertError broken rule.
  9. Update: BeforeUpdate re-binds the same field set (SQL-068, COMPANY).
    • On failure (rows affected ≠ 1): add the UpdateError broken rule with the DB-contention message.
  10. Delete: BeforeDelete issues Delete COMPANY Where COMPANY_SEQ_NUM = :CustomerID (SQL-082, COMPANY).
    • On failure (rows affected ≠ 1): add the DeleteError broken rule.
  11. Save children after the root row (OnSaveComplete). In this fixed order: Addresses, Emails, Phones, Contacts, then CustomerNote. Each child collection or object persists through its own criteria and SQL (see Address info CRUD and linking, Email info CRUD and linking, Phone info CRUD and linking, Customer-contact relationship CRUD, Long-note persistence).
  12. Resolve the customer-type description. CustomerTypeDescription calls GetCustomerCode(dpi)(CustomerTypeCode).Description from the shared SAIF.Business code-map module to translate the stored type code into a display description; this is a lookup only and performs no persistence.

Process flow

The following diagram visualizes the customer save cascade.

flowchart TD
    Start([Caller calls Save]) --> Deleted{IsDeleted?}
    Deleted -->|Yes| ClearChildren[Clear Emails, Phones, Addresses;<br>delete CustomerNote;<br>save those four now]
    ClearChildren --> DeleteRoot[Delete COMPANY row]
    Deleted -->|No| SetAudit[Set UpdateDate, UpdateUser]
    SetAudit --> NoteCheck{CustomerNote not new<br>and NoteText blank?}
    NoteCheck -->|Yes| DeleteNote[Delete CustomerNote row]
    NoteCheck -->|No| RootSave
    DeleteNote --> RootSave[Insert or update COMPANY row]
    RootSave --> RowCheck{RowsAffected = 1?}
    RowCheck -->|No| Error([InsertError / UpdateError added])
    RowCheck -->|Yes| Cascade[OnSaveComplete:<br>save Addresses, Emails,<br>Phones, Contacts, CustomerNote]
    DeleteRoot --> RowCheck2{RowsAffected = 1?}
    RowCheck2 -->|No| DeleteError([DeleteError added])
    RowCheck2 -->|Yes| Done2([Customer removed])
    Cascade --> Done([Save complete])

Failure and edge-case behavior

  • A contention failure on the root insert/update/delete adds the corresponding broken rule, but the child-save cascade in OnSaveComplete still runs unconditionally afterward; there is no explicit short-circuit tied to the root row's row-count check.*
  • On delete, Contacts (the customer-contact relationship collection) is not cleared or explicitly cleaned up in OnSave, unlike Emails, Phones, and Addresses; any COMPANY_PERSON cleanup for a deleted customer is not evident in this class.*
  • Removing an item from Addresses, Emails, or Phones deletes only the link row (COMPANY_ADDRESS, COMPANY_EMAIL, COMPANY_PHONE); the standalone ADDRESS/EMAIL_ADDRESS/PHONE row is not deleted as part of this cascade.
  • A blank long note is deleted rather than saved as an empty string, so retrieving a customer with no note always yields a fresh, clean note object rather than a stored blank row (BR-009).

Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.

Sources

  • Business\SAIF.Business.CustomerContact\Customer\clsCustomer.vb