Files
TinyORM/docs/tinyorm/casts.mdx
silverqx 978f9160fb docs bugfix class vs className 😱
[skip ci]
2024-08-16 20:40:22 +02:00

337 lines
15 KiB
Plaintext

---
sidebar_position: 3
sidebar_label: Casts
description: 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.
keywords: [c++ orm, orm, casts, casting, attributes, tinyorm]
---
import Link from '@docusaurus/Link'
# TinyORM: Casting
- [Introduction](#introduction)
- [Accessors](#accessors)
- [Defining An Accessor](#defining-an-accessor)
- [Attribute Casting](#attribute-casting)
- [Date Casting](#date-casting)
- [Query Time Casting](#query-time-casting)
## Introduction
<div className="api-stability alert alert--success">
<Link to='/stability#stability-indexes'>__Stability: 2__</Link> - Stable
</div>
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
:::warning
Accessors are currently only used during the serialization by the [Appending Values](tinyorm/serialization.mdx#appending-values-to-json) 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](tinyorm/serialization.mdx#appending-values-to-json) 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`:
```cpp
#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:
```cpp
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`](https://github.com/silverqx/TinyORM/blob/main/include/orm/tiny/types/modelattributes.hpp) 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.
:::info
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](tinyorm/serialization.mdx#appending-values-to-json).
:::
:::danger
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:
```cpp
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:
```cpp
/*! 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:
```cpp
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:
<div id="casts-types-list">
- `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`
- <code>CastType::Decimal:&lt;precision&gt;</code>
- `CastType::QByteArray`
</div>
:::info
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:
```cpp
#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:
```cpp
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:
```cpp
user->mergeCasts({
{"is_paid", CastType::Boolean},
{"income", {CastType::Decimal, 2}},
});
```
:::warning
You should never define a cast (or an attribute) that has the same name as a relationship.
:::
:::info
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.<br/>
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](tinyorm/serialization.mdx):
```cpp
/*! The attributes that should be cast. */
inline static std::unordered_map<QString, CastItem> u_casts {
{"created_at", {CastType::CustomQDateTime, "yyyy-MM-dd"}},
};
```
:::note
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 <u>__serialization__</u>.
:::
You may customize the [default serialization format](tinyorm/serialization.mdx#customizing-the-default-date-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:
```cpp
/*! 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:
```cpp
/*! 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:
```cpp
/*! The storage format of the model's time columns. */
inline static QString u_timeFormat {"HH:mm:ss.zzz"};
```
#### Date Casting, Serialization & Timezones {#date-casting-serialization-and-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.
:::info
Passing the `Qt::TimeSpec` (eg. `Qt::UTC` or `Qt::LocalTime`) to `QDateTime` methods was deprecated in [`Qt >=6.5`](https://github.com/qt/qtbase/commit/8c8d6ff7b6e2e6b1b673051685f1499ae4d65e05?diff=unified&w=0), 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:
```cpp
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:
```cpp
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();
```