Lightweight 0.20260921.0
Loading...
Searching...
No Matches
HasManyThrough.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../Utils.hpp"
6#include "Error.hpp"
7#include "Record.hpp"
8
9#include <reflection-cpp/reflection.hpp>
10
11#include <compare>
12#include <functional>
13#include <memory>
14#include <vector>
15
16namespace Lightweight
17{
18
19/// @brief This API represents a many-to-many relationship between two records through a third record.
20///
21/// The join record is named with the @ref Through marker, so that the reader can tell it apart from
22/// the referenced record at a glance:
23///
24/// @code
25/// struct Friendship;
26/// struct Human
27/// {
28/// Field<int, PrimaryKey::AutoAssign> id;
29/// HasManyThrough<Human, Through<Friendship>> friends;
30/// };
31/// @endcode
32///
33/// The join record must declare one `BelongsTo` pointing back at the record owning this relationship,
34/// and one pointing at the referenced record. Both are located by matching the relationship *type*.
35///
36/// When the join record cannot be resolved that way - most notably a self-referential many-to-many,
37/// where both of its foreign keys point at the *same* table - name the two foreign key columns
38/// explicitly:
39///
40/// @code
41/// struct Friendship;
42/// struct Human
43/// {
44/// Field<int, PrimaryKey::AutoAssign> id;
45/// HasManyThrough<Human, Through<Friendship>, SqlRealName { "a_id" }, SqlRealName { "b_id" }> friends;
46/// };
47/// struct Friendship
48/// {
49/// Field<int, PrimaryKey::AutoAssign> id;
50/// BelongsTo<&Human::id, SqlRealName { "a_id" }> a;
51/// BelongsTo<&Human::id, SqlRealName { "b_id" }> b;
52/// };
53/// @endcode
54///
55/// @tparam ReferencedRecordT The record type on the "many" side of the relationship.
56/// @tparam ThroughSpec The join record, wrapped as `Through<T>`. Naming the record bare is deprecated.
57/// @tparam TheOwnerSelector Singles out the join record's foreign key pointing at the *owning* record.
58/// @tparam TheReferencedSelector Singles out the join record's foreign key pointing at @p ReferencedRecordT.
59///
60/// @see DataMapper, Field, HasMany, Through, RelationSelector
61/// @ingroup DataMapper
62template <typename ReferencedRecordT,
63 typename ThroughSpec,
64 auto TheOwnerSelector = AutoDetectRelation,
65 auto TheReferencedSelector = AutoDetectRelation>
67{
69 "The selector template arguments of HasManyThrough must be foreign key column names "
70 "(a SqlRealName) or std::nullopt to resolve the relationship automatically.");
71
72 static_assert(!IsThrough<ReferencedRecordT>,
73 "The referenced record of HasManyThrough must not be wrapped in Through<>, "
74 "only the join record is.");
75
76 public:
77 /// The record type of the "through" side of the relationship.
79
80 /// The record type of the "many" side of the relationship.
81 using ReferencedRecord = ReferencedRecordT;
82
83 /// Singles out the join record's foreign key pointing at the record owning this relationship.
84 static constexpr auto OwnerSelector = TheOwnerSelector;
85
86 /// Singles out the join record's foreign key pointing at @ref ReferencedRecord.
87 static constexpr auto ReferencedSelector = TheReferencedSelector;
88
89 /// The list of records on the "many" side of the relationship.
90 using ReferencedRecordList = std::vector<std::shared_ptr<ReferencedRecord>>;
91
92 /// Value type for range-based iteration.
94 /// Iterator type for the list of records.
95 using iterator = ReferencedRecordList::iterator;
96 /// Const iterator type for the list of records.
97 using const_iterator = ReferencedRecordList::const_iterator;
98
99 /// Retrieves the list of loaded records.
100 [[nodiscard]] ReferencedRecordList const& All() const noexcept;
101
102 /// Retrieves the list of records as mutable reference.
103 [[nodiscard]] ReferencedRecordList& All() noexcept;
104
105 /// Emplaces the given list of records into this relationship.
107
108 /// Retrieves the number of records in this relationship.
109 [[nodiscard]] std::size_t Count() const;
110
111 /// Checks if this relationship is empty.
112 [[nodiscard]] bool IsEmpty() const;
113
114 /// @brief Retrieves the record at the given index.
115 ///
116 /// @param index The index of the record to retrieve.
117 /// @note This method will on-demand load the records if they are not already loaded.
118 /// @note This method will throw if the index is out of bounds.
119 [[nodiscard]] ReferencedRecord const& At(std::size_t index) const;
120
121 /// @brief Retrieves the record at the given index.
122 ///
123 /// @param index The index of the record to retrieve.
124 /// @note This method will on-demand load the records if they are not already loaded.
125 /// @note This method will throw if the index is out of bounds.
126 [[nodiscard]] ReferencedRecord& At(std::size_t index);
127
128 /// @brief Retrieves the record at the given index.
129 ///
130 /// @param index The index of the record to retrieve.
131 /// @note This method will on-demand load the records if they are not already loaded.
132 /// @note This method will NOT throw if the index is out of bounds. The behaviour is undefined.
133 [[nodiscard]] ReferencedRecord const& operator[](std::size_t index) const;
134
135 /// @brief Retrieves the record at the given index.
136 ///
137 /// @param index The index of the record to retrieve.
138 /// @note This method will on-demand load the records if they are not already loaded.
139 /// @note This method will NOT throw if the index is out of bounds. The behaviour is undefined.
140 [[nodiscard]] ReferencedRecord& operator[](std::size_t index);
141
142 /// Returns an iterator to the beginning of the record list.
143 [[nodiscard]] iterator begin() noexcept;
144 /// Returns an iterator to the end of the record list.
145 [[nodiscard]] iterator end() noexcept;
146 /// Returns a const iterator to the beginning of the record list.
147 [[nodiscard]] const_iterator begin() const noexcept;
148 /// Returns a const iterator to the end of the record list.
149 [[nodiscard]] const_iterator end() const noexcept;
150
151 /// Default three-way comparison operator.
152 std::weak_ordering operator<=>(HasManyThrough const& other) const noexcept = default;
153
154 struct Loader
155 {
156 std::function<size_t()> count;
157 std::function<ReferencedRecordList()> all;
158 std::function<void(std::function<void(ReferencedRecord const&)>)> each;
159 };
160
161 /// Used internally to configure on-demand loading of the records.
162 void SetAutoLoader(Loader loader) noexcept
163 {
164 _loader = std::move(loader);
165 }
166
167 /// Reloads the records from the database.
168 void Reload()
169 {
170 _count = std::nullopt;
171 _records = std::nullopt;
172 RequireLoaded();
173 }
174
175 /// @brief Iterates over all records in this relationship.
176 ///
177 /// @param callable The callable to invoke for each record.
178 /// @note This method will on-demand load the records if they are not already loaded,
179 /// but not hold them all in memory.
180 template <typename Callable>
181 void Each(Callable const& callable)
182 {
183 if (!_records && _loader.each)
184 {
185 _loader.each(callable);
186 return;
187 }
188
189 for (auto const& record: All())
190 callable(*record);
191 }
192
193 private:
194 void RequireLoaded()
195 {
196 if (_records)
197 return;
198
199 if (_loader.all)
200 _records = _loader.all();
201
202 if (!_records)
203 throw SqlRequireLoadedError(Reflection::TypeNameOf<std::remove_cvref_t<decltype(*this)>>);
204 }
205
206 Loader _loader;
207
208 std::optional<size_t> _count;
209 std::optional<ReferencedRecordList> _records;
210};
211
212namespace detail
213{
214 template <typename T>
215 struct IsHasManyThroughType: std::false_type
216 {
217 };
218
219 template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
220 struct IsHasManyThroughType<HasManyThrough<ReferencedRecordT, ThroughSpec, OwnerSelector, ReferencedSelector>>:
221 std::true_type
222 {
223 };
224
225} // namespace detail
226
227template <typename T>
228constexpr bool IsHasManyThrough = detail::IsHasManyThroughType<std::remove_cvref_t<T>>::value;
229
230template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
233{
234 const_cast<HasManyThrough*>(this)->RequireLoaded();
235
236 return _records.value();
237}
238
239template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
242{
243 RequireLoaded();
244
245 return _records.value(); // NOLINT(bugprone-unchecked-optional-access)
246}
247
248template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
250 ReferencedRecordT,
251 ThroughSpec,
252 OwnerSelector,
253 ReferencedSelector>::Emplace(ReferencedRecordList&& records) noexcept
254{
255 _records = { std::move(records) };
256 _count = _records->size();
257 return *_records;
258}
259
260template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
262{
263 if (_records)
264 return _records->size();
265
266 if (!_count)
268 _loader.count();
269
270 return _count.value_or(0);
271}
272
273template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
278
279template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
282{
283 return *All().at(index);
284}
285
286template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
292
293template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
299
300template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
306
307template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
313
314template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
320
321template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
327
328template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
334
335} // namespace Lightweight
This API represents a many-to-many relationship between two records through a third record.
ReferencedRecordList & Emplace(ReferencedRecordList &&records) noexcept
Emplaces the given list of records into this relationship.
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
ReferencedRecordList const & All() const noexcept
Retrieves the list of loaded records.
ThroughRecordOf< ThroughSpec > ThroughRecord
The record type of the "through" side of the relationship.
static constexpr auto OwnerSelector
Singles out the join record's foreign key pointing at the record owning this relationship.
ReferencedRecordList::const_iterator const_iterator
Const iterator type for the list of records.
iterator begin() noexcept
Returns an iterator to the beginning of the record list.
void Each(Callable const &callable)
Iterates over all records in this relationship.
std::vector< std::shared_ptr< ReferencedRecord > > ReferencedRecordList
The list of records on the "many" side of the relationship.
ReferencedRecordT ReferencedRecord
The record type of the "many" side of the relationship.
iterator end() noexcept
Returns an iterator to the end of the record list.
ReferencedRecord const & operator[](std::size_t index) const
Retrieves the record at the given index.
ReferencedRecord const & At(std::size_t index) const
Retrieves the record at the given index.
bool IsEmpty() const
Checks if this relationship is empty.
static constexpr auto ReferencedSelector
Singles out the join record's foreign key pointing at ReferencedRecord.
ReferencedRecord value_type
Value type for range-based iteration.
std::size_t Count() const
Retrieves the number of records in this relationship.
void Reload()
Reloads the records from the database.
ReferencedRecordList::iterator iterator
Iterator type for the list of records.
Represents an error when a record is required to be loaded but is not.
Definition Error.hpp:16
Constrains what may be used to single out one of several foreign keys into the same table.
Definition Record.hpp:134
@ Count
Number of enumerators; not an operation itself.
typename detail::ThroughRecordOfHelper< ThroughSpec >::type ThroughRecordOf
Resolves the join record of a through-relationship from its template argument.
Definition Record.hpp:231
constexpr std::nullopt_t AutoDetectRelation
Selector value meaning "resolve the relationship automatically; the match must be unique".
Definition Record.hpp:119