#pragma once #ifndef ORM_TINY_TINYBUILDER_HPP #define ORM_TINY_TINYBUILDER_HPP #include "orm/macros/systemheader.hpp" TINY_SYSTEM_HEADER #include #include #include #include #include "orm/databaseconnection.hpp" #include "orm/tiny/concerns/queriesrelationships.hpp" #include "orm/tiny/exceptions/modelnotfounderror.hpp" #include "orm/tiny/tinybuilderproxies.hpp" TINYORM_BEGIN_COMMON_NAMESPACE namespace Orm::Tiny { /*! ORM Tiny builder. */ template class Builder : public BuilderProxies, public Concerns::QueriesRelationships { Q_DISABLE_COPY(Builder) // Used by TinyBuilderProxies::where/latest/oldest/update() friend BuilderProxies; /*! Alias for the attribute utils. */ using AttributeUtils = Orm::Tiny::Utils::Attribute; public: /*! Constructor. */ Builder(const QSharedPointer &query, Model &model); /*! Get the SQL representation of the query. */ inline QString toSql() const; /*! Get the current query value bindings as flattened QVector. */ inline QVector getBindings() const; /*! Execute the query as a "select" statement. */ QVector get(const QVector &columns = {ASTERISK}); /*! Get a single column's value from the first result of a query. */ QVariant value(const Column &column); /*! Get the vector with the values of a given column. */ QVector pluck(const QString &column) const; /*! Get the vector with the values of a given column. */ template std::map pluck(const QString &column, const QString &key) const; /*! Find a model by its primary key. */ std::optional find(const QVariant &id, const QVector &columns = {ASTERISK}); /*! Find a model by its primary key or return fresh model instance. */ Model findOrNew(const QVariant &id, const QVector &columns = {ASTERISK}); /*! Find a model by its primary key or throw an exception. */ Model findOrFail(const QVariant &id, const QVector &columns = {ASTERISK}); /*! Find multiple models by their primary keys. */ QVector findMany(const QVector &ids, const QVector &columns = {ASTERISK}); /*! Execute the query and get the first result. */ std::optional first(const QVector &columns = {ASTERISK}); /*! Get the first record matching the attributes or instantiate it. */ Model firstOrNew(const QVector &attributes = {}, const QVector &values = {}); /*! Get the first record matching the attributes or create it. */ Model firstOrCreate(const QVector &attributes = {}, const QVector &values = {}); /*! Execute the query and get the first result or throw an exception. */ Model firstOrFail(const QVector &columns = {ASTERISK}); /*! Add a basic where clause to the query, and return the first result. */ std::optional firstWhere(const Column &column, const QString &comparison, const QVariant &value, const QString &condition = AND); /*! Add a basic equal where clause to the query, and return the first result. */ std::optional firstWhereEq(const Column &column, const QVariant &value, const QString &condition = AND); /*! Add a where clause on the primary key to the query. */ Builder &whereKey(const QVariant &id); /*! Add a where clause on the primary key to the query. */ Builder &whereKey(const QVector &ids); /*! Add a where clause on the primary key to the query. */ Builder &whereKeyNot(const QVariant &id); /*! Add a where clause on the primary key to the query. */ Builder &whereKeyNot(const QVector &ids); /*! Set the relationships that should be eager loaded. */ template Builder &with(const QVector &relations); /*! Set the relationships that should be eager loaded. */ template Builder &with(const QString &relation); /*! Set the relationships that should be eager loaded. */ Builder &with(const QVector &relations); /*! Set the relationships that should be eager loaded. */ Builder &with(QVector &&relations); /*! Prevent the specified relations from being eager loaded. */ Builder &without(const QVector &relations); /*! Prevent the specified relations from being eager loaded. */ Builder &without(const QString &relation); /*! Set the relationships that should be eager loaded while removing any previously added eager loading specifications. */ Builder &withOnly(const QVector &relations); /*! Set the relationship that should be eager loaded while removing any previously added eager loading specifications. */ Builder &withOnly(const QString &relation); /* Insert, Update, Delete */ /*! Save a new model and return the instance. */ Model create(const QVector &attributes = {}); /*! Save a new model and return the instance. */ Model create(QVector &&attributes = {}); /*! Create or update a record matching the attributes, and fill it with values. */ Model updateOrCreate(const QVector &attributes, const QVector &values = {}); /* TinyBuilder methods */ /*! Create a new instance of the model being queried. */ Model newModelInstance(const QVector &attributes = {}); /*! Get the hydrated models without eager loading. */ QVector getModels(const QVector &columns = {ASTERISK}); /*! Eager load the relationships for the models. */ void eagerLoadRelations(QVector &models); /*! Eagerly load the relationship on a set of models. */ template void eagerLoadRelationVisited(Relation &&relation, QVector &models, const WithItem &relationItem) const; /*! Create a vector of models from the QSqlQuery. */ QVector hydrate(QSqlQuery &&result); /*! Get the model instance being queried. */ inline Model &getModel(); /*! Get the underlying query builder instance. */ inline QueryBuilder &getQuery() const; // TODO now fix revisit silverqx /*! Get the underlying query builder instance as a QSharedPointer. */ inline const QSharedPointer & getQuerySharedPointer() const; /*! Get a database connection. */ inline DatabaseConnection &getConnection() const; /*! Get a base query builder instance. */ inline QueryBuilder &toBase() const; // FUTURE add Query Scopes feature silverqx // { return $this->applyScopes()->getQuery(); } /*! Qualify the given column name by the model's table. */ inline QString qualifyColumn(const QString &column) const; protected: /*! Expression alias. */ using Expression = Orm::Query::Expression; /*! Parse a list of relations into individuals. */ QVector parseWithRelations(const QVector &relations); /*! Create a constraint to select the given columns for the relation. */ WithItem createSelectWithConstraint(const QString &name); /*! Parse the nested relationships in a relation. */ void addNestedWiths(const QString &name, QVector &results) const; /*! Get the deeply nested relations for a given top-level relation. */ QVector relationsNestedUnder(const QString &topRelationName) const; /*! Determine if the relationship is nested. */ bool isNestedUnder(const QString &topRelation, const QString &nestedRelation) const; /*! Add the "updated at" column to the vector of values. */ QVector addUpdatedAtColumn(QVector values) const; /*! Get the name of the "created at" column. */ Column getCreatedAtColumnForLatestOldest(Column column) const; /*! Apply the given scope on the current builder instance. */ // template // Builder &callScope(const std::function &scope, // Args &&...parameters); /*! The base query builder instance. */ const QSharedPointer m_query; /* This can't be a reference because the model is created on the stack in Model::query(), then copied here and the original is destroyed immediately. */ /*! The model being queried. */ Model m_model; /*! The relationships that should be eager loaded. */ QVector m_eagerLoad; }; template Builder::Builder(const QSharedPointer &query, Model &model) : m_query(query) , m_model(model) { m_query->from(m_model.getTable()); } template QString Builder::toSql() const { return toBase().toSql(); } template QVector Builder::getBindings() const { return toBase().getBindings(); } // TODO now name QVector model collections by using, eg CollectionType silverqx template QVector Builder::get(const QVector &columns) { auto models = getModels(columns); /* If we actually found models we will also eager load any relationships that have been specified as needing to be eager loaded, which will solve the n+1 query issue for the developers to avoid running a lot of queries. */ if (models.size() > 0) /* 'models' are passed down as the reference and relations are set on models at the end of the call tree, no need to return models. */ eagerLoadRelations(models); return models; // Laravel does it this way // return $builder->getModel()->newCollection($models); } template QVariant Builder::value(const Column &column) { auto model = first({column}); if (!model) return {}; // Expression support QString column_; if (std::holds_alternative(column)) column_ = std::get(column).getValue().value(); else column_ = std::get(column); return model->getAttribute(column_.mid(column_.lastIndexOf(DOT) + 1)); } template QVector Builder::pluck(const QString &column) const { return toBase().pluck(column); } template template std::map Builder::pluck(const QString &column, const QString &key) const { return toBase().template pluck(column, key); } // FEATURE dilemma primarykey, Model::KeyType for id silverqx template std::optional Builder::find(const QVariant &id, const QVector &columns) { return whereKey(id).first(columns); } template Model Builder::findOrNew(const QVariant &id, const QVector &columns) { auto model = find(id, columns); // Found if (model) return *model; return newModelInstance(); } template Model Builder::findOrFail(const QVariant &id, const QVector &columns) { auto model = find(id, columns); // Found if (model) return *model; throw Exceptions::ModelNotFoundError( Orm::Utils::Type::classPureBasename(), {id}); } template QVector Builder::findMany(const QVector &ids, const QVector &columns) { if (ids.isEmpty()) return {}; return whereKey(ids).get(columns); } template std::optional Builder::first(const QVector &columns) { auto models = this->take(1).get(columns); if (models.isEmpty()) return std::nullopt; return std::move(models.first()); } template Model Builder::firstOrNew(const QVector &attributes, const QVector &values) { auto instance = this->where(attributes).first(); // Model found in db if (instance) return *instance; return newModelInstance(AttributeUtils::joinAttributesForFirstOr( attributes, values, m_model.getKeyName())); } template Model Builder::firstOrCreate(const QVector &attributes, const QVector &values) { // Model found in db if (auto instance = this->where(attributes).first(); instance) return *instance; auto newInstance = newModelInstance(AttributeUtils::joinAttributesForFirstOr( attributes, values, m_model.getKeyName())); newInstance.save(); return newInstance; } template Model Builder::firstOrFail(const QVector &columns) { auto model = first(columns); // Found if (model) return *model; throw Exceptions::ModelNotFoundError( Orm::Utils::Type::classPureBasename()); } template std::optional Builder::firstWhere(const Column &column, const QString &comparison, const QVariant &value, const QString &condition) { return this->where(column, comparison, value, condition).first(); } template std::optional Builder::firstWhereEq(const Column &column, const QVariant &value, const QString &condition) { return this->where(column, EQ, value, condition).first(); } template Builder & Builder::whereKey(const QVariant &id) { return this->where(m_model.getQualifiedKeyName(), EQ, id); } template Builder & Builder::whereKey(const QVector &ids) { m_query->whereIn(m_model.getQualifiedKeyName(), ids); return *this; } template Builder & Builder::whereKeyNot(const QVariant &id) { return this->where(m_model.getQualifiedKeyName(), NE, id); } template Builder & Builder::whereKeyNot(const QVector &ids) { m_query->whereNotIn(m_model.getQualifiedKeyName(), ids); return *this; } template template Builder & Builder::with(const QVector &relations) { auto eagerLoad = parseWithRelations(relations); std::ranges::move(eagerLoad, std::back_inserter(m_eagerLoad)); return *this; } template template Builder & Builder::with(const QString &relation) { return with(QVector {{relation}}); } template Builder & Builder::with(const QVector &relations) { QVector relationsConverted; relationsConverted.reserve(relations.size()); for (const auto &relation : relations) relationsConverted.append({relation}); return with(relationsConverted); } template Builder & Builder::with(QVector &&relations) { QVector relationsConverted; relationsConverted.reserve(relations.size()); for (auto &relation : relations) relationsConverted.append({std::move(relation)}); return with(relationsConverted); } template Builder & Builder::without(const QVector &relations) { // Remove relations in the "relations" vector from m_eagerLoad vector m_eagerLoad = m_eagerLoad | ranges::views::remove_if([&relations](const WithItem &with) { return relations.contains(with.name); }) | ranges::to>(); return *this; } template Builder & Builder::without(const QString &relation) { return without(QVector {relation}); } template Builder & Builder::withOnly(const QVector &relations) { m_eagerLoad.clear(); return with(relations); } template Builder & Builder::withOnly(const QString &relation) { return withOnly(QVector {{relation}}); } template Model Builder::create(const QVector &attributes) { auto model = newModelInstance(attributes); model.save(); return model; } template Model Builder::create(QVector &&attributes) { auto model = newModelInstance(std::move(attributes)); model.save(); return model; } template Model Builder::updateOrCreate(const QVector &attributes, const QVector &values) { auto instance = firstOrNew(attributes); instance.fill(values).save(); return instance; } template Model Builder::newModelInstance(const QVector &attributes) { return m_model.newInstance(attributes) .setConnection(m_query->getConnection().getName()); // TODO study, or stackoverflow move or not move? its a question 🤔 silverqx // return std::move(m_model.newInstance(attributes) // .setConnection(m_query->getConnection().getName())); } template QVector Builder::getModels(const QVector &columns) { return hydrate(m_query->get(columns)); } template void Builder::eagerLoadRelations(QVector &models) { if (m_eagerLoad.isEmpty()) return; for (const auto &relation : std::as_const(m_eagerLoad)) /* For nested eager loads we'll skip loading them here and they will be set as an eager load on the query to retrieve the relation so that they will be eager loaded on that query, because that is where they get hydrated as models. */ if (!relation.name.contains(DOT)) /* Get the relation instance for the given relation name, have to be done through the visitor pattern. */ m_model.eagerLoadRelationWithVisitor(relation, *this, models); } template template void Builder::eagerLoadRelationVisited( Relation &&relation, QVector &models, const WithItem &relationItem) const { // TODO docs add similar note for lazy load silverqx /* Look also at EagerRelationStore::visited(), where the whole flow begins. How this relation flow works: EagerRelationStore::visited() obtains a reference by the relation name to the relation method, these relation methods are defined on models as member functions. A reference to the relation methods are defined in the Model::u_relations hash as lambda expressions. These lambda expressions will be visited/invoked by EagerRelationStore::visited() to obtain references to the relation methods. Relation constraints will be disabled for eager relations by Relation::noConstraints() method, these default constraints are only used for lazy loading, for eager constraints are used constraints, which are defined by Relation::addEagerConstraints() virtual methods. To the Relation::noConstraints() method is passed lambda, which invokes obtained reference to the relation method defined on the model and invokes it on the 'new' model instance refered as 'dummyModel'. The Relation instance is created by this relation method, this relation method calls factory method, which creates the Relation instance. Every Relation has it's own Relation::create() factory method, to which the following parameters are passed, newly created Related model instance, current/parent model instance, database column names of the relationship, and for a BelongsTo relation also the name of the relation. The Relation instance saves a non-const reference to the current/parent model instance, a copy of the related model instance because it is created on the stack. The Relation instance creates a new TinyBuilder instance from the Related model instance by TinyBuilder::newQuery() and saves ownership as the unique pointer. Then eager constraints are applied to this newly created TinyBuilder and the result is returned back to the initial model. The result is transformed into models and these models are hydrated. Hydrated models are saved to the Model::m_relations data member. */ /* First we will "back up" the existing where conditions on the query so we can add our eager constraints, this is done in the EagerRelationStore::visited() by help of the Relations::Relation::noConstraints(). Folowing is not implemented for now, it is true for relationItem.constraints: Then we will merge the wheres that were on the query back to it in order that any where conditions might be specified. */ const auto nested = relationsNestedUnder(relationItem.name); /* If there are nested relationships set on the query, we will put those onto the query instances so that they can be handled after this relationship is loaded. In this way they will all trickle down as they are loaded. */ if (nested.size() > 0) relation->getQuery().with(nested); relation->addEagerConstraints(models); // Add relation contraints defined in a user callback // NOTE api different, Eloquent is passing the Relation reference into the lambda, it would be almost impossible to do it silverqx if (relationItem.constraints) std::invoke(relationItem.constraints, relation->getBaseQuery()); /* Once we have the results, we just match those back up to their parent models using the relationship instance. Then we just return the finished vectors of models which have been eagerly hydrated and are readied for return. */ relation->match(relation->initRelation(models, relationItem.name), relation->getEager(), relationItem.name); } template QVector Builder::hydrate(QSqlQuery &&result) { auto instance = newModelInstance(); QVector models; // Table row, instantiate the QVector once and then re-use QVector row; row.reserve(result.record().count()); while (result.next()) { row.clear(); // Populate table row with data from the database const auto record = result.record(); for (int i = 0; i < record.count(); ++i) row.append({record.fieldName(i), result.value(i)}); // Create a new model instance from the table row models.append(instance.newFromBuilder(row)); } return models; } template Model &Builder::getModel() { return m_model; } template QueryBuilder &Builder::getQuery() const { return *m_query; } template const QSharedPointer & Builder::getQuerySharedPointer() const { return m_query; } template DatabaseConnection & Builder::getConnection() const { return m_query->getConnection(); } template QueryBuilder &Builder::toBase() const { return getQuery(); } template QString Builder::qualifyColumn(const QString &column) const { return m_model.qualifyColumn(column); } template QVector Builder::parseWithRelations(const QVector &relations) { QVector results; // Can contain nested relations results.reserve(relations.size() * 2); for (auto relation : relations) { const auto isSelectConstraint = relation.name.contains(COLON); if (isSelectConstraint && relation.constraints) throw Orm::Exceptions::InvalidArgumentError( "Passing both 'Select constraint' and 'Lambda expression " "constraint' to the Model::with() method is not allowed, use " "only one of them."); if (isSelectConstraint) relation = createSelectWithConstraint(relation.name); /* We need to separate out any nested includes, which allows the developers to load deep relationships using "dots" without stating each level of the relationship with its own key in the vector of eager-load names. */ addNestedWiths(relation.name, results); results.append(std::move(relation)); } return results; } template WithItem Builder::createSelectWithConstraint(const QString &name) { auto splitted = name.split(COLON); auto relation = splitted.at(0).trimmed(); auto &columns = splitted[1]; auto belongsToManyRelatedTable = m_model.getRelatedTableForBelongsToManyWithVisitor(relation); return { std::move(relation), [columns = std::move(columns), belongsToManyRelatedTable = std::move(belongsToManyRelatedTable)] (auto &query) { QVector columnsList; columnsList.reserve(columns.count(COMMA_C) + 1); // Avoid 'clazy might detach' warning for (const auto columns_ = columns.split(COMMA_C); auto column : columns_) { column = column.trimmed(); // Fully qualified column passed, not needed to process if (column.contains(DOT)) { columnsList << std::move(column); continue; } /* Generate fully qualified column name for the BelongsToMany relation. */ if (belongsToManyRelatedTable) { #ifdef __GNUG__ columnsList << QString("%1.%2") .arg(*belongsToManyRelatedTable, column); #else columnsList << QStringLiteral("%1.%2") .arg(*belongsToManyRelatedTable, column); #endif continue; } columnsList << std::move(column); } // TODO move, query.select() silverqx query.select(std::move(columnsList)); } }; } template void Builder::addNestedWiths(const QString &name, QVector &results) const { QStringList progress; /* If the relation has already been set on the result vector, we will not set it again, since that would override any constraints that were already placed on the relationships. We will only set the ones that are not specified. */ // Prevent container detach const auto names = name.split(DOT); progress.reserve(names.size()); for (const auto &segment : names) { progress << segment; auto last = progress.join(DOT); const auto containsRelation = [&last](const auto &relation) { return relation.name == last; }; const auto contains = ranges::contains(results, true, containsRelation); // Don't add a relation in the 'name' variable if (!contains && (last != name)) results.append({std::move(last)}); } } template QVector Builder::relationsNestedUnder(const QString &topRelationName) const { QVector nested; /* We are basically looking for any relationships that are nested deeper than the given top-level relationship. We will just check for any relations that start with the given top relations and adds them to our vectors. */ for (const auto &relation : m_eagerLoad) if (isNestedUnder(topRelationName, relation.name)) nested.append({relation.name.mid(topRelationName.size() + 1), relation.constraints}); return nested; } template bool Builder::isNestedUnder(const QString &topRelation, const QString &nestedRelation) const { return nestedRelation.contains(DOT) && nestedRelation.startsWith(QStringLiteral("%1.").arg(topRelation)); } template QVector Builder::addUpdatedAtColumn(QVector values) const { const auto &updatedAtColumn = m_model.getUpdatedAtColumn(); const auto &qualifiedUpdatedAtColumn = m_model.getQualifiedUpdatedAtColumn(); if (!m_model.usesTimestamps() || updatedAtColumn.isEmpty()) return values; const auto valuesUpdatedAtColumn = std::ranges::find_if(values, [&updatedAtColumn](const auto &updateItem) { return updateItem.column == updatedAtColumn; }); // Not found if (valuesUpdatedAtColumn == std::ranges::cend(values)) values.append({qualifiedUpdatedAtColumn, m_model.freshTimestampString()}); else // Rename updated_at column to the qualified column valuesUpdatedAtColumn->column = qualifiedUpdatedAtColumn; return values; } template Column Builder::getCreatedAtColumnForLatestOldest(Column column) const { /* Don't initialize column when user passed column expression, only when it holds the QString type. */ if (std::holds_alternative(column) && std::get(column).isEmpty() ) { if (const auto &createdAtColumn = m_model.getCreatedAtColumn(); createdAtColumn.isEmpty() ) column = CREATED_AT; else column = createdAtColumn; } return column; } // FEATURE scopes, anyway std::apply() do the same, will have to investigate it silverqx // template // template // Builder & // Builder::callScope( // const std::function &scope, // Args &&...parameters) // { // std::invoke(scope, *this, std::forward(parameters)...); // return *this; // } } // namespace Orm::Tiny TINYORM_END_COMMON_NAMESPACE #endif // ORM_TINY_TINYBUILDER_HPP