Developing ORM Entities for 1C-Bitrix D7
ORM D7 is Bitrix's answer to the question "how to work with a database without writing raw SQL for every little thing." A DataManager entity is a PHP class that describes a database table, its fields, and relationships with other tables. Instead of $DB->Query("SELECT ...") you write MyTable::getList([...]) and get typed objects instead of raw arrays. The platform appeared in Bitrix 14 and has since become the standard for serious development.
Basic DataManager Structure
Minimal entity:
namespace MyProject\Storage;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Fields\DatetimeField;
class OrderLogTable extends DataManager
{
public static function getTableName(): string
{
return 'my_order_log';
}
public static function getMap(): array
{
return [
new IntegerField('ID', [
'primary' => true,
'autocomplete' => true,
]),
new IntegerField('ORDER_ID', [
'required' => true,
]),
new StringField('ACTION', [
'required' => true,
'size' => 100,
]),
new DatetimeField('CREATED_AT', [
'default_value' => new \Bitrix\Main\Type\DateTime(),
]),
];
}
}
The class is registered in the module's autoload map or via PSR-4 in composer.json inside /local/.
ORM Field Types
| Field Class | DB Type | Notes |
|---|---|---|
IntegerField |
INT | primary, autocomplete, unsigned |
StringField |
VARCHAR | size — column length |
TextField |
TEXT | for long strings |
FloatField |
FLOAT / DECIMAL | |
BooleanField |
TINYINT(1) / CHAR(1) | values — value pair (N/Y) |
DateField |
DATE | returns \Bitrix\Main\Type\Date |
DatetimeField |
DATETIME | returns \Bitrix\Main\Type\DateTime |
EnumField |
VARCHAR | values — allowed values |
JsonField |
TEXT / JSON | automatic serialization/deserialization |
For field validation, use validation in the field parameters:
new StringField('STATUS', [
'required' => true,
'values' => ['DRAFT', 'ACTIVE', 'CLOSED'],
'validation' => function() {
return [new \Bitrix\Main\ORM\Fields\Validators\LengthValidator(1, 50)];
},
]),
Relationships Between Entities
One-to-many (Reference):
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
// Add relationship with order in OrderLogTable
new Reference(
'ORDER',
\Bitrix\Sale\Internals\OrderTable::class,
Join::on('this.ORDER_ID', 'ref.ID'),
['join_type' => 'LEFT']
),
One-to-one and many-to-many are implemented similarly, via Reference with the corresponding keys or through an intermediate table.
After defining the relationship, joined table data can be fetched in queries:
$result = OrderLogTable::getList([
'select' => ['ID', 'ACTION', 'ORDER_.USER_ID', 'ORDER_.DATE_INSERT'],
'filter' => ['ORDER_ID' => 42],
]);
Creating the Table: Migrations
The table is created by the createDbTable() method:
// In the module installation method
OrderLogTable::getEntity()->createDbTable();
For modifying an existing table structure — direct DDL queries via Application::getConnection():
$conn = \Bitrix\Main\Application::getConnection();
$conn->query("ALTER TABLE my_order_log ADD COLUMN CONTEXT TEXT DEFAULT NULL");
Bitrix does not have a built-in migration mechanism — for production projects this is solved either by a custom migration script or third-party packages such as arrilot/bitrix-migrations.
ORM Queries: Core Operations
Query with conditions:
$result = OrderLogTable::getList([
'select' => ['ID', 'ORDER_ID', 'ACTION', 'CREATED_AT'],
'filter' => [
'>CREATED_AT' => new \Bitrix\Main\Type\DateTime('-7 days'),
'ACTION' => 'STATUS_CHANGED',
],
'order' => ['CREATED_AT' => 'DESC'],
'limit' => 50,
'offset' => 0,
]);
while ($row = $result->fetch()) {
// $row — associative array
}
Object interface (D7 EO — Entity Objects, Bitrix 18+):
$result = OrderLogTable::getList([
'select' => ['*'],
'filter' => ['ORDER_ID' => 42],
]);
foreach ($result->fetchCollection() as $log) {
echo $log->getAction(); // typed getter
$log->setAction('UPDATED');
$log->save();
}
Insert:
$addResult = OrderLogTable::add([
'ORDER_ID' => 42,
'ACTION' => 'STATUS_CHANGED',
'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
if ($addResult->isSuccess()) {
$newId = $addResult->getId();
}
Update and delete:
OrderLogTable::update($id, ['ACTION' => 'CANCELLED']);
OrderLogTable::delete($id);
Aggregation and Grouping
$result = OrderLogTable::getList([
'select' => [
new \Bitrix\Main\ORM\Query\Query\ExpressionField('CNT', 'COUNT(*)'),
'ACTION',
],
'group' => ['ACTION'],
]);
Indexes
Indexes are added separately from table creation:
$conn = \Bitrix\Main\Application::getConnection();
$conn->query("CREATE INDEX idx_order_log_order_id ON my_order_log (ORDER_ID)");
$conn->query("CREATE INDEX idx_order_log_created ON my_order_log (CREATED_AT)");
Indexes are mandatory for frequently filtered fields. On a 500,000-row table, a query without an index on ORDER_ID is a full table scan.
Query Caching
ORM integrates with the Bitrix cache:
$result = OrderLogTable::getList([
'select' => ['ID', 'ACTION'],
'filter' => ['ORDER_ID' => 42],
'cache' => ['ttl' => 3600, 'cache_joins' => true],
]);
The cache is cleared automatically when data is modified through ORM methods (if managed cache is enabled).
Timeline
| Task | Timeline |
|---|---|
| 1–3 simple entities (no relations, CRUD operations) | 2–4 days |
| Complex schema: 5–10 entities with relations, validation, events | 1–2 weeks |
| Full replacement of legacy tables with a custom ORM schema including data migration | 2–4 weeks |
DataManager entities are an investment in code readability and maintainability. A year later, a developer who did not write the code will understand the database schema in an hour rather than a day. With raw SQL, there is no such guarantee.







