OData V2 has been the workhorse of SAPUI5 apps for a decade. OData V4 is the model SAP is investing in — it powers the latest Fiori elements and CAP-based services. If you are starting a new app against a modern backend, the choice matters. Here is what actually changes.
Different Model, Different Philosophy
The V2 model (sap.ui.model.odata.v2.ODataModel) is request-centric: you call read, create, update, remove, and the model fires events. The V4 model (sap.ui.model.odata.v4.ODataModel) is binding-centric: you bind controls to contexts, change the data, and the framework figures out the requests.
In V4, there are no explicit CRUD calls in normal use. Editing a bound input marks the context dirty; submitBatch on the binding sends the changes. The model owns the request lifecycle.
// V4: declarative binding, framework handles the rest
<Table items="{/Orders}">
<ColumnListItem>
<Input value="{DeliveryDate}"/>
</ColumnListItem>
</Table>
Key takeaway: V2 asks "what request do I send?", V4 asks "what data is bound?". Your code shifts from imperative calls to declarative bindings.
Bindings and Contexts
V4 bindings return Context objects rather than raw data. A list binding's contexts each know their entity, their key predicate, and their change state. Operations like create happen through the binding:
const oListBinding = this.byId("orderTable").getBinding("items");
const oContext = oListBinding.create({ OrderNo: "4711", Status: "Open" });
this.getView().setBindingContext(oContext); // navigate to new entry
Filtering, sorting, and grouping also move onto the binding: oBinding.filter(), oBinding.sort(). There is no separate read with $filter strings — though $filter still exists in the URL the framework builds for you.
Batching and Group IDs
V2 batches everything into one $batch by default, which developers constantly fought with useBatch toggles. V4 replaces this with group IDs: each binding declares which group its requests belong to.
// manifest.json — V4 data source
"dataSources": {
"mainService": {
"uri": "/odata/v4/sales/",
"type": "OData",
"settings": { "odataVersion": "4.0" }
}
}
The default group is $auto, which submits changes automatically after a short delay. Explicit control comes from $direct (immediate, no batch) or custom groups you submit yourself with oModel.submitBatch("myGroup").
What Gets Better — and What Gets Harder
- Better: two-way binding by default, server-driven paging without manual
$skip/$top, cleaner$expandhandling, built-in support for OData V4 features like actions and functions bound to contexts. - Better: message handling — backend errors arrive as structured messages tied to properties, feeding straight into the MessageManager.
- Harder: the learning curve. Contexts, bindings, and group IDs replace the familiar read/update callbacks. Debugging means inspecting binding state, not network request code.
- Harder: V4 is stricter about metadata. A service that is slightly off-spec will fail in V4 where V2 tolerated it.
Key takeaway: V4 rewards you for thinking in bindings. If your app does heavy manual request orchestration, budget learning time — the mental model is genuinely different.
Migration Reality Check
There is no automatic migration path; V2 and V4 APIs do not map one to one. SAP's guidance is pragmatic: keep V2 apps on V2, build new apps on V4. The two models can coexist in one app during a transition, but sharing state between them is painful — plan the cutover per view, not per line.
Say you have an existing V2 master-detail app and the backend team ships a V4 service: start new features on V4 bindings in parallel views, and migrate the old screens when they next need real work. Big-bang rewrites rarely survive contact with a release calendar.
Have you migrated a V2 app to V4, or are you holding the line? Share your experience in the comments.