Lightweight 0.20261105.1
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 <optional>
15#include <type_traits>
16#include <utility>
17#include <vector>
18
19namespace Lightweight
20{
21
22/// @brief This API represents a many-to-many relationship between two records through a third record.
23///
24/// The join record is named with the @ref Through marker, so that the reader can tell it apart from
25/// the referenced record at a glance:
26///
27/// @code
28/// struct Friendship;
29/// struct Human
30/// {
31/// Field<int, PrimaryKey::AutoAssign> id;
32/// HasManyThrough<Human, Through<Friendship>> friends;
33/// };
34/// @endcode
35///
36/// The join record must declare one `BelongsTo` pointing back at the record owning this relationship,
37/// and one pointing at the referenced record. Both are located by matching the relationship *type*.
38///
39/// When the join record cannot be resolved that way - most notably a self-referential many-to-many,
40/// where both of its foreign keys point at the *same* table - name the two foreign key columns
41/// explicitly:
42///
43/// @code
44/// struct Friendship;
45/// struct Human
46/// {
47/// Field<int, PrimaryKey::AutoAssign> id;
48/// HasManyThrough<Human, Through<Friendship>, SqlRealName { "a_id" }, SqlRealName { "b_id" }> friends;
49/// };
50/// struct Friendship
51/// {
52/// Field<int, PrimaryKey::AutoAssign> id;
53/// BelongsTo<&Human::id, SqlRealName { "a_id" }> a;
54/// BelongsTo<&Human::id, SqlRealName { "b_id" }> b;
55/// };
56/// @endcode
57///
58/// @tparam ReferencedRecordT The record type on the "many" side of the relationship.
59/// @tparam ThroughSpec The join record, wrapped as `Through<T>`. Naming the record bare is deprecated.
60/// @tparam TheOwnerSelector Singles out the join record's foreign key pointing at the *owning* record.
61/// @tparam TheReferencedSelector Singles out the join record's foreign key pointing at @p ReferencedRecordT.
62///
63/// @see DataMapper, Field, HasMany, Through, RelationSelector
64/// @ingroup DataMapper
65template <typename ReferencedRecordT,
66 typename ThroughSpec,
67 auto TheOwnerSelector = AutoDetectRelation,
68 auto TheReferencedSelector = AutoDetectRelation>
70{
72 "The selector template arguments of HasManyThrough must be foreign key column names "
73 "(a SqlRealName) or std::nullopt to resolve the relationship automatically.");
74
75 static_assert(!IsThrough<ReferencedRecordT>,
76 "The referenced record of HasManyThrough must not be wrapped in Through<>, "
77 "only the join record is.");
78
79 public:
80 /// The record type of the "through" side of the relationship.
82
83 /// The record type of the "many" side of the relationship.
84 using ReferencedRecord = ReferencedRecordT;
85
86 /// Singles out the join record's foreign key pointing at the record owning this relationship.
87 static constexpr auto OwnerSelector = TheOwnerSelector;
88
89 /// Singles out the join record's foreign key pointing at @ref ReferencedRecord.
90 static constexpr auto ReferencedSelector = TheReferencedSelector;
91
92 /// The list of records on the "many" side of the relationship.
93 using ReferencedRecordList = std::vector<std::shared_ptr<ReferencedRecord>>;
94
95 /// Value type for range-based iteration.
97 /// Iterator type for the list of records.
98 using iterator = ReferencedRecordList::iterator;
99 /// Const iterator type for the list of records.
100 using const_iterator = ReferencedRecordList::const_iterator;
101
102 /// @brief Retrieves the records, loading them on first access.
103 ///
104 /// Never throws for unavailable records: the reason comes back as the error instead.
105 ///
106 /// @return The records; or @ref RelationError::NotConfigured when there is no loader (a hand-built
107 /// record, or one read with `loadRelations = false`), @ref RelationError::Outdated when the
108 /// default connection string changed since the record was read, @ref RelationError::QueryFailed
109 /// when the load query failed.
111 {
112 return Load().transform([this] { return std::reference_wrapper<ReferencedRecordList const> { *_records }; });
113 }
114
115 /// @copydoc All() const
117 {
118 return Load().transform([this] { return std::reference_wrapper<ReferencedRecordList> { *_records }; });
119 }
120
121 /// Emplaces the given list of records into this relationship.
123 {
124 _records = { std::move(records) };
125 _count = _records->size();
126 _loadError.reset();
127 return *_records;
128 }
129
130 /// @return The number of records in this relationship - counted by a query, without loading them,
131 /// unless they are loaded already - or why it is unavailable (see @ref All()).
132 [[nodiscard]] RelationResult<std::size_t> Count() const
133 {
134 if (_records)
135 return _records->size();
136 if (_count)
137 return *_count;
138 if (_loadError)
139 return std::unexpected { *_loadError };
140 if (!_loader.count)
141 return std::unexpected { RelationError::NotConfigured };
142
143 auto counted = _loader.count();
144 if (counted)
145 _count = *counted;
146 else if (counted.error() != RelationError::QueryFailed)
147 _loadError = counted.error();
148 return counted;
149 }
150
151 /// @return Whether this relationship is empty, or why that is unknown (see @ref All()).
152 [[nodiscard]] RelationResult<bool> IsEmpty() const
153 {
154 return Count().transform([](std::size_t count) { return count == 0; });
155 }
156
157 /// @brief Retrieves the record at the given index, loading the records on first access.
158 ///
159 /// @param index The index of the record to retrieve.
160 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
161 /// @throws std::out_of_range @p index is out of bounds.
162 [[nodiscard]] ReferencedRecord const& At(std::size_t index) const
163 {
164 return *LoadOrThrow().at(index);
165 }
166
167 /// @copydoc At(std::size_t) const
168 [[nodiscard]] ReferencedRecord& At(std::size_t index)
169 {
170 return *LoadOrThrow().at(index);
171 }
172
173 /// @brief Retrieves the record at the given index, loading the records on first access.
174 ///
175 /// @param index The index of the record to retrieve; out of bounds is undefined behaviour.
176 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
177 [[nodiscard]] ReferencedRecord const& operator[](std::size_t index) const
178 {
179 return *LoadOrThrow()[index];
180 }
181
182 /// @copydoc operator[](std::size_t) const
183 [[nodiscard]] ReferencedRecord& operator[](std::size_t index)
184 {
185 return *LoadOrThrow()[index];
186 }
187
188 /// Returns an iterator to the beginning of the record list, loading the records on first access.
189 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
190 [[nodiscard]] iterator begin()
191 {
192 return LoadOrThrow().begin();
193 }
194
195 /// Returns an iterator to the end of the record list, loading the records on first access.
196 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
197 [[nodiscard]] iterator end()
198 {
199 return LoadOrThrow().end();
200 }
201
202 /// Returns a const iterator to the beginning of the record list, loading the records on first access.
203 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
204 [[nodiscard]] const_iterator begin() const
205 {
206 return std::as_const(LoadOrThrow()).begin();
207 }
208
209 /// Returns a const iterator to the end of the record list, loading the records on first access.
210 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
211 [[nodiscard]] const_iterator end() const
212 {
213 return std::as_const(LoadOrThrow()).end();
214 }
215
216 /// Default three-way comparison operator.
217 std::weak_ordering operator<=>(HasManyThrough const& other) const noexcept = default;
218
219 /// Carries the deferred loads, installed by the DataMapper.
220 struct Loader
221 {
222 /// Counts the records without loading them.
223 std::function<RelationResult<size_t>()> count;
224 /// Loads all records.
225 std::function<RelationResult<ReferencedRecordList>()> all;
226 /// Streams the records to a callback.
227 std::function<RelationResult<void>(std::function<void(ReferencedRecord const&)>)> each;
228 };
229
230 /// Used internally to configure on-demand loading of the records.
231 void SetAutoLoader(Loader loader) noexcept
232 {
233 _loader = std::move(loader);
234 }
235
236 /// @brief Loads the records from the database again, forgetting what was loaded or known before.
237 /// @return Nothing, or why the records are unavailable (see @ref All()).
239 {
240 _count = std::nullopt;
241 _records = std::nullopt;
242 _loadError.reset();
243 return Load();
244 }
245
246 /// @brief Calls @p callable for each record, without holding them all in memory when not loaded yet.
247 ///
248 /// An exception thrown by @p callable propagates unchanged.
249 ///
250 /// @param callable Called once per record.
251 /// @return Nothing, or why the records are unavailable (see @ref All()).
252 template <typename Callable>
253 RelationResult<void> Each(Callable const& callable)
254 {
255 if (!_records && !_loadError && _loader.each)
256 {
257 auto streamed = _loader.each(callable);
258 if (!streamed && streamed.error() != RelationError::QueryFailed)
259 _loadError = streamed.error();
260 return streamed;
261 }
262
263 return All().transform([&callable](ReferencedRecordList const& records) {
264 for (auto const& record: records)
265 callable(*record);
266 });
267 }
268
269 private:
270 /// Loads the records unless they already are, or their unavailability is already known.
271 ///
272 /// @ref RelationError::Outdated is remembered - it does not change by asking again - until records
273 /// are emplaced or reloaded; a failed query is retried on the next access.
274 [[nodiscard]] RelationResult<void> Load() const
275 {
276 if (_records)
277 return {};
278 if (_loadError)
279 return std::unexpected { *_loadError };
280 if (!_loader.all)
281 return std::unexpected { RelationError::NotConfigured };
282
283 auto loaded = _loader.all();
284 if (!loaded)
285 {
286 if (loaded.error() != RelationError::QueryFailed)
287 _loadError = loaded.error();
288 return std::unexpected { loaded.error() };
289 }
290 _records = std::move(*loaded);
291 return {};
292 }
293
294 /// @return The loaded records, or throws SqlRequireLoadedError.
295 [[nodiscard]] ReferencedRecordList& LoadOrThrow() const
296 {
297 if (auto const loaded = Load(); !loaded)
298 throw SqlRequireLoadedError(Reflection::TypeNameOf<std::remove_cvref_t<decltype(*this)>>, loaded.error());
299 return *_records; // NOLINT(bugprone-unchecked-optional-access)
300 }
301
302 Loader _loader;
303
304 mutable std::optional<size_t> _count;
305 mutable std::optional<ReferencedRecordList> _records;
306 mutable std::optional<RelationError> _loadError;
307};
308
309namespace detail
310{
311 template <typename T>
312 struct IsHasManyThroughType: std::false_type
313 {
314 };
315
316 template <typename ReferencedRecordT, typename ThroughSpec, auto OwnerSelector, auto ReferencedSelector>
317 struct IsHasManyThroughType<HasManyThrough<ReferencedRecordT, ThroughSpec, OwnerSelector, ReferencedSelector>>:
318 std::true_type
319 {
320 };
321
322} // namespace detail
323
324template <typename T>
325constexpr bool IsHasManyThrough = detail::IsHasManyThroughType<std::remove_cvref_t<T>>::value;
326
327} // namespace Lightweight
This API represents a many-to-many relationship between two records through a third record.
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
ThroughRecordOf< ThroughSpec > ThroughRecord
The record type of the "through" side of the relationship.
ReferencedRecord & At(std::size_t index)
Retrieves the record at the given index, loading the records on first access.
const_iterator begin() const
ReferencedRecord const & operator[](std::size_t index) const
Retrieves the record at the given index, loading the records on first access.
static constexpr auto OwnerSelector
Singles out the join record's foreign key pointing at the record owning this relationship.
RelationResult< void > Reload()
Loads the records from the database again, forgetting what was loaded or known before.
const_iterator end() const
ReferencedRecordList::const_iterator const_iterator
Const iterator type for the list of records.
RelationResult< std::reference_wrapper< ReferencedRecordList > > All()
Retrieves the records, loading them on first access.
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.
std::weak_ordering operator<=>(HasManyThrough const &other) const noexcept=default
Default three-way comparison operator.
RelationResult< void > Each(Callable const &callable)
Calls callable for each record, without holding them all in memory when not loaded yet.
ReferencedRecordList & Emplace(ReferencedRecordList &&records) noexcept
Emplaces the given list of records into this relationship.
static constexpr auto ReferencedSelector
Singles out the join record's foreign key pointing at ReferencedRecord.
ReferencedRecord value_type
Value type for range-based iteration.
ReferencedRecord const & At(std::size_t index) const
Retrieves the record at the given index, loading the records on first access.
RelationResult< bool > IsEmpty() const
ReferencedRecord & operator[](std::size_t index)
Retrieves the record at the given index, loading the records on first access.
RelationResult< std::reference_wrapper< ReferencedRecordList const > > All() const
Retrieves the records, loading them on first access.
RelationResult< std::size_t > Count() const
ReferencedRecordList::iterator iterator
Iterator type for the list of records.
Constrains what may be used to single out one of several foreign keys into the same table.
Definition Record.hpp:153
detail::ThroughRecordOfHelper< ThroughSpec >::type ThroughRecordOf
Resolves the join record of a through-relationship from its template argument.
Definition Record.hpp:250
std::expected< T, RelationError > RelationResult
The outcome of a relation access: the requested value, or why it is unavailable.
Definition Error.hpp:44
constexpr std::nullopt_t AutoDetectRelation
Selector value meaning "resolve the relationship automatically; the match must be unique".
Definition Record.hpp:138
Carries the deferred loads, installed by the DataMapper.
std::function< RelationResult< void >(std::function< void(ReferencedRecord const &)>)> each
Streams the records to a callback.
std::function< RelationResult< size_t >()> count
Counts the records without loading them.
std::function< RelationResult< ReferencedRecordList >()> all
Loads all records.