TinyORM: Casting
Introduction​
Stability: 2 - Stable
Attribute casting allows you to transform TinyORM attribute values when you retrieve them on model instances. For example, you may want to convert a datetime
string that is stored in your database to the QDateTime
instance when it is accessed via your TinyORM model. Or, you may want to convert a tinyint
number that is stored in the database to the bool
when you access it on the TinyORM model.
Accessors​
Accessors are currently only used during the serialization by the Appending Values feature. They are not used during the getAttribute
or getAttributeValue
methods calls.
Defining An Accessor​
An accessor transforms a TinyORM attribute value when it is accessed (currently during serialization only by the Appending Values feature). To define an accessor, create a protected method on your model to represent the accessible attribute. This method name should correspond to the "camelCase" representation of the true underlying model attribute / database column when applicable.
In this example, we'll define an accessor for the first_name
attribute. The accessor will automatically be called by TinyORM during serialization if the first_name
attribute is defined in the u_appends
data member set. All attribute accessor methods must return the Orm::Tiny::Casts::Attribute
:
#include <orm/tiny/model.hpp>
using Orm::Tiny::Model;
class User final : public Model<User>
{
friend Model;
using Model::Model;
protected:
/*! Get the user's first name (accessor). */
Attribute firstName() const noexcept
{
return Attribute::make(/* get */ [this]() -> QVariant
{
auto firstName = getAttribute<QString>("first_name");
if (!firstName.isEmpty())
firstName[0] = firstName.at(0).toUpper();
return firstName;
});
}
private:
/*! Map of mutator names to methods. */
inline static const QHash<QString, MutatorFunction> u_mutators {
{"first_name", &User::firstName},
};
};
All accessor methods return an Attribute
instance which defines how the attribute will be accessed. To do so, we supply the get
argument to the Attribute
class constructor or Attribute::make
factory method.
As you can see, the current model is captured by-reference using the [this]
capture, allowing you to obtain a value by the getAttribute
method inside the lambda expression, manipulate it and return a new value.
You can also use the second overload that allows you to pass the ModelAttributes
unordered map to the lambda expression:
protected:
/*! Get the user's first name (accessor). */
Attribute firstName() const noexcept
{
return Attribute::make(
/* get */ [](const ModelAttributes &attributes) -> QVariant
{
auto firstName = attributes.at<QString>("first_name");
if (!firstName.isEmpty())
firstName[0] = firstName.at(0).toUpper();
return firstName;
});
}
The ModelAttributes
container extends the std::unordered_map<QString, QVariant>
and adds the at<T>
method that allows you to cast the underlying QVariant value.
Special note should be given to the u_mutators
static data member map, which maps accessors' attribute names to its methods. This data member is required because C++ does not currently support reflection.
If you would like these computed values to be added to the vector, map, or JSON representations of your model, you will need to append them.
You must guarantee that the current model will live long enough to avoid the dangling reference and crash if the current model is captured by-reference. Of course, you can capture it by-copy in edge cases or if you can't guarantee this.
Building Value From Multiple Attributes​
Sometimes your accessor may need to transform multiple model attributes into a single value. You can use both methods described above to accomplish this:
using Orm::Constants::SPACE_IN;
protected:
/*! Get the user's full name (accessor). */
Attribute fullName() const noexcept
{
return Attribute::make(/* get */ [this]() -> QVariant
{
return SPACE_IN.arg(getAttribute<QString>("first_name"),
getAttribute<QString>("last_name"));
});
}
Or you can use the ModelAttributes
overload:
/*! Get the user's full name (accessor). */
Attribute fullName() const noexcept
{
return Attribute::make(
/* get */ [](const ModelAttributes &attributes) -> QVariant
{
return SPACE_IN.arg(attributes.at<QString>("first_name"),
attributes.at<QString>("last_name"));
});
}
Accessor Caching​
Sometimes computing an attribute value can be intensive, in this case, you can enable caching for this attribute value. To accomplish this, you have to invoke the shouldCache
method when defining your accessor:
using Orm::Constants::SPACE_IN;
protected:
/*! Get the user's full name (accessor). */
Attribute fullName() const noexcept
{
return Attribute::make(/* get */ [this]() -> QVariant
{
return SPACE_IN.arg(getAttribute<QString>("first_name"),
getAttribute<QString>("last_name"));
}).shouldCache();
}
Attribute Casting​
Attribute casting provides functionality that allows converting model attributes to the appropriate QVariant
metatype when it is accessed via your TinyORM model. The core of this functionality is a model's u_casts
static data member that provides a convenient method of converting attributes' QVariant
internal types to the defined cast types.
The u_casts
static data member should be the std::unordered_map<QString, Orm::CastItem>
where the key is the name of the attribute being cast and the value is the type you wish to cast the column to. The supported cast types are:
CastType::QString
CastType::Boolean
/CastType::Bool
CastType::Integer
/CastType::Int
CastType::UInteger
/CastType::UInt
CastType::LongLong
CastType::ULongLong
CastType::Short
CastType::UShort
CastType::QDate
CastType::QDateTime
CastType::Timestamp
CastType::Real
CastType::Float
CastType::Double
-
CastType::Decimal:<precision>
CastType::QByteArray
The primary key name defined by the u_primaryKey
model's data member is automatically cast to the CastType::ULongLong
for all database drivers if the u_incrementing
is set to true (its default value).
To demonstrate attribute casting, let's cast the is_admin
attribute, which is stored in our database as an integer (0
or 1
) to a QVariant(bool)
value:
#pragma once
#include <orm/tiny/model.hpp>
using Orm::Tiny::Model;
class User final : public Model<User>
{
friend Model;
using Model::Model;
/*! The attributes that should be cast. */
inline static std::unordered_map<QString, CastItem> u_casts {
{"is_admin", CastType::Boolean},
};
};
After defining the cast, the is_admin
attribute will always be cast to a QVariant(bool)
when you access it, even if the underlying value is stored in the database as an integer:
using Orm::Utils::Helpers;
auto isAdmin = User::find(1)->getAttribute("is_admin");
// Proof of the QVariant type
Q_ASSERT(Helpers::qVariantTypeId(isAdmin) == QMetaType::Bool);
if (isAdmin.value<bool>()) {
//
}
If you need to add a new, temporary cast at runtime, you may use the mergeCasts
method. These cast definitions will be added to any of the casts already defined on the model:
user->mergeCasts({
{"is_paid", CastType::Boolean},
{"income", {CastType::Decimal, 2}},
});
You should never define a cast (or an attribute) that has the same name as a relationship.
Attributes that are null
will also be cast so that the QVariant
's internal type will have the correct type.
Date Casting​
By default, TinyORM will cast the created_at
and updated_at
columns to instances of QDateTime
. You may cast additional date attributes by defining additional date casts within your model's u_casts
static data member unordered map. Typically, dates should be cast using the CastType::QDateTime
, CastType::QDate
, or CastType::Timestamp
cast types.
When a database column is of the date type, you may set the corresponding model attribute value to a Unix timestamp, date string (Y-m-d
), date-time string, QDate
, or QDateTime
instance. The date's value will be correctly converted and stored in your database.
The same is true for the datetime or timestamp database column types, you can set the corresponding model attribute value to a Unix timestamp, date-time string, or a QDateTime
instance.
When defining the CastType::QDate
or CastType::QDateTime
cast, you may also specify the date's format. In this case you must use the CastType::CustomQDate
or CastType::CustomQDateTime
cast types. This format will be used when the model is serialized to a vector, map, or JSON:
/*! The attributes that should be cast. */
inline static std::unordered_map<QString, CastItem> u_casts {
{"created_at", {CastType::CustomQDateTime, "yyyy-MM-dd"}},
};
The CastType::CustomQDate
and CastType::CustomQDateTime
cast types behave exactly like the CastType::QDate
and CastType::QDateTime
cast types with the additional date's format functionality during serialization.
You may customize the default serialization format for all of your model's dates or datetimes by defining a serializeDate
or serializeDateTime
methods on your model. These methods do not affect how your dates are formatted for storage in the database:
/*! Prepare a date for vector, map, or JSON serialization. */
QString serializeDate(const QDate date)
{
return date.toString("yyyy-MM-dd");
}
/*! Prepare a datetime for vector, map, or JSON serialization. */
QString serializeDateTime(const QDateTime &datetime)
{
return datetime.toUTC().toString("yyyy-MM-ddTHH:mm:ssZ");
}
To specify the format that should be used when actually storing a model's dates within your database, you should define a u_dateFormat
data member on your model:
/*! The storage format of the model's date columns. */
inline static QString u_dateFormat {QLatin1Char('U')};
This format can be any format that the QDateTime's fromString
or toString
methods accept or the special U
format that represents the Unix timestamp (this U
format is TinyORM-specific and isn't supported by QDateTime
).
Define a u_timeFormat
data member on your model to specify the format that should be used when storing a model's times within your database:
/*! The storage format of the model's time columns. */
inline static QString u_timeFormat {"HH:mm:ss.zzz"};
Date Casting, Serialization & Timezones​
By default, the CastType::CustomQDate
and CastType::CustomQDateTime
casts will serialize dates to a UTC ISO-8601 date string (yyyy-MM-ddTHH:mm:ss.zzzZ
), regardless of the timezone specified in your database connection's qt_timezone
configuration option. You are strongly encouraged to always use this serialization format, as well as to store your application's dates in the UTC timezone by not changing your database connection's qt_timezone
configuration option from its default QTimeZone::UTC
value. Consistently using the UTC timezone throughout your application will provide the maximum level of interoperability with other date manipulation libraries or services written in any programming language.
If a custom format is applied to the CastType::CustomQDate
or CastType::CustomQDateTime
cast types, such as {CastType::CustomQDateTime, "yyyy-MM-dd HH:mm:ss"}
, the inner timezone of the QDateTime instance will be used during date serialization. Typically, this will be the timezone specified in your database connection's qt_timezone
configuration option.
Passing the Qt::TimeSpec
(eg. Qt::UTC
or Qt::LocalTime
) to QDateTime
methods was deprecated in Qt >=6.5
, it was replaced by the enum QTimeZone::Initialization
(QTimeZone::UTC
and QTimeZone::LocalTime
).
If you need to support older and newer versions of Qt at the same time, you can use the Orm::QtTimeZoneConfig::utc()
or QtTimeZoneConfig::localTime()
factory methods to create the QtTimeZoneConfig
instance.
Query Time Casting​
Sometimes you may need to apply casts while executing a query, such as when selecting a raw value from a table. For example, consider the following query:
using Models::Post;
using Models::User;
auto users = User::select("users.*")
->addSelect(
Post::selectRaw("MAX(created_at)")
->whereColumnEq("user_id", "users.id")
.toBase(),
"last_posted_at"
).get();
The last_posted_at
attribute on the results of this query will be a simple string. It would be wonderful if we could apply a CastType::QDateTime
cast to this attribute when executing the query. Thankfully, we may accomplish this using the withCasts
or withCast
methods:
auto users = User::select("users.*")
->addSelect(Post::selectRaw("MAX(created_at)")
->whereColumnEq("user_id", "users.id")
.toBase(),
"last_posted_at")
.withCast({"last_posted_at", CastType::QDateTime})
.get();