Think of your module's data like a filing cabinet you're installing in someone else's office. You can't just start throwing folders in — Drupal needs to know exactly how many drawers this cabinet has, how each folder is labeled, and what kind of paper goes in each one, before a single byte gets written. That's what hook_schema() is: a blueprint, declared in your module's .install file, that tells Drupal precisely how to build a custom database table the moment your module is installed.
What you'll learn in this lesson
- Where table definitions live — the
.installfile, not your.modulefile - How
hook_schema()describes columns, types, and keys as a plain PHP array - How
hook_install()can seed a brand-new table with starting data - Why every column needs an explicit type, length, and "not null" default
Declaring the table with hook_schema()
Here's a real, working example from Drupal's own Examples project — a table that stores simple person records:
function dbtng_example_schema() {
$schema['dbtng_example'] = [
'description' => 'Stores example person entries for demonstration purposes.',
'fields' => [
'pid' => [
'type' => 'serial',
'not null' => TRUE,
'description' => 'Primary Key: Unique person ID.',
],
'uid' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => "Creator user's {users}.uid",
],
'name' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'surname' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'age' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
],
],
'primary key' => ['pid'],
'indexes' => [
'name' => ['name'],
'surname' => ['surname'],
'age' => ['age'],
],
];
return $schema;
}
The function name is the naming convention itself: {module_name}_schema(). Drupal scans every enabled module for a function matching this pattern the moment the module is installed, and builds the real SQL CREATE TABLE statement from this array for you — you never write raw DDL by hand, and the same array works identically across MySQL, MariaDB, PostgreSQL, and SQLite.
Why pid is a serial and everything else has a strict type
serial is Drupal's schema API shorthand for an auto-incrementing integer primary key — exactly what you'd reach for as a row's unique ID. Every other column declares a concrete type (int, varchar) plus a length where relevant and a default — this isn't busywork, it's what lets the Schema API generate a table that behaves identically and safely no matter which database backend the site actually runs on.
Seeding data with hook_install()
function dbtng_example_install() {
$entries = [
['name' => 'John', 'surname' => 'Doe', 'age' => 0],
['name' => 'John', 'surname' => 'Roe', 'age' => 100, 'uid' => 1],
];
$connection = \Drupal::database();
foreach ($entries as $entry) {
$connection->insert('dbtng_example')->fields($entry)->execute();
}
}
hook_install() runs exactly once, the moment your module is enabled — right after Drupal has already created the table from your schema. This is the natural place to seed default or demo rows, exactly as this example does with two starter "person" records so the module has something to show immediately.
hook_schema() after your module is already installed does nothing on its own. Drupal only reads the schema once, at install time. To alter an existing table's structure, you need a numbered hook_update_N() function that runs the actual ALTER TABLE logic — the schema array is the blueprint for a fresh install, not a live sync target.Quick check: if you added a new column to the
fieldsarray inhook_schema()and randrush cron a site where this module is already installed, would the new column appear in the database? (No — the table already exists, sohook_schema()is never re-read for it. You'd need ahook_update_N()that callsdb_add_field()or the equivalent Schema API method to actually add it.)
Key takeaways
- Table definitions live in
hook_schema()inside your module's.installfile, named{module_name}_schema(). - Every column needs an explicit
type, and usually alengthanddefault— this is what makes the same schema portable across database engines. serialis the standard type for an auto-incrementing primary key.hook_install()runs once at install time and is the right place to seed starter data via\Drupal::database()->insert().- Schema changes to an already-installed module require a
hook_update_N(), not a change tohook_schema()alone.
Coming up next
You've got a real table with real rows in it. Next, you'll learn how to actually get data back out — using Database::getConnection() and the query builder to read those rows safely and efficiently.