<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - qsqlrelationaltablemodel</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/qsqlrelationaltablemodel.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2021-06-06T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>QDataWidgetMapper with QSqlRelationalTableModel and QComboBoxes — How to correctly map relational database fields to combo boxes using QDataWidgetMapper</title><link href="https://www.pythonguis.com/faq/qdatawidgetmapper-with-relational-model-and-qcomboxes-working-of-relations/" rel="alternate"/><published>2021-06-06T09:00:00+00:00</published><updated>2021-06-06T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2021-06-06:/faq/qdatawidgetmapper-with-relational-model-and-qcomboxes-working-of-relations/</id><summary type="html">I'm trying to use &lt;code&gt;QDataWidgetMapper&lt;/code&gt; with a &lt;code&gt;QSqlRelationalTableModel&lt;/code&gt; to map foreign key fields to &lt;code&gt;QComboBox&lt;/code&gt; widgets. The regular fields (like text and numbers) work fine, but the combo boxes for my relational columns remain empty. How do I get this working correctly?</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;I'm trying to use &lt;code&gt;QDataWidgetMapper&lt;/code&gt; with a &lt;code&gt;QSqlRelationalTableModel&lt;/code&gt; to map foreign key fields to &lt;code&gt;QComboBox&lt;/code&gt; widgets. The regular fields (like text and numbers) work fine, but the combo boxes for my relational columns remain empty. How do I get this working correctly?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Getting &lt;code&gt;QDataWidgetMapper&lt;/code&gt; to play nicely with &lt;code&gt;QSqlRelationalTableModel&lt;/code&gt; and &lt;code&gt;QComboBox&lt;/code&gt; widgets can be frustrating. The Qt documentation provides snippets in C++, but a complete, working Python example is hard to find. The main source of confusion is that each combo box needs its &lt;em&gt;own&lt;/em&gt; model &amp;mdash; the relation model for that specific column &amp;mdash; rather than sharing the main table model.&lt;/p&gt;
&lt;p&gt;Let's walk through the correct approach step by step, and then put it all together in a complete example.&lt;/p&gt;
&lt;h2 id="understanding-the-problem"&gt;Understanding the problem&lt;/h2&gt;
&lt;p&gt;When you have a database table with foreign key columns (like &lt;code&gt;ssh_group_id&lt;/code&gt; referencing a &lt;code&gt;sshgroup&lt;/code&gt; table), &lt;code&gt;QSqlRelationalTableModel&lt;/code&gt; can resolve those foreign keys into human-readable values using &lt;code&gt;QSqlRelation&lt;/code&gt;. That part usually works well with a &lt;code&gt;QTableView&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The challenge comes when you want to use &lt;code&gt;QDataWidgetMapper&lt;/code&gt; to display a single record in a form, with combo boxes for the relational fields. A common mistake is setting each combo box's model to the &lt;em&gt;main&lt;/em&gt; relational table model:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;# This won't work correctly for combo boxes
self.ssh_group_combo.setModel(self.model)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The main model represents the &lt;code&gt;sshconnections&lt;/code&gt; table. What you actually want in each combo box is the list of options from the &lt;em&gt;related&lt;/em&gt; table (e.g., all groups from &lt;code&gt;sshgroup&lt;/code&gt;). &lt;code&gt;QSqlRelationalTableModel&lt;/code&gt; provides access to these through its &lt;code&gt;relationModel()&lt;/code&gt; method.&lt;/p&gt;
&lt;h2 id="setting-up-the-relational-model"&gt;Setting up the relational model&lt;/h2&gt;
&lt;p&gt;First, set up the &lt;code&gt;QSqlRelationalTableModel&lt;/code&gt; with its relations. You need to set the table and define the relations &lt;em&gt;before&lt;/em&gt; calling &lt;code&gt;select()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;self.model = QSqlRelationalTableModel(db=self.sshdb)
self.model.setTable("sshconnections")
self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)

# Define which columns hold foreign keys and where they point
ssh_group_col = self.model.fieldIndex("ssh_group_id")
ssh_key_col = self.model.fieldIndex("ssh_key_id")
ssh_config_col = self.model.fieldIndex("ssh_config_id")

self.model.setRelation(ssh_group_col, QSqlRelation("sshgroup", "id", "ssh_group_name"))
self.model.setRelation(ssh_key_col, QSqlRelation("sshkeys", "id", "sshkey_name"))
self.model.setRelation(ssh_config_col, QSqlRelation("sshconfig", "id", "ssh_config_name"))

self.model.select()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Notice that we're using the &lt;em&gt;foreign key column names&lt;/em&gt; (&lt;code&gt;ssh_group_id&lt;/code&gt;, &lt;code&gt;ssh_key_id&lt;/code&gt;, &lt;code&gt;ssh_config_id&lt;/code&gt;) when calling &lt;code&gt;fieldIndex()&lt;/code&gt;, not the display column names from the related tables. These are the actual columns in the &lt;code&gt;sshconnections&lt;/code&gt; table.&lt;/p&gt;
&lt;h2 id="configuring-the-combo-boxes-with-relation-models"&gt;Configuring the combo boxes with relation models&lt;/h2&gt;
&lt;p&gt;Here's the part that makes everything work. After calling &lt;code&gt;select()&lt;/code&gt; on the main model, you can retrieve the relation model for each foreign key column using &lt;code&gt;relationModel()&lt;/code&gt;. Each combo box gets its own relation model:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;# Each combo box gets the relation model for its specific column
self.ssh_group_combo.setModel(self.model.relationModel(ssh_group_col))
self.ssh_group_combo.setModelColumn(
    self.model.relationModel(ssh_group_col).fieldIndex("ssh_group_name")
)

self.ssh_key_combo.setModel(self.model.relationModel(ssh_key_col))
self.ssh_key_combo.setModelColumn(
    self.model.relationModel(ssh_key_col).fieldIndex("sshkey_name")
)

self.ssh_config_combo.setModel(self.model.relationModel(ssh_config_col))
self.ssh_config_combo.setModelColumn(
    self.model.relationModel(ssh_config_col).fieldIndex("ssh_config_name")
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;relationModel()&lt;/code&gt; method returns a &lt;code&gt;QSqlTableModel&lt;/code&gt; representing the related table (e.g., &lt;code&gt;sshgroup&lt;/code&gt;). By setting this as the combo box's model and pointing &lt;code&gt;setModelColumn()&lt;/code&gt; to the display field, the combo box will show all available options from that related table.&lt;/p&gt;
&lt;h2 id="setting-up-the-mapper"&gt;Setting up the mapper&lt;/h2&gt;
&lt;p&gt;With the combo boxes properly configured, you can set up the &lt;code&gt;QDataWidgetMapper&lt;/code&gt;. The &lt;code&gt;QSqlRelationalDelegate&lt;/code&gt; is essential here &amp;mdash; it tells the mapper how to handle the translation between foreign key IDs and the displayed relation values:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;self.mapper = QDataWidgetMapper()
self.mapper.setModel(self.model)
self.mapper.setItemDelegate(QSqlRelationalDelegate(self))

self.mapper.addMapping(self.ssh_id, 0)
self.mapper.addMapping(self.ssh_group_combo, ssh_group_col)
self.mapper.addMapping(self.ssh_connection_name, 2)
# ... other mappings ...
self.mapper.addMapping(self.ssh_key_combo, ssh_key_col)
self.mapper.addMapping(self.ssh_config_combo, ssh_config_col)

self.mapper.toFirst()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id="the-order-matters"&gt;The order matters&lt;/h2&gt;
&lt;p&gt;One thing to be aware of: the order in which you do things matters. Here's the sequence that works reliably:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create the &lt;code&gt;QSqlRelationalTableModel&lt;/code&gt; and set its table.&lt;/li&gt;
&lt;li&gt;Set the relations on the appropriate columns.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;select()&lt;/code&gt; on the model.&lt;/li&gt;
&lt;li&gt;Set each combo box's model to the corresponding &lt;code&gt;relationModel()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create the &lt;code&gt;QDataWidgetMapper&lt;/code&gt; and add all mappings.&lt;/li&gt;
&lt;li&gt;Navigate to a record with &lt;code&gt;toFirst()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you try to get the relation models before calling &lt;code&gt;select()&lt;/code&gt;, they may return &lt;code&gt;None&lt;/code&gt; or be unpopulated.&lt;/p&gt;
&lt;h2 id="complete-working-example"&gt;Complete working example&lt;/h2&gt;
&lt;p&gt;Here's a full, self-contained example that creates an SQLite database, populates it with sample data, and displays a form with working combo boxes for all relational fields:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import os

from PyQt5.QtCore import QSize, Qt
from PyQt5.QtSql import (
    QSqlDatabase,
    QSqlTableModel,
    QSqlRelationalTableModel,
    QSqlRelation,
    QSqlRelationalDelegate,
    QSqlQuery,
)
from PyQt5.QtWidgets import (
    QApplication,
    QComboBox,
    QDataWidgetMapper,
    QFormLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QPushButton,
    QSpinBox,
    QVBoxLayout,
    QWidget,
)

DB_PATH = "wizardwebsshv2.db"

def create_database():
    """Create the database, tables, and insert sample data."""
    db = QSqlDatabase.addDatabase("QSQLITE", "SSHCONFIG")
    db.setDatabaseName(DB_PATH)
    db.open()

    query = QSqlQuery(db=db)

    query.exec_(
        """CREATE TABLE IF NOT EXISTS sshgroup (
            id INTEGER PRIMARY KEY,
            ssh_group_name TEXT NOT NULL UNIQUE,
            ssh_group_description TEXT
        )"""
    )

    query.exec_(
        """CREATE TABLE IF NOT EXISTS sshkeys (
            id INTEGER PRIMARY KEY,
            sshkey_name VARCHAR(25) NOT NULL UNIQUE,
            sshkey_passphrase TEXT,
            sshkey_public_file TEXT,
            sshkey_private_file TEXT
        )"""
    )

    query.exec_(
        """CREATE TABLE IF NOT EXISTS sshconfig (
            id INTEGER PRIMARY KEY,
            ssh_config_name TEXT NOT NULL UNIQUE,
            ssh_config_content TEXT
        )"""
    )

    query.exec_(
        """CREATE TABLE IF NOT EXISTS sshconnections (
            id INTEGER NOT NULL PRIMARY KEY,
            ssh_group_id INTEGER DEFAULT 1,
            ssh_connection_name VARCHAR(100) NOT NULL UNIQUE,
            ssh_username VARCHAR(100),
            ssh_password VARCHAR(100),
            Host VARCHAR(100),
            HostName VARCHAR(100),
            Port INTEGER,
            ProxyCommand TEXT,
            ssh_key_id INTEGER DEFAULT 1,
            ssh_config_id INTEGER DEFAULT 1,
            FOREIGN KEY(ssh_group_id) REFERENCES sshgroup(id),
            FOREIGN KEY(ssh_key_id) REFERENCES sshkeys(id),
            FOREIGN KEY(ssh_config_id) REFERENCES sshconfig(id)
        )"""
    )

    # Insert sample data (ignore errors if already exists)
    query.exec_(
        "INSERT OR IGNORE INTO sshgroup (id, ssh_group_name, ssh_group_description) "
        "VALUES (1, 'default', 'Default SSH Group')"
    )
    query.exec_(
        "INSERT OR IGNORE INTO sshgroup (id, ssh_group_name, ssh_group_description) "
        "VALUES (2, 'production', 'Production Servers')"
    )
    query.exec_(
        "INSERT OR IGNORE INTO sshgroup (id, ssh_group_name, ssh_group_description) "
        "VALUES (3, 'staging', 'Staging Servers')"
    )

    query.exec_(
        "INSERT OR IGNORE INTO sshkeys (id, sshkey_name) VALUES (1, 'None')"
    )
    query.exec_(
        "INSERT OR IGNORE INTO sshkeys (id, sshkey_name) VALUES (2, 'my_deploy_key')"
    )
    query.exec_(
        "INSERT OR IGNORE INTO sshkeys (id, sshkey_name) VALUES (3, 'personal_key')"
    )

    query.exec_(
        "INSERT OR IGNORE INTO sshconfig (id, ssh_config_name, ssh_config_content) "
        "VALUES (1, 'default', '')"
    )
    query.exec_(
        "INSERT OR IGNORE INTO sshconfig (id, ssh_config_name, ssh_config_content) "
        "VALUES (2, 'custom_config', 'ServerAliveInterval 60')"
    )

    query.exec_(
        "INSERT OR IGNORE INTO sshconnections "
        "(ssh_group_id, ssh_connection_name, ssh_username, ssh_password, "
        "Host, HostName, Port, ProxyCommand, ssh_key_id, ssh_config_id) "
        "VALUES (1, 'web_server', 'admin', 'secret', "
        "'web', 'web.example.com', 22, '', 1, 1)"
    )
    query.exec_(
        "INSERT OR IGNORE INTO sshconnections "
        "(ssh_group_id, ssh_connection_name, ssh_username, ssh_password, "
        "Host, HostName, Port, ProxyCommand, ssh_key_id, ssh_config_id) "
        "VALUES (2, 'db_server', 'dbadmin', 'dbpass', "
        "'db', 'db.example.com', 2222, '', 2, 2)"
    )

    return db

class MainWindow(QMainWindow):
    def __init__(self, db):
        super().__init__()
        self.setWindowTitle("SSH Connection Manager")
        self.setMinimumSize(QSize(450, 400))

        self.db = db

        # Create form widgets
        self.ssh_id = QSpinBox()
        self.ssh_id.setDisabled(True)

        self.ssh_group_combo = QComboBox()
        self.ssh_connection_name = QLineEdit()
        self.ssh_username = QLineEdit()
        self.ssh_password = QLineEdit()
        self.ssh_password.setEchoMode(QLineEdit.Password)
        self.ssh_host = QLineEdit()
        self.ssh_hostname = QLineEdit()
        self.ssh_port = QSpinBox()
        self.ssh_port.setRange(1, 65535)
        self.ssh_proxy_command = QLineEdit()
        self.ssh_key_combo = QComboBox()
        self.ssh_config_combo = QComboBox()

        # Build the form layout
        form = QFormLayout()
        form.addRow(QLabel("ID"), self.ssh_id)
        form.addRow(QLabel("Group"), self.ssh_group_combo)
        form.addRow(QLabel("Connection Name"), self.ssh_connection_name)
        form.addRow(QLabel("SSH Username"), self.ssh_username)
        form.addRow(QLabel("SSH Password"), self.ssh_password)
        form.addRow(QLabel("Host"), self.ssh_host)
        form.addRow(QLabel("HostName"), self.ssh_hostname)
        form.addRow(QLabel("Port"), self.ssh_port)
        form.addRow(QLabel("Proxy Command"), self.ssh_proxy_command)
        form.addRow(QLabel("SSH Key Name"), self.ssh_key_combo)
        form.addRow(QLabel("SSH Config Name"), self.ssh_config_combo)

        # --- Set up the relational model ---
        self.model = QSqlRelationalTableModel(db=self.db)
        self.model.setTable("sshconnections")
        self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)

        # Get the column indices for the foreign key columns
        ssh_group_col = self.model.fieldIndex("ssh_group_id")
        ssh_key_col = self.model.fieldIndex("ssh_key_id")
        ssh_config_col = self.model.fieldIndex("ssh_config_id")

        # Define the relations
        self.model.setRelation(
            ssh_group_col,
            QSqlRelation("sshgroup", "id", "ssh_group_name"),
        )
        self.model.setRelation(
            ssh_key_col,
            QSqlRelation("sshkeys", "id", "sshkey_name"),
        )
        self.model.setRelation(
            ssh_config_col,
            QSqlRelation("sshconfig", "id", "ssh_config_name"),
        )

        # Populate the model &amp;mdash; this must happen before accessing relationModel()
        self.model.select()

        # --- Configure combo boxes with their relation models ---
        # Each combo box gets the model for its own related table
        group_relation_model = self.model.relationModel(ssh_group_col)
        self.ssh_group_combo.setModel(group_relation_model)
        self.ssh_group_combo.setModelColumn(
            group_relation_model.fieldIndex("ssh_group_name")
        )

        key_relation_model = self.model.relationModel(ssh_key_col)
        self.ssh_key_combo.setModel(key_relation_model)
        self.ssh_key_combo.setModelColumn(
            key_relation_model.fieldIndex("sshkey_name")
        )

        config_relation_model = self.model.relationModel(ssh_config_col)
        self.ssh_config_combo.setModel(config_relation_model)
        self.ssh_config_combo.setModelColumn(
            config_relation_model.fieldIndex("ssh_config_name")
        )

        # --- Set up the data widget mapper ---
        self.mapper = QDataWidgetMapper()
        self.mapper.setModel(self.model)
        self.mapper.setItemDelegate(QSqlRelationalDelegate(self))

        self.mapper.addMapping(self.ssh_id, 0)
        self.mapper.addMapping(self.ssh_group_combo, ssh_group_col)
        self.mapper.addMapping(self.ssh_connection_name, 2)
        self.mapper.addMapping(self.ssh_username, 3)
        self.mapper.addMapping(self.ssh_password, 4)
        self.mapper.addMapping(self.ssh_host, 5)
        self.mapper.addMapping(self.ssh_hostname, 6)
        self.mapper.addMapping(self.ssh_port, 7)
        self.mapper.addMapping(self.ssh_proxy_command, 8)
        self.mapper.addMapping(self.ssh_key_combo, ssh_key_col)
        self.mapper.addMapping(self.ssh_config_combo, ssh_config_col)

        self.mapper.toFirst()

        # --- Navigation and action buttons ---
        controls = QHBoxLayout()

        prev_btn = QPushButton("Previous")
        prev_btn.clicked.connect(self.mapper.toPrevious)

        next_btn = QPushButton("Next")
        next_btn.clicked.connect(self.mapper.toNext)

        save_btn = QPushButton("Save Changes")
        save_btn.clicked.connect(self.save_changes)

        add_btn = QPushButton("Add")
        add_btn.clicked.connect(self.add_record)

        controls.addWidget(prev_btn)
        controls.addWidget(next_btn)
        controls.addWidget(save_btn)
        controls.addWidget(add_btn)

        # Status label to show current record
        self.status_label = QLabel()
        self.mapper.currentIndexChanged.connect(self.update_status)
        self.update_status()

        # Assemble the main layout
        layout = QVBoxLayout()
        layout.addLayout(form)
        layout.addLayout(controls)
        layout.addWidget(self.status_label)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

    def update_status(self):
        current = self.mapper.currentIndex() + 1
        total = self.model.rowCount()
        self.status_label.setText(f"Record {current} of {total}")

    def save_changes(self):
        self.mapper.submit()
        if not self.model.submitAll():
            print("Error saving:", self.model.lastError().text())
        else:
            print("Changes saved successfully.")

    def add_record(self):
        row = self.model.rowCount()
        self.model.insertRow(row)
        self.mapper.setCurrentIndex(row)

app = QApplication(sys.argv)
db = create_database()
window = MainWindow(db)
window.show()
app.exec_()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt5 see my book, &lt;a href="https://www.martinfitzpatrick.com/pyqt5-book/"&gt;Create GUI Applications with Python &amp; Qt5.&lt;/a&gt;&lt;/p&gt;
            </content><category term="pyqt5"/><category term="sql"/><category term="qdatawidgetmapper"/><category term="qcombobox"/><category term="qsqlrelationaltablemodel"/><category term="python"/><category term="qt"/><category term="qt5"/><category term="databases"/></entry></feed>