лайфхак QSqlRelationalTableModel

QSqlRelationalTableModel полезное развитие класса QSqlTableModel.

Есть одна глобальная проблема у класса QSqlRelationalTableModel в том, что он не предоставляет значение id поля внешней связи. Предоставляет он только текстовую замену.

Но на самом деле все можно исправить, мы давно сделали свой вариант развития модели данных Qt, смотрите новые возможности модели данных Qt Sql.

Давайте посмотрим реализацию QSqlRelationalTableModel::data(..).


QVariant QSqlRelationalTableModel::data(const QModelIndex &index, int role) const
{
    Q_D(const QSqlRelationalTableModel);

    if (role == Qt::DisplayRole && index.column() >= 0 && index.column() < d->relations.count() &&
            d->relations.value(index.column()).isValid()) {
        QRelation &relation = d->relations[index.column()];
        if (!relation.isDictionaryInitialized())
            relation.populateDictionary();

        //only perform a dictionary lookup for the display value
        //when the value at index has been changed or added.
        //At an unmodified index, the underlying model will
        //already have the correct display value.
        QVariant v;
        switch (d->strategy) {
            case OnFieldChange:
                break;
            case OnRowChange:
                if ((index.row() == d->editIndex || index.row() == d->insertIndex)
                    && d->editBuffer.isGenerated(index.column()))
                    v = d->editBuffer.value(index.column());
                break;
            case OnManualSubmit:
                const QSqlTableModelPrivate::ModifiedRow row = d->cache.value(index.row());
                if (row.op != QSqlTableModelPrivate::None && row.rec.isGenerated(index.column()))
                    v = row.rec.value(index.column());
                break;
        }
        if (v.isValid())
            return relation.dictionary[v.toString()];
    }
    return QSqlTableModel::data(index, role);
}

Может показаться, что id внешней связи можно как-то вытащить из relation.dictionary, это по сути ключ в этом массиве. Но во первых это только если этот ключ есть в кэше, во временном хранилище, это до submitAll.

Это в случае когда у нас например стратегия OnRowChange и строка еще не записана в базу данных (не сделан submit), то значение ключа поля еще в каком-то кэше editBuffer.

На самом деле вообще не понятно зачем этот dictionary нужен, когда, кем и для чего он заполняется.

В обычном варианте мы переходим на return QSqlTableModel::data(index, role);

А это значит, что данные предоставит нам родитель, а именно QSqlTableModel.

И может показаться, что будет банальный select, который сделает банальную QSqlQuery выборку из базы. Но есть нюанс...

SELECT purchases."id", purchases."productName", purchases."price", purchases."qty", purchases."sum", purchases."additionalAttribute", purchases."exciseAmount", purchases."addInfo", relTblAl_26.name FROM purchases
LEFT JOIN suppliers relTblAl_26 ON purchases."suppliers" = relTblAl_26.id

Тут добавляется от QSqlRelationalTableModel LEFT JOIN.

Немного подправим исходники Qt 4.8.1.

QString QSqlRelationalTableModel::selectStatement() const
{
    Q_D(const QSqlRelationalTableModel);
    QString query;

    if (tableName().isEmpty())
        return query;
    if (d->relations.isEmpty())
        return QSqlTableModel::selectStatement();

    QString tList;
    QString fList;
    QString where;

    QSqlRecord rec = d->baseRec;
    QStringList tables;
    const QRelation nullRelation;

    // Count how many times each field name occurs in the record
    QHash<QString, int> fieldNames;
    QStringList fieldList;

    for (int i = 0; i < rec.count();   i)
    {
        QSqlRelation relation = d->relations.value(i, nullRelation).rel;
        QString name;
        if (relation.isValid())
        {
            // Count the display column name, not the original foreign key
            name = relation.displayColumn();
            if (d->db.driver()->isIdentifierEscaped(name, QSqlDriver::FieldName))
                name = d->db.driver()->stripDelimiters(name, QSqlDriver::FieldName);

            QSqlRecord rec = database().record(relation.tableName());
            for (int i = 0; i < rec.count();   i) {
                if (name.compare(rec.fieldName(i), Qt::CaseInsensitive) == 0) {
                    name = rec.fieldName(i);
                    break;
                }
            }
        }
        else
            name = rec.fieldName(i);
        fieldNames.insert(name, fieldNames.value(name, 0)   1);
        fieldList.append(name);
    }

    //!! -------------- my --------------------------
    QString my;

    for (int i = 0; i < rec.count();   i)
    {
        QSqlRelation relation = d->relations.value(i, nullRelation).rel;

        if (relation.isValid())
        {
            QString relTableAlias = QString::fromLatin1("relTblAl_%1").arg(i);

            if (!fList.isEmpty())
                fList.append(QLatin1String(", "));

            fList.append(d->relationField(relTableAlias , relation.displayColumn()));

            // If there are duplicate field names they must be aliased
            if (fieldNames.value(fieldList[i]) > 1)
            {
                QString relTableName = relation.tableName().section(QChar::fromLatin1('.'), -1, -1);

                if (d->db.driver()->isIdentifierEscaped(relTableName, QSqlDriver::TableName))
                    relTableName = d->db.driver()->stripDelimiters(relTableName, QSqlDriver::TableName);

                QString displayColumn = relation.displayColumn();

                if (d->db.driver()->isIdentifierEscaped(displayColumn, QSqlDriver::FieldName))
                    displayColumn = d->db.driver()->stripDelimiters(displayColumn, QSqlDriver::FieldName);
                fList.append(QString::fromLatin1(" AS %1_%2_%3").arg(relTableName).arg(displayColumn).arg(fieldNames.value(fieldList[i])));
                fieldNames.insert(fieldList[i], fieldNames.value(fieldList[i])-1);
            }

            //!! -------------- my --------------------------
            my.append(QLatin1String(", "));
            my.append(relTableAlias);
            my.append(QLatin1String("."));
            my.append(relation.indexColumn());
            my.append(QLatin1String(" as "));
            my.append(relation.tableName());
            my.append(QLatin1String("_"));
            my.append(relation.indexColumn());
            my.append(QLatin1String("_"));
            // --------------------------------------------

            if (d->joinMode == QSqlRelationalTableModel::InnerJoin)
            {
                // this needs fixing!! the below if is borken.
                // Use LeftJoin mode if you want correct behavior
                tables.append(relation.tableName().append(QLatin1Char(' ')).append(relTableAlias));

                if(!where.isEmpty())
                    where.append(QLatin1String(" AND "));

                where.append(d->relationField(tableName(), d->db.driver()->escapeIdentifier(rec.fieldName(i), QSqlDriver::FieldName)));
                where.append(QLatin1String(" = "));
                where.append(d->relationField(relTableAlias, relation.indexColumn()));
            } else {
                tables.append(QLatin1String(" LEFT JOIN"));
                tables.append(relation.tableName().append(QLatin1Char(' ')).append(relTableAlias));
                tables.append(QLatin1String("ON"));

                QString clause;
                clause.append(d->relationField(tableName(), d->db.driver()->escapeIdentifier(rec.fieldName(i), QSqlDriver::FieldName)));
                clause.append(QLatin1String(" = "));
                clause.append(d->relationField(relTableAlias, relation.indexColumn()));

                tables.append(clause);
            }
        }
        else
        {
            if (!fList.isEmpty())
                fList.append(QLatin1String(", "));
            fList.append(d->relationField(tableName(), d->db.driver()->escapeIdentifier(rec.fieldName(i), QSqlDriver::FieldName)));
        }
    }

    if (d->joinMode == QSqlRelationalTableModel::InnerJoin && !tables.isEmpty())
    {
        tList.append(tables.join(QLatin1String(", ")));
        if(!tList.isEmpty())
            tList.prepend(QLatin1String(", "));
    } else
        tList.append(tables.join(QLatin1String(" ")));

    if (fList.isEmpty())
        return query;

    tList.prepend(tableName());
    query.append(QLatin1String("SELECT "));
    
    //!! --------------- my -------------------------
    //!!query.append(fList).append(QLatin1String(" FROM ")).append(tList);
    query.append(fList).append(my).append(QLatin1String(" FROM ")).append(tList);

    if (d->joinMode == QSqlRelationalTableModel::InnerJoin) {
        qAppendWhereClause(query, where, filter());
    } else if (!filter().isEmpty()) {
        query.append(QLatin1String(" WHERE ("));
        query.append(filter());
        query.append(QLatin1String(")"));
    }

    QString orderBy = orderByClause();
    if (!orderBy.isEmpty())
        query.append(QLatin1Char(' ')).append(orderBy);

    return query;
}

В результате Qt будет теперь выполнять запрос по другому:

SELECT purchases."id", purchases."productName", purchases."price", purchases."qty", purchases."sum", purchases."additionalAttribute", purchases."exciseAmount", purchases."addInfo", relTblAl_26.name,
relTblAl_26.id as suppliers_id FROM purchases
LEFT JOIN suppliers relTblAl_26 ON purchases."suppliers" = relTblAl_26.id

У нас в выдаче появится дополнительное поле с ключом id связи.

Какие это плюсы имеет понятно. Мы наконец-то имеем текстовую замену внешней связи и сам id внешней связи в нашей строке. Само поле id конечно лучше скрывать, но это не проблема.

Какие минусы это еще вопрос.
Если не делать setData этому полю id, то проблем не должно возникать. Особенно если после setData делать setGenerated(false) этому полю, чтобы потом никто не захотел записать его обратно в базу.

Решил сделать доброе дело давече , поделился на habr.com данным материалом - отказали в размещении. Правда это была первая публикация. Но отказали без объяснение причины, отписав объяснять не обязаны.

Вот так иногда и чувствуешь себя идиотом...

Есть еще один способ решить проблему - это добавить в исходники Qt свой вариант класса от QSqlTableModel. Все по фен шую в стиле самого Qt (с наследованием приватной части класса QSqlTableModelPrivate и т.д .как расширить функционал QSqlTableModel.