BISO entities use read/write table separation with database views that JOIN multiple tables, resulting in entities with ~100 properties (e.g., Beratungsfall). The backend always loaded ALL view columns via SELECT *, causing:
In list views (grids), only 10-15 fields are typically needed for display, but the backend would load all 100 fields for each record.
The Selective Column Loading feature allows the frontend to specify which entity fields it needs. The backend returns partial entities with only the requested columns, reducing query complexity and network payload.
Key Components:
BISOEntityBuilder::applyColumnSelection() handles field filtering with automatic system column inclusionKP.base.Store supports partialColumns config to request specific fieldsKP.base.Model prevents saving partial records to avoid data lossControllers using EntityRequestTrait::entityList() automatically support field selection:
// No changes needed - already supports 'fields' parameter
class PersonController extends KPController {
use EntityRequestTrait;
public function listAction() {
return $this->entityList(Person::class);
}
}
For controllers with custom list logic, call processFields():
// File: webroot/backend/controllers/BeratungsfallController.php
public function listAction() {
$builder = $this->createEntityBuilder(Beratungsfall::class);
// Add this line to support selective columns
$this->processFields($builder, $this->params);
// ... rest of your custom logic
return $builder->executeSelect()->fetchAll();
}
Frontend sends fields parameter:
POST /backend/Beratungsfall/list
fields: ["kunde_name", "kunde_vorname", "berater_name"]
Backend processes fields:
EntityRequestTrait::processFields() extracts and decodes the fields parameterBISOEntityBuilder::applyColumnSelection() automatically adds:#[IdProperty] attribute)user_id, group_id, group_mod, world_modcreation_date, modification_date$builder->columns($fields) to generate SELECT id, kunde_name, kunde_vorname, ...Backend returns partial entities:
$entity->setPartial()// File: backend-test/specs/logic/BISOEntityBuilderTest.php
public function testApplyColumnSelectionAddsSystemColumns() {
$eb = $this->entityBuilder(Beratungsfall::class);
$eb->applyColumnSelection(['kunde_name']);
$sql = $eb->getQuery()->getSql();
// Verify ID and system columns are included
$this->assertStringContainsString('id', $sql);
$this->assertStringContainsString('user_id', $sql);
$this->assertStringContainsString('creation_date', $sql);
$this->assertStringContainsString('kunde_name', $sql);
}
// File: webroot/public/app/src/beratungsfall/List.js
Ext.define('KP.beratungsfall.List', {
extend: 'Ext.grid.Panel',
initComponent() {
this.store = Ext.create('KP.beratungsfall.Store', {
// Specify only the fields needed for grid display
partialColumns: [
'kunde_name',
'kunde_vorname',
'kunde_name_vorname', // computed field
'berater_name',
'berater_vorname',
'creation_date',
'abschluss_am',
'regionalstelle_kurz'
]
});
this.callParent();
}
});
What happens:
fields parameter to backend on every load/filterisPartial = truePartial records cannot be saved directly to prevent data loss. Reload the full record before editing:
// File: webroot/public/app/src/beratungsfall/ListController.js
editRecord(record) {
if (record.isPartial) {
// Show loading indicator
this.getView().setLoading(AG('LADEN'));
// Reload full record
record.reloadFull({
success: (fullRecord) => {
this.getView().setLoading(false);
this.openFormWithRecord(fullRecord);
},
failure: (record, operation) => {
this.getView().setLoading(false);
Ext.Msg.alert('Error', 'Failed to load full record');
}
});
} else {
this.openFormWithRecord(record);
}
}
openFormWithRecord(record) {
// record.isPartial === false
// Safe to edit and save
const form = Ext.create('KP.beratungsfall.Form', { record: record });
form.show();
}
// Automatic protection in KP.base.Model
save() {
if (this.isPartial) {
throw new Error(
'Cannot save partial record. Reload the full record first.'
);
}
return this.callParent(arguments);
}
// Model.load() always loads all fields (not partial)
KP.beratungsfall.Model.load(123, {
success: (record) => {
// record.isPartial === false
// All fields loaded, safe to edit and save
}
});
1. Store Configuration
// File: webroot/public/app/src/beratungsfall/List.js
this.store = Ext.create('KP.beratungsfall.Store', {
partialColumns: [
'kunde_name',
'kunde_vorname',
'kunde_name_vorname',
'berater_name',
'berater_vorname',
'berater_name_kurz',
'creation_date',
'abschluss_am',
'regionalstelle_kurz'
]
});
2. Backend Processing
// File: webroot/backend/controllers/BeratungsfallController.php
public function listAction() {
$builder = $this->createEntityBuilder(Beratungsfall::class);
// Enable selective column loading
$this->processFields($builder, $this->params);
// Apply custom filters
if (!empty($this->params['homeFilter'])) {
$builder->where('user_id = :userId', [':userId' => $this->getUserId()]);
}
return $builder->executeSelect()->fetchAll();
}
3. Edit Workflow
// File: webroot/public/app/src/beratungsfall/ListController.js
onItemDblClick(grid, record) {
this.editRecord(record);
}
editRecord(record) {
if (record.isPartial) {
this.getView().setLoading(AG('LADEN'));
record.reloadFull({
success: (fullRecord) => {
this.getView().setLoading(false);
this.openFormWithRecord(fullRecord);
},
failure: () => {
this.getView().setLoading(false);
Ext.Msg.alert('Error', AG('ERROR_LOADING_RECORD'));
}
});
} else {
this.openFormWithRecord(record);
}
}
openFormWithRecord(record) {
Ext.globalEvents.fireEvent(
KP.GlobalEventsController.getShowBeratungsfall(),
record.getId()
);
}
Before (full load):
After (41 partial columns):
Stores without partialColumns config work unchanged:
// Full load (all columns) - backward compatible
Ext.create('KP.person.Store', {
// No partialColumns - loads all fields as before
}).load();
You don't need to specify these - the backend adds them automatically:
user_id, group_id, group_mod, world_modcreation_date, modification_dateIf your view has computed fields (e.g., kunde_name_vorname computed from kunde_name + kunde_vorname), include the computed field name in partialColumns:
partialColumns: [
'kunde_name', // Base field
'kunde_vorname', // Base field
'kunde_name_vorname' // Computed field (requires both base fields)
]
Use for:
Don't use for:
Model.load()