SAP Hybris Tutorial – Data Modeling | items.xml File | Unit 9
This is the very important part we need to know about the data modeling in Hybris.
SAP Commerce allows you to match your specific business needs through product and data modeling. Good understanding of the type system allows you to reflect in your application your business model. The bean and enum generation feature also serves that purpose.
The items.xml file specifies the types (classes) of items (objects). By editing the items.xml file, you can define new types or extend existing types. In addition, you can define, override, and extend attributes in the same way.
In this post, you will know how important the items.xml file is.
Let’s go deep dive into each part.
1. Sections of the file, in order
1.1 The location of the items.xml file
The items.xml is located in the resources directory of an extension. The items.xml files are prefixed with the name of their respective extension in the form of <extension name>-items.xml. For example:
- For the core extension, the file is called core-items.xml.
- For the catalog extension, the file is called catalog-items.xml.
1.2 The structure of the items.xml file
<items>
<atomictypes/> <!-- rarely used: basic Java types -->
<collectiontypes/> <!-- List/Set of something -->
<enumtypes/>
<maptypes/>
<relations/>
<itemtypes/> <!-- the actual tables -->
</items>
The order is enforced by the XSD, and so is the order inside <itemtype>: <description> must come before <deployment>.
Every type is identified by its code, for example SearchItem. At build time Hybris generates a Model class for it (SearchItemModel), and update system creates the database columns.
2. Item types (≈ tables)
<itemtype code="SearchItem" extends="GenericItem" autocreate="true" generate="true">
<description>...</description>
<deployment table="SearchItemTbl" typecode="12106"/>
<attributes>...</attributes>
<indexes>...</indexes>
</itemtype>
| Attribute | Meaning |
|---|---|
extends | The parent type. GenericItem is the root for new types. You can also extend Product, B2BCustomer, and so on. A subtype inherits all parent attributes and is returned by FlexibleSearch queries on the parent. |
autocreate="true" | This file creates the compose type. Use false to add attributes to an existing type from another extension. |
generate="true" | Generate the Jalo source for the type (the Generated<Type> class, plus the jaloclass stub if it doesn’t exist yet). It does not control the Model class: <Type>Model is still generated with generate="false". To skip the Model, add <model generate="false"/> inside the <itemtype> (items.xsd: “Whether a model for the type and models for subtypes will be generated”). |
abstract="true" | The type can’t be instantiated; only its subtypes can. |
jaloclass | Legacy Jalo layer class. (identify the custom package name) |
A few things to watch for:
- Don’t specify a
jaloclassattribute pointing to a class that won’t exist. - Avoid
persistence type="jalo"attributes. Those are computed by methods in the Jalo class. Usepersistence type="dynamic"with aDynamicAttributeHandlerinstead, which is the ServiceLayer approach. - Legacy code that calls the Jalo API for your type directly, such as old custom ImpEx translators or Jalo-based logic, would need the Jalo class. For a new type written only with models and services, this usually doesn’t apply.
- Only add custom code (if any) in the SearchItem.java file but not GeneratedSearchItem.java file (cause this file is an abstract class in your extension’s
gensrcfolder. It is regenerated on every build, so you should never edit it.)
<deployment>: table and typecode
- The typecode must be unique across the whole system. Custom types should use 10000 or higher; values below that are reserved for SAP.
- A subtype without its own
<deployment>shares its parent’s table. That’s why mostProductsubtypes end up in theproductstable. - Once a type has data, never change its typecode or table.
3. Attributes (≈ columns)
<attribute qualifier="dataType" type="SearchDataType">
<persistence type="property"/>
<modifiers optional="false" unique="true"/>
<defaultvalue>...</defaultvalue>
<description>...</description>
</attribute>
qualifier: the attribute name, which generates getDataType() and setDataType().
type: what the attribute holds
| Kind | Examples |
|---|---|
| Atomic | java.lang.String, java.lang.Integer, java.lang.Boolean, java.util.Date, java.math.BigDecimal |
| Item reference | B2BCustomer, Product: stored as a PK column (foreign-key style) |
| Enum | SearchDataType |
| Collection | GenderList |
| Localized | localized:java.lang.String: one value per language, stored in a separate ...lp table |
<persistence type=...>
property: stored in a database column (the normal case).dynamic: not stored. It’s computed by a SpringattributeHandlerbean on every get.- Column size and type: add
<columndefinition>to override the default per database.SearchFilter.filtersusesHYBRIS.LONG_STRING, which maps to CLOB/NVARCHAR(MAX) for long JSON.
<modifiers>: constraints and access rules
| Modifier | Effect |
|---|---|
optional="false" | Mandatory. The service layer rejects saving the model if the value is null. |
unique="true" | Part of the type’s uniqueness key; checked at the service layer. All attributes marked unique together form one combined key. That’s how customer + dataType became a unique pair. |
read / write | Generate the getter / setter. write="false" makes the attribute read-only after creation. |
initial="true" | Can be set when the item is created, even if write="false". The usual combination is write="false" initial="true", for a value set at creation and never changed. |
search="true" | Can be used in FlexibleSearch queries. |
partof="true" | Cascade delete: removing the owner removes this child too (searchItems, searchFilters). |
encrypted="true" | The value is encrypted in the database. |
<defaultvalue>
A Java expression, for example Boolean.FALSE. For an enum: em().getEnumerationValue("SearchDataType","DELIVERYNOTE").
4. Enums
<enumtype code="BrandType" dynamic="true">
<value code="A"/>
<value code="B"/>
</enumtype>
dynamic="false": compiled into a real Javaenum. New values require a code change and a build.dynamic="true": stored as rows in the database. You can add values with ImpEx or in Backoffice without a build, so the generated class isn’t a real Javaenum(it’s a class with constants).- Extending an existing enum: use
autocreate="false" generate="false"and list only the new values. That’s how to adds statuses to OOTBOrderStatus.
5. Relations
A relation links two item types.
One-to-many
There’s no extra table; the “one” side is stored as a column on the “many” side.
<relation code="B2BCustomer2SearchItem" localized="false">
<sourceElement type="B2BCustomer" qualifier="customer" cardinality="one">
<modifiers optional="false" unique="true"/>
</sourceElement>
<targetElement type="SearchItem" qualifier="searchItems" cardinality="many" collectiontype="list">
<modifiers partof="true"/>
</targetElement>
</relation>
The qualifier names are easy to read backwards. Each element’s qualifier is the property name on the other type:
customeris the column onSearchItem, so you callsearchItem.getCustomer().searchItemsis the property onB2BCustomer, so you callcustomer.getSearchItems(). It’s not a column: Hybris queries it from the other side.
So the modifiers on sourceElement describe the customer attribute of SearchItem. That’s why unique="true" went there.
Many-to-many
This uses a link table, so it needs its own <deployment>:
<relation code="PromoReq2Approver" ...>
<deployment table="PromoReq2ApproverRel" typecode="10108"/>
<sourceElement ... cardinality="many"/>
<targetElement ... cardinality="many"/>
</relation>
collectiontype="list|set|collection": the Java type of the getter’s return value.ordered="true": keeps the list order with an extra position column. This is the order items were saved in, not a sort by attribute; see “Ordering the many side” below.
Relation vs. collection-type attribute
Use a relation. A collection-type attribute (for example List<Product>) is stored as a comma-separated PK string in one column. You can’t query it with FlexibleSearch joins, and it gets slow. Collection types are fine for small lists of enums, like GenderList.
Reading a relation: getter navigation vs. a DAO query
Question (2026-10-17): why not get the filters with customer.getSearchItems().stream().filter(DELIVERYNOTE).findFirst().getSearchFilters() instead of a DAO query?
// Navigating the relation
final List<SearchFilterModel> filters = customer.getSearchItems().stream()
.filter(searchItem -> SearchDataType.DELIVERYNOTE.equals(searchItem.getDataType()))
.findFirst()
.map(SearchItemModel::getSearchFilters)
.orElse(List.of());
This works. Behind the getters, Hybris runs two queries: WHERE p_customer = ? for the search items, then WHERE p_searchitem = ? for the filters. It then loads each row as a model.
| Relation getters | DAO query (DefaultSearchItemDao) | |
|---|---|---|
| Database round trips | 2, plus loading each row as a model | 1 |
| Rows loaded | All search items, then all of their filters | Only the rows that match |
ORDER BY code | Done in Java. The relation isn’t ordered="true", so the collection order isn’t guaranteed | Done by the database |
Filtering by codes | Done in Java, after loading everything | IN (?codes) in SQL |
| Remove by code | Load all filters, then find the one to remove | One query that also checks the customer |
- Ownership is checked either way. Both versions start from the current customer.
- Indexes still matter.
getSearchFilters()runsWHERE p_searchitem = ?, so it needs the same index on the relation column as the DAO join (see section 6). - Fresh data. A FlexibleSearch query always reflects what’s in the database. A collection that’s already loaded on a model can, in principle, miss a row another request just added. Not checked for OCC’s current-user model.
- When getters are fine: small collections, and code that already has the owner model. Getters go through the model cache and are easy to stub in unit tests.
- Decision for Project: depends on situation to use the proper method.
Ordering the many side
Question (2026-10-17): how do you order the items of a relation’s “many” side by a specific criterion?
Hybris has no items.xml setting that sorts a relation by an attribute (such as code or name). The options:
1. ordered="true": keeps the order you save in, not a sort.
<targetElement type="SearchFilter" qualifier="searchFilters" cardinality="many"
collectiontype="list" ordered="true"/>
items.xsd: “If ‘true’ an additional ordering attribute will be generated for maintaining ordering. Default is ‘false’.”- One-to-many: a position attribute on the “many” side, named
<qualifier>POS(here it would besearchItemPOS). Platform examples in the generated models:classificationClassPOS,unitPOS. - Many-to-many: the position is stored in the link table.
- The getter returns items in the order the list was saved. To change the order, reorder the list and save it.
- Use it for orders people arrange by hand (a sequence, “move up/down”). It doesn’t keep items sorted by an attribute.
- Adding it to an existing relation: update system adds the column, but existing rows have no position value, so you need to fill it in.
2. Sort in Java after calling the getter. This is the simplest option for small collections; all rows are still loaded.
searchItem.getSearchFilters().stream()
.sorted(Comparator.comparing(SearchFilterModel::getCode))
.toList();
3. A dynamic attribute that returns the sorted list. Callers get a getter without knowing how the sorting is done. The cost is a collection type, a handler class and a Spring bean.
<attribute qualifier="sortedSearchFilters" type="SearchFilterList">
<persistence type="dynamic" attributeHandler="sortedSearchFiltersAttributeHandler"/>
<modifiers write="false"/>
</attribute>
The DynamicAttributeHandler either sorts the relation collection or runs an ORDER BY query.
4. ORDER BY in a DAO query. The database sorts, and you can sort on any attribute, several attributes, or a joined type’s attribute. Uses this: ... ORDER BY {sf:code} ASC in DefaultSearchItemDao.
| Need | Use |
|---|---|
| Order set by hand by users or admins | ordered="true" |
| Small collection, sort only in one place | Java Comparator |
| Sorted getter reused in many places | Dynamic attribute |
| Sort by data, larger collections, filtering at the same time | DAO ORDER BY |
6. Indexes and constraints
<indexes>
<index name="SearchItemCustomerIdx" unique="true">
<key attribute="customer"/>
<key attribute="dataType"/>
</index>
</indexes>
- Without
unique, an index only speeds up lookups. Add indexes for columns used inWHEREclauses. - One-to-many relation columns get no index automatically. Declare one if you look up children by their parent; the platform does this itself (
oeOrdonAbstractOrderEntry.orderincore-items.xml).SearchFilterSearchItemIdx(searchItem), because the filter list query andgetSearchFilters()both look up rows byp_searchitem. - With
unique="true", the database enforces uniqueness.
Hybris has no real database foreign keys and no CHECK constraints. Data integrity comes from these layers:
| Layer | Mechanism | Catches |
|---|---|---|
| Service layer | optional="false", unique="true" (via interceptors such as UniqueAttributesInterceptor) | Normal modelService.save() calls, with readable errors |
| Database | Unique <index> | Everything, including concurrent saves and direct SQL/ImpEx |
| Custom code | ValidateInterceptor / PrepareInterceptor Spring beans | Business rules, for example startDate <= endDate |
| Data validation | Constraints framework (AttributeConstraint, set up in Backoffice/ImpEx) | Size/pattern/not-null rules, shown in Backoffice |
When the service layer or the database rejects a duplicate, modelService.save() throws a ModelSavingException. For how to handle a concurrent duplicate save, see DefaultSearchItemService.getOrCreateSearchItem in SEARCH_ITEM_FILTERS.md.
Use both layers: the interceptor gives a readable error, and the database index covers concurrent saves that the interceptor can miss.
7. What happens when you change items.xml
ant buildregenerates the Model classes. If an attribute exists in XML but you get “cannot find symbol”, the build hasn’t run.- Update system (HAC or
ant updatesystem) adds new tables and columns.- It never drops or alters existing columns. Renaming an attribute creates a new empty column, and changing a type may silently do nothing. Plan renames with a data migration.
- Index changes on existing tables may not apply. For example, turning an existing index unique may need a manual drop or rename. Delete duplicate rows first, or the unique index can’t be created.
- Initialize drops everything. Never run it on shared or production systems.
8. Common gotchas
- Typecode collisions break the build. Check them with
grep 'typecode="12'before you choose one. <description>must come before<deployment>.- Don’t extend OOTB types with
autocreate="true"; that tries to create the type again. Useautocreate="false". partof="true"on the wrong side can delete more than you expect. Put it on the children side.- Every
propertyattribute is a column, and wide tables likeproductshit database limits. Large text should beHYBRIS.LONG_STRINGor a separate type. - To see the live result, open HAC → Platform → Type system and look up a type such as
SearchItem. It shows attributes, modifiers and the deployment table.
9. Practice
Using the xml code that defined the Manufacturer item type on step 3 above to practice.
Step 1: add the XML code to the trainingcore-items.xml file
Step 2: Add the one-to-many relationship between the compose types.
Step 3: Build the Hybris system to see the new type and the relationship will be generated on each item type. $ ant clean all
Step 4. Start the server -> update the system
Step 5: Import sample data
Step 6: Verify the result in BO.
10. Best practice
Congratulation.! Good job. You have just finished the lesson. Thanks for your concentration. If this post is useful, please share and give me a heart up in the post.
That’s all for today. In the next post, we will learn about the Converter and Populator. Hope to see you in the next article.
Happy coding.!!! <3
