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 populate

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Add a primary key to the underlying table
  2. Override primaryKey() in the model to return the identifying column name(s)
  3. For view-backed read-only models, never call save()/delete(); query with find() only
  4. 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

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


AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17). Data as JSON: /api/errors/2db14fbe88dbc0e2. Report an issue: GitHub.