yiisoft/yii2 · critical · yii\db\Exception
{class} does not have a primary key. You should either defin
Error message
{class} does not have a primary key. You should either define a primary key for the corresponding table or override the primaryKey() method. What it means
getOldPrimaryKey() returns the key value(s) the record was loaded with, reading static::primaryKey() first; an empty key list means the model cannot identify its row, so a plain Exception (not InvalidConfigException) is thrown instructing you to define a table primary key or override primaryKey(). updateInternal()/deleteInternal() call it with $asArray = true, so save() and delete() on a keyless model surface here.
Source
Thrown at framework/db/BaseActiveRecord.php:1171
/**
* Returns the old primary key value(s).
* This refers to the primary key value that is populated into the record
* after executing a find method (e.g. find(), findOne()).
* The value remains unchanged even if the primary key attribute is manually assigned with a different value.
* @param bool $asArray whether to return the primary key value as an array. If `true`,
* the return value will be an array with column name as key and column value as value.
* If this is `false` (default), a scalar value will be returned for non-composite primary key.
* @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
* is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if
* the key value is null).
* @throws Exception if the AR model does not have a primary key
*/
public function getOldPrimaryKey($asArray = false)
{
$keys = static::primaryKey();
if (empty($keys)) {
throw new Exception(get_class($this) . ' does not have a primary key. You should either define a primary key for the corresponding table or override the primaryKey() method.');
}
if (!$asArray && count($keys) === 1) {
return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
}
$values = [];
foreach ($keys as $name) {
$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
}
return $values;
}
/**
* Populates an active record object using a row of data from the database/storage.
*
* This is an internal method meant to be called to create active record objects after
* fetching data from the database. It is mainly used by [[ActiveQuery]] to populateView on GitHub (pinned to 66f00d18a2)
Solutions
- Add a primary key to the underlying table
- Override primaryKey() in the model to return the identifying column name(s)
- For view-backed read-only models, never call save()/delete(); query with find() only
- Flush the schema cache after primary-key changes in the database
Example fix
// before
class ReportRow extends \yii\db\ActiveRecord
{
public static function tableName() { return 'vw_report'; }
}
ReportRow::findOne(5)->delete(); // throws: no primary key
// after
class ReportRow extends \yii\db\ActiveRecord
{
public static function tableName() { return 'vw_report'; }
public static function primaryKey() { return ['report_id']; }
} Defensive patterns
Strategy: validation
Validate before calling
if (empty($model::primaryKey())) {
throw new LogicException(get_class($model) . ' has no primary key; save()/delete() are unavailable.');
}
$model->delete(); Try / catch
try {
$model->delete();
} catch (yii\db\Exception $e) {
if (strpos($e->getMessage(), 'does not have a primary key') !== false) {
throw new RuntimeException('Model ' . get_class($model) . ' cannot be persisted: define primaryKey().', 0, $e);
}
throw $e;
} Prevention
- Define a primary key (table constraint or primaryKey() override) before enabling writes on any AR class
- Mark view-backed models read-only and exclude them from save/delete code paths
- Flush schema cache after key changes so primaryKey() reflects reality
When it happens
Trigger: $model->save() on an existing record or $model->delete() when primaryKey() returns [] — keyless table/view, override removed, or schema cache stale after the key was dropped; calling getOldPrimaryKey() directly for logging or comparison.
Common situations: AR over database views; legacy tables without a PRIMARY KEY; custom AR subclasses that forgot the primaryKey() override; cached schema metadata after DDL changes.
Related errors
- Primary key of '{$class}' can not be empty.
- "{}" must have a primary key.
- "{class}" must have a primary key.
- Key "{}" is not a column name and can not be used as a filte
- The table does not exist: {table}
AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17).
Data as JSON: /api/errors/2db14fbe88dbc0e2.
Report an issue: GitHub.