ormtable.h 14.9 KB
/* ****************************************************************************
 * Copyright 2019 Open Systems Development BV                                 *
 *                                                                            *
 * Permission is hereby granted, free of charge, to any person obtaining a    *
 * copy of this software and associated documentation files (the "Software"), *
 * to deal in the Software without restriction, including without limitation  *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,   *
 * and/or sell copies of the Software, and to permit persons to whom the      *
 * Software is furnished to do so, subject to the following conditions:       *
 *                                                                            *
 * The above copyright notice and this permission notice shall be included in *
 * all copies or substantial portions of the Software.                        *
 *                                                                            *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    *
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        *
 * DEALINGS IN THE SOFTWARE.                                                  *
 * ***************************************************************************/
#ifndef OSDEV_COMPONENTS_ORMTABLE_H
#define OSDEV_COMPONENTS_ORMTABLE_H

#include <QObject>
#include <QStringList>
#include <QSqlRelationalTableModel>
#include <QSortFilterProxyModel>
#include <QSqlDatabase>
#include <QSqlRecord>
#include <QHash>

#include "dbconnector.h"
#include "ormreldata.h"
#include "ormtablerow.h"
#include "dbrelation.h"
#include "conversionutils.h"
#include "timeline.h"

namespace osdev {
namespace components {

/*!
 * \brief   This class is used as a TransActionsscopeGuard
 *          It takes a database connection at its constructor
 *          and calls the rollBack method in its destructor.
 */
class OrmTransGuard
{
public:
    OrmTransGuard()
        : m_db(nullptr)
    {
    }

    explicit OrmTransGuard( DbConnector& _db )
        : m_db( &_db )
    {
    }

    ~OrmTransGuard()
    {
        if (nullptr != m_db)
        {
            m_db->transactionRollback();
        }
    }

    OrmTransGuard(const OrmTransGuard&) = delete;
    OrmTransGuard& operator=(const OrmTransGuard&) = delete;

    OrmTransGuard(OrmTransGuard&& rhs)
        : m_db(std::move(rhs.m_db))
    {
        rhs.m_db = nullptr;
    }

    OrmTransGuard& operator=(OrmTransGuard&& rhs)
    {
        if (this == &rhs)
        {
            return *this;
        }

        if (nullptr != m_db)
        {
            m_db->transactionRollback();
        }
        m_db = std::move(rhs.m_db);
        rhs.m_db = nullptr;
        return *this;
    }

private:
    DbConnector* m_db;
};

/*!
 * \brief   The OrmTable represents a database table. Depending
 *          on the EditStrategy, it updates the database by each
 *          inserted record or after all records are inserted.
 * \note    Internally setTable is used. Please do not use setQuery
 *          also because that will invalidate the OrmTable meta data.
 */
class OrmTable : public QSqlRelationalTableModel
{
    Q_OBJECT

public:
    /*!
     * \brief Default constructor.
     * \param parent The parent object as a QObject pointer
     * \param db The database layer we're connected through.
     */
    explicit OrmTable(const DbConnector& db, QObject *parent = nullptr );

    // Deleted copy- and move constructors
    OrmTable( const OrmTable& ) = delete;
    OrmTable( const OrmTable&& ) = delete;
    OrmTable& operator=( const OrmTable& ) = delete;
    OrmTable& operator=( const OrmTable&& ) = delete;

    /// \brief Destructor
    virtual ~OrmTable();

    /*!
     * \brief Sets the tablename, this model represents.
     * \param table The name of the table.
     */
    void setTable( const QString& table );

    /*!
     * \brief Insert a single QSqlRecord into the model.
     * \param row Inserts the record at position row.
     *            If row is negative, the record will be appended to the end.
     * \param record Record to insert
     * \return true if the record could be inserted, otherwise false.
     */
    bool insertRecord( int row, const QSqlRecord &record );

    /*!
     * \brief Insert multiple QSqlRecords into the model.
     * \param row Inserts the records at position row.
     *            If row is negative, the records will be appended to the end.
     *            calls insertRecord internally.
     * \param records Records to insert
     * \return true if the record could be inserted, otherwise false.
     */
    bool insertRecords(int row, const QList<QSqlRecord>& records );

    /*!
     * \brief   writes the data given in the dataobject to the table and to the database.
     * \param   dataObject - Contains all relational data.
     */
    void writeData( const QSharedPointer<ORMRelData>& dataObject );

    /*!
     * \brief   writes the values to a given record, identified by the fieldname in "KeyField".
     * \param   dataObject - Contains "flat" data
     * \return  True if successful, false if not.
     */
    bool updateRecord( ORMData* dataObject );

    /*!
     * \brief   This method is for convenience only. It will retrieve (a set of) records, based on the criteria given.
     * \param   fieldName - The fieldname we like to filter on
     * \param   filterExp - The filter expression
     * \return  A QList of SQLRecords containing the results.
     *          The list will be empty if no records were found or the table is empty.
     */
    QList<OrmTableRow> readData( const QString& fieldName = QString(), const QString& filterExp = QString() );


    /*!
     * \brief   Returns the number of relations added to this table.
     * \return  The number of relations. 0 if none.
     */
    int numberOfRelations() const { return m_qhRelations.count(); }

    /*!
     * \brief setRelatedTable
     * \param tableName The name of the related table.
     * \todo Verify pointer ownership
     * \param pTable Owning pointer to the Table. Ownership is transferred to this instance.
     */
    void setRelatedTable( const QString& tableName, QSortFilterProxyModel* pTable );

    /*!
     * \brief Gets the RelatedTable for the specified table name.
     * \param tableName The name of the table for which to get the relation.
     * \return The relation to the table with the specified table name.
     */
    QSortFilterProxyModel* getRelatedTable( const QString& tableName ) const;

    /*!
     * \brief Store the relation to another table, stored by its unique name.
     *        ORMTable is owner of the relation pointer.
     * \param relationName - the Name of the relation as known by the database.
     */
    void    saveRelation( const QString& relationName, DbRelation* pRelation );

    /*!
     * \brief   Get the relationstructure
     * \param   relationName The name of the relation to get.
     * \return  The pointer to the datastructure. NULL if none exist.
     */
    DbRelation* getRelation( const QString& relationName ) const;

    /*!
     * \brief   Get all registered constraintnames by their true name.
     * \return  The list of all relation by their contraint names. If no relations exist, the list will be empty.
     */
    QStringList getRelationNames() const;

    /*!
     * \brief   Gets the attached relation based on a given tablename.
     * \param   tableName - The tablename we want the relation on.
     * \return  The relation we're searching for. isValid is true if there is a relation, false if not.
     */
    QList<DbRelation*> getRelationsByTableName( const QString& tableName );

    /*!
     * \brief   This method will solve any relations in the structure.
     * \param   dataObject - The ORMRelData object describing the data
     * \return  The ORMData object with all solved references, if there were any.
     *          If the main container can't be retrieved, it will return a nullptr. Check for validity.
     */
    ORMData* solveRelations( const QSharedPointer<ORMRelData>& dataObject );

    /*!
     * \brief   Do a "batched" update. The dataobject contains lists which represents different records.
     * \param   dataObject  - The "maintable" object after resolving the relations.
     *                        It is the responsibility of this TableObject to delete this object if successful,
     *                        else it will be rejected and ownership will be transferred to the next subsystem.
     *                        before calling this method, a transaction should be started on the referenced database
     *                        connection. If this method ( and possible following statements like deCoupleRecords )
     *                        the transaction should be committed.
     * \param   sourceId      The source from which the dataObject originates.
     * \return  True if the complete update was successful. False if not and should result in a rejected
     *          transaction. ( See signalRejectedData() ) Rejecting should be done by the calling method.
     */
    bool    batchUpdate( ORMData* dataObject, const QUuid& sourceId );

    /*!
     * \brief   Decouple records from their previous value (Normally a foreign key relation).
     *          This method will fail if a field in the database is not-nullable or the number
     *          of fields is larger than 2 (1 searchField and 1 updateable field.
     * \param   dataObject  - The dataobject containing all the information needed to update the record(s).
     *                        It is the responsibility of this TableObject to delete this object if successful,
     *                        else it will be rejected and ownership will be transferred to the next subsystem.
     *                        before calling this method, a transaction should be started on the referenced database
     *                        connection. If this method ( and possible previous statements like batchUpdate() )
     *                        the transaction should be committed.
     * \return  true if successful. false if not.
     */
    bool    deCoupleRecords( ORMData* dataObject );

    /*!
     * \brief   Get the name of the field that will track the mutation date / time in uSecs since EPOCH.
     * \return  The name of the field. Empty if no field was configured.
     */
    QString trackField() { return m_recTrackField; }

    /*!
     * \brief   Get the name of the field that will track the mutation date / time in uSecs since EPOCH.
     * \param   _recTrackFieldName - The name of the field, coming from the configuration.
     */
    void    setTrackField( const QString& _recTrackFieldName ) { m_recTrackField = _recTrackFieldName; }


signals:
    /*!
     * \brief Signal for RejectedData.
     * \param dataContainer The data container for this signal.
     */
    void signalRejectedData( const QSharedPointer<ORMRelData>& dataContainer );

private:

    /*!
     * \brief   Translates an ORMData into an QSqlRecord
     * \param   tableObject - The tableObject we like to translate into an sql-record to store.
     * \return  The QSqlRecord object representing an entry. If the creation of the record fails,
     *          the result will be an empty SqlRecord structure.
     */
    QSqlRecord                              buildSqlRecord( ORMData* tableObject );

    /*!
     * \brief   Builds a query and filters the table data. Data isn't removed, but a resultset is shown.
     *
     * \param   dataObject - The dataobject of this particular table as extracted from ORMRelData. Based on the
     *                       keyField(s) and values, the filter is set. ( See also resetFilter() )
     * \return  The number of resulting records that match the criteria. If the filter fails, the result will be -1.
     */
    int                                     tableFilter( ORMData* dataObject );

    /*!
     * \brief   Checks if the ORMData package is valid against the record it tries to update. Each table should have a
     *          field of type INT which contains the timestamp since epoch. Before updating a record, it should check if the timestamp
     *          of the package is actually newer than the timestamp of that record. If the package is newer ( a.k.a. Package timestamp > timestamp_record )
     *          it is valid to update the record with its values, including the timestamp. This will prevent the situation where records will be overwritten
     *          with "old" data. This method works together with the internal variable 'm_recTrackField'. Before calling this method, we have to check if this
     *          variable isn't empty. If so, we don't keep track of record changes in a timeline and we can update the records anyway.
     *
     * \param   dataObject  - The ORMData object containing a complete transaction.
     *                        Does not have to be resolved by relations yet, this will be done after the validity check.
     *
     * \return  True if the record can be updated, False if the package is older than the actual record.
     */
    bool                                    isPackageValid( ORMData* dataObject );

    /*!
     * \brief   resets the filter, allowing the table to show all records. ( See also tableFilter() )
     * \return  The number of records show by the table.
     */
    int                                     resetFilter();

    /*!
     * \brief   Commit a database transaction if one is in progress.
     * \note If the driver does not support transactions this method will look at the last driver error and return false if it contains a message.
     * \return  True if commit succeeds or if there was no transaction in progress, false if commit fails.
     */
    bool                                    commit();

    QString                                 m_className;        ///< Class name for logging purposes
    QString                                 m_recTrackField;    ///< Name of the field, which keep tracks of changes on each record. If Empty, we don't track.
    DbConnector                             m_db;               ///< Database layer we're connected through. We need the reference to access its helper methods.
    QHash<QString, QSortFilterProxyModel*>  m_qhRelTables;      ///< Filtered view on a related table.
    QHash<QString, DbRelation*>             m_qhRelations;      ///< The actual relation Meta information stored for reference.
    QHash<QString, QVariant::Type>          m_qhFieldTypes;     ///< The internal storage for all fieldtypes by their fieldname.
    QHash<QUuid, QSharedPointer<Timeline>>  m_qhTimelines;      ///< Timelines used for the batch update
};

} // End namespace components
} // End namespace osdev

#endif /* OSDEV_COMPONENTS_ORMTABLE_H */