QSqlRelationalTableModel and Proxy Models

Niclas BlombergNiclas Blomberg
10 min read

Introduction

This article outlines a problem I faced when working on one of my side projects. The project is a budget app written in Qt and C++. Since I’m still learning I don’t dare to claim that the solution here is the best - or even that it is correct. If you would have approached the solution in another way, I’d love to hear about your solution!

The article is part of my learning C++ journey, where I document my own learning doing both simple and crazy things. Ultimately the solution here came from reading the source code of Qt, which highlights the importance of reading the code for your favorite frameworks when learning, which in turn is made possible by open source or at leas source-available projects. So a big shout out to everyone who make their source available!

Models and Views in Qt

Qt adopts a slightly different approach to the Model-View-Controller pattern than you might be used to if you are coming from other languages or frameworks. Here we don’t have any controllers, but models, views and delegates. The responsibilities corresponding to the controller in other frameworks have been distributed among these classes.

  • A model is an abstraction that handles the data access. The data can come from a file, an sql database or some external sensors on your toaster - by hiding these details in the model, the rest of the application doesn’t need to care.

  • A view is responsible for displaying the data provided by the model. This is not too different from other frameworks.

  • A delegate is invoked when it’s time to display or edit some data from a model in a view.

Sql Table Model

QSqlTableModel is a model that represents data from a table in an sql database. If you give this model a database and hook it up to a table view, the columns and rows of that table will be displayed without minimal effort. Below is a minimal example of how this is set up, adapted from the Qt documentation:

QSqlTableModel *model = new QSqlTableModel;
model->setTable("employee");
model->select();

// Give some human readable names to the columns
model->setHeaderData(0, Qt::Horizontal, tr("Name"));
model->setHeaderData(1, Qt::Horizontal, tr("Salary"));

// Hook up to the view
QTableView *view = new QTableView;
view->setModel(model);
view->hideColumn(0); // don't show the ID
view->show();

This is really all you need to display the data in a table in Qt!

Sql Relational Table Model

We can also display data from related tables using the QSqlRelationalTableModel. This is a class that inherits from the sql table model above, but with the added ability to perform joins and display foreign keys with more meaningful names.

We can tell the model to display foreign keys like this:

model->setRelation(categoryColumn, QSqlRelation("categories", "id", "category"));

Now the foreign keys in the category column will be replaced with meaningful values from the categories table. An example from my own project may look like this:

Here the category column is populated by the QSqlRelation - if I switch to a “normal” table model we only see the foreign key values:

Proxy Models

If you want to, you can build your own model and implement filtering and sorting etc directly in that. If you are using a database in the background it might even be nice to translate the state of the model directly to queries for efficiency. However that would mean that you would need to change the source code of your model each time you want to add a filter to your application, for instance. And what do you do if you need to display the same data in different places, but with different filters? Maintaining multiple classes for what is basically the “same” model doesn’t sound like a good engineering solution.

It is for this purpose that Qt gives us proxy models. A proxy model is a kind of model (implements the interface of QAbstractItemModel) but it is not intended to hold any data. Instead it takes in data from a model, transforms it somehow, and then passes on the transformed data. To a view, the proxy model is indistinguishable from a “real” model:

QTreeView* treeView = new QTreeView;
MyItemModel* sourceModel = new MyItemModel(this);
QSortFilterProxyModel* proxyModel = new QSortFilterProxyModel(this);

proxyModel->setSourceModel(sourceModel);
treeView->setModel(proxyModel);

(source: https://doc.qt.io/qt-6/qsortfilterproxymodel.html#details)

The QSortFilterProxyModel is a proxy model provided by Qt that allows sorting and filtering. It contains some pre-built filters but can be subclassed to allow filtering adapted to your projects particular needs.

But it doesn’t end there, the real power of this construct comes from composability. If you have one proxy model that filters on a date (for instance) and one proxy model that removes rows where some column is empty, you can combine these into one “stack” of models:

MyItemModel* sourceModel = new MyItemModel(this);
QSortFilterProxyModel* dateFilterModel = new QSortFilterProxyModel(this);
QSortFilterProxyModel* nullFilterModel = new QSortFilterProxyModel(this);

dateFilterModel->setSourceModel(sourceModel);
nullFilterModel->setSourceModel(dateFilterModel);

QTreeView* treeView = new QTreeView;
treeView->setModel(nullFilterModel);

Since a proxy model is also a model, we can feed a proxy model into another one, and then send that composition to the view instead. Now we see that if we have one model, but need to display it in multiple places with different filters, all we need to do is hook up the view with different proxy model stacks and then we’re good to go - no need to maintain multiple model classes for the same thing. This also leads to smaller and more general proxy models that can be reused in different situations.

Delegates

The final piece of the Model-View puzzle in Qt is the delegate. The delegate provides ways to display and edit the data in a model. For instance, in my own project I have a delegate for editing dates. Dates are just strings when coming from the sql table model, but using a delegate that forces the user to input dates via a date input component ensures that the input is valid.

To achieve this, the quickest (meaning less code to write) way is to subclass QStyledItemDelegate (I think). When the user double clicks a cell the corresponding delegate’s createEditor member function is invoked. This gives us a chance to create a QDateEdit and return to the view:

QWidget* LS::LSDateFromStringDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    QDateEdit* editor = new QDateEdit(parent);
    editor->setCalendarPopup(true);

    QString format = m_settings->value(application::ApplicationContext::LocaleDateFormatKey).toString();
    editor->setDisplayFormat(format);
    return editor;
}

I wanted to show a screenshot here, but I ran into what I think is the bug reported here - when you open the delegate editor, it’s impossible to take a screenshot (at least on Ubuntu, which I’m using at the time of writing).

The Problem

So now we have some basic idea of what a model and a view is in Qt. We also know what a delegate is, and that there are no controllers in the Model-View-Delegate framework. Lastly we have seen that advanced behavior can be achieved in a SOLID and modular way by building small but focused proxy models, and stack these on top of each other.

The problem I encountered was when using a QSqlRelationalTableModel. This model is accompanied by a delegate called QSqlRelationalDelegate. This we should already understand the need for, since the relational model displays different data in the columns you choose (e.g., a string with a human readable name instead of the foreign key). If we were to attempt to edit a row using this setup as-is, we would end up in a situation where the model is trying to save the wrong value to a foreign key column (e.g., “pets” instead of the id in the categories table, which might be 97).

The relational delegate will create a combo box with the human readable names to choose from, and then do the right thing when it’s time to actually save the foreign key. This I used for my category column that you can see in the screenshots above.

In the beginning, this worked really well without any intervention on my part. The problem began when the relational table model was later hidden under a stack of proxy models. Now suddenly the relational delegate is not doing what it’s supposed to do. What is going on here?

The Solution

After reading a bit about this problem on Stack Overflow and Qt forums, I couldn’t really find a satisfactory solution. There was this article that attempts to construct a subclass of the relational delegate. While it did work for me as well, after some tweaking, this solution was rather less than satisfactory (for me), the reason being that the code in this article is a copy-paste + modification of the actual source code of QSqlRelationalDelegate.

Let’s take a look at what the problem with this class is. When the delegate wants to save the data, the member function setModelData is invoked. The purpose of this function is to “[get] data from the editor widget and [store] it in the specified model at the item index.“ (source: Qt documentation). The implementation looks like this:

void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
{
    if (!index.isValid())
        return;

    QSqlRelationalTableModel *sqlModel = qobject_cast<QSqlRelationalTableModel *>(model);
    QSqlTableModel *childModel = sqlModel ? sqlModel->relationModel(index.column()) : nullptr;
    QComboBox *combo = qobject_cast<QComboBox *>(editor);
    if (!sqlModel || !childModel || !combo) {
        QStyledItemDelegate::setModelData(editor, model, index);
        return;
    }
    const QSqlDriver *const driver = childModel->database().driver();

    int currentItem = combo->currentIndex();
    int childColIndex = fieldIndex(childModel, driver,
                                   sqlModel->relation(index.column()).displayColumn());
    int childEditIndex = fieldIndex(childModel, driver,
                                    sqlModel->relation(index.column()).indexColumn());
    sqlModel->setData(index,
            childModel->data(childModel->index(currentItem, childColIndex), Qt::DisplayRole),
            Qt::DisplayRole);
    sqlModel->setData(index,
            childModel->data(childModel->index(currentItem, childEditIndex), Qt::EditRole),
            Qt::EditRole);
}

(source: Gihub/qtbase)

So after validating the model index, we (the widget cast is not important here):

  1. Cast the model to QSqlRelationalTableModel,

  2. If the cast succeeded we get the model that represents the relation that we join with in the relational table model,

  3. Check that the steps in (1) and (2) succeeded.

  4. If we pass the check in (3), we continue with the operation.

The problem that I had is already visible here - since the view in my case is holding a pointer to a QAbstractProxyModel that is not a subclass of QSqlRelationalTableModel, the first cast will fail. Always.This means that the relational delegate assumes that it will always operate directly on a relational table model (or subclass thereof).

We can, however, solve this in a subclass of our own by realizing this, and compensating by checking if the model is a proxy model. If so, we will pick out the underlying model and call the member function in the base class:

auto const* proxy = qobject_cast<QAbstractProxyModel const*>(index.model())

If proxy is not null we fetch the source model of the proxy (the model below it in the stack):

auto* underlyingModel = proxy->sourceModel();

We must also map the indices between the models:

auto const mappedIndex = proxy->mapToSource(index);

A detailed discussion of model indices will require a much deeper discussion of models in Qt, so it is beyond the scope of this article - and perhaps beyond the knowledge of the author as well :)

Then we can invoke the member function in the superclass:

QSqlRelationalDelegate::setModelData(editor, model, index);

Wait a Minute …

If you know Qt (or have been paying attention to my description above) you can perhaps already see that there is a potential problem here, namely that proxy models don’t necessarily come alone - they come in stacks. This means that the underlyingModel above may actually point to yet another proxy model!

How do we fix this? It’s actually quite simple, we just keep peeling off the proxy models until we hit the proper model. To make the code simpler, I decided to do this recursively (I won’t use deep enough stacks that it will be a problem) but we could also attempt to unwind the recursion and do it in a loop.

Here is the final version of my solution:

void LSRelationalProxyDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex &index) const
{
    if(auto const* proxy = qobject_cast<QAbstractProxyModel const*>(index.model()))
    {
        auto* underlyingModel = proxy->sourceModel();
        auto const mappedIndex = proxy->mapToSource(index);
        setModelData(editor, underlyingModel, mappedIndex);
    }

    QSqlRelationalDelegate::setModelData(editor, model, index);
}

So we are recursively calling ourselves until model is no longer pointing to a proxy model, each time we peel of one layer of proxies, and also take care to map the indices so that the models can index the correct data.

I put my solution in a class called LSRelationalProxyDelegate, and the full source can be found here. I don’t claim that this is the “best” solution (or even that it is correct) but it’s my attempt so far, and since I’m still learning Qt and C++, I would love to hear any feedback you might have, if any!

This solution is better than the article I found (for me) in that we don’t need to duplicate the code in QSqlRelationalDelegate::setModelData to make it work. If the delegate is by any chance registered (wrongly) on a different model than an sql relational model, then the call to the base class will do some casts and return without doing nothing. If you would rather have a crash or error message in this case, I’m sure you can add it yourself :)

References

Qt Docs

Gihub

0
Subscribe to my newsletter

Read articles from Niclas Blomberg directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Niclas Blomberg
Niclas Blomberg

Dotnet developer, C++ learner, Unreal Engine lover