Inserting records
How to insert one row, insert thousands efficiently, and read back the id the database generated — plus the two rules that decide what actually gets written.
Insert a single row
insert() takes a table name and an array (or object) of column → value pairs.
It returns the generated primary key.
$id = $cdo->insert('users', [
'name' => 'Alice',
'email' => 'alice@example.com',
]);The returned id is the value of the first key of the entity you passed,
fetched via RETURNING on PostgreSQL/MariaDB or lastInsertId() on MySQL,
SQLite or Oracle. See
Driver detection for why the two paths exist.
The primary key is the first key
insert() derives the returned column from array_key_first() of your entity —
before NULLs are stripped. Put your primary-key column first (even as
'id' => null) so the right value comes back. A null/absent auto-id column
still returns the generated id via the driver.
Two rules for what gets written
nullvalues are dropped. Any column whose value isnullis removed from theINSERT, so the column takes its database default or auto-generated value.- Objects are flattened. An object entity is converted with
get_object_vars(), so its public properties become columns.
$user = new stdClass();
$user->name = 'Bob';
$user->email = 'bob@example.com';
$user->note = null; // dropped — column keeps its default
$id = $cdo->insert('users', $user);Insert a batch
insertBatch() writes many rows with far fewer round-trips. It automatically
splits the input into chunks and emits one multi-row INSERT per chunk.
$users = [
['name' => 'Alice', 'email' => 'alice@example.com'],
['name' => 'Bob', 'email' => 'bob@example.com'],
// ... thousands more
];
$inserted = $cdo->insertBatch('users', $users); // default chunk size 1000
$inserted = $cdo->insertBatch('users', $users, chunkSize: 500);insertBatch() returns an int — the total number of rows inserted. It is a
bulk write, not an id fetch. An empty input is a no-op and returns 0.
The second argument takes any iterable, so the rows need not sit in an array. A
generator yields them one at a time and only the current chunk lives in memory —
this is how files too large to load are imported:
$inserted = $cdo->insertBatch('users', (function () use ($csv) {
while (($line = fgetcsv($csv)) !== false) {
yield ['name' => $line[0], 'email' => $line[1]];
}
})());The details are in Batches and chunking.
Rows are grouped by shape
You don’t need to normalise your rows first. Before chunking, rows are grouped by their column signature (the set of non-null columns), so rows of different shapes are batched into separate statements that always line up. One consequence: rows are re-ordered by group, so don’t rely on auto-increment ids following the input array order when rows differ in shape. Details in Batch & chunking.
Choosing a chunk size
chunkSize caps how many rows go into one statement, which keeps you under the
database’s bound-parameter and packet limits. Smaller chunks use less memory per
statement; larger chunks mean fewer round-trips. The default of 1000 is a safe
starting point — lower it if rows are very wide (many columns).
Handling duplicate keys
A plain insert into a row that violates a unique constraint throws a
CDOException. If you instead want insert-or-update (or insert-or-ignore),
use upsert / upsertBatch.
Related
- Upserts — insert-or-update on conflict
- Batch & chunking — how multi-row inserts are built
- CDO API —
insert/insertBatchsignatures - Parameter binding — how each value is typed