Lightweight 0.20260921.0
Loading...
Searching...
No Matches
HasMany.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../DataBinder/Core.hpp"
6#include "../DataBinder/SqlNullValue.hpp"
7#include "../SqlStatement.hpp"
8#include "BelongsTo.hpp"
9#include "Error.hpp"
10#include "Field.hpp"
11#include "Record.hpp"
12
13#include <reflection-cpp/reflection.hpp>
14
15#include <compare>
16#include <memory>
17#include <optional>
18#include <type_traits>
19#include <vector>
20
21namespace Lightweight
22{
23
24/// @brief This HasMany<OtherRecord> represents a simple one-to-many relationship between two records.
25///
26/// The HasMany<OtherRecord> is a member of the "one" side of the relationship.
27///
28/// `OtherRecord` must declare a `BelongsTo` member that points back to this "one" side. That member is
29/// located by matching the relationship *type*, not by its position in either record, so the two
30/// relationship members may be declared at any index. Declaring no such `BelongsTo` is a compile-time
31/// error.
32///
33/// When `OtherRecord` holds more than one foreign key into this record's table - say a meeting that
34/// references the same person table both as its organizer and as whoever writes the minutes - the
35/// inverse is ambiguous. Name the foreign key column through @p TheInverseSelector to single one out:
36///
37/// @code
38/// struct Meeting;
39/// struct Human
40/// {
41/// Field<int, PrimaryKey::AutoAssign> id;
42/// HasMany<Meeting, SqlRealName { "organizer_id" }> organizedMeetings;
43/// HasMany<Meeting, SqlRealName { "minute_taker_id" }> minutedMeetings;
44/// };
45/// struct Meeting
46/// {
47/// Field<int, PrimaryKey::AutoAssign> id;
48/// BelongsTo<&Human::id, SqlRealName { "organizer_id" }> organizer;
49/// BelongsTo<&Human::id, SqlRealName { "minute_taker_id" }, SqlNullable::Null> minuteTaker;
50/// };
51/// @endcode
52///
53/// A meeting with *many* attendees is a many-to-many instead - see `HasManyThrough`. The worked
54/// example in `docs/sql-to-lightweight.md` combines both shapes.
55///
56/// @tparam OtherRecord The record type on the "many" side of the relationship.
57/// @tparam TheInverseSelector Singles out one of several foreign keys, see the RelationSelector concept.
58///
59/// @see InverseBelongsToIndexOf, RelationSelector
60///
61/// @see DataMapper, Field, HasManyThrough
62/// @ingroup DataMapper
63template <typename OtherRecord, auto TheInverseSelector = AutoDetectRelation>
65{
67 "The second template argument of HasMany must be a foreign key column name (a SqlRealName) "
68 "or std::nullopt to resolve the relationship automatically.");
69
70 public:
71 /// The record type of the "many" side of the relationship.
72 using ReferencedRecord = OtherRecord;
73
74 /// Singles out the foreign key of `OtherRecord` that backs this relationship.
75 static constexpr auto InverseSelector = TheInverseSelector;
76
77 /// The list of records on the "many" side of the relationship.
78 using ReferencedRecordList = std::vector<std::shared_ptr<OtherRecord>>;
79
80 /// Record type of the "many" side of the relationship.
81 using value_type = OtherRecord;
82
83 /// Iterator type for the list of records.
84 using iterator = ReferencedRecordList::iterator;
85
86 /// Const iterator type for the list of records.
87 using const_iterator = ReferencedRecordList::const_iterator;
88
89 /// @brief Retrieves the list of loaded records.
90 ///
91 /// @note This method will on-demand load the records if they are not already loaded, and
92 /// therefore throws whatever the loader throws. It also throws SqlRequireLoadedError if
93 /// no auto-loader was configured for this relation.
94 [[nodiscard]] ReferencedRecordList const& All() const;
95
96 /// @brief Retrieves the list of records as mutable reference.
97 ///
98 /// @note This method will on-demand load the records if they are not already loaded, and
99 /// therefore throws whatever the loader throws. It also throws SqlRequireLoadedError if
100 /// no auto-loader was configured for this relation.
101 [[nodiscard]] ReferencedRecordList& All();
102
103 /// @brief Iterates over the list of records and calls the given callable for each record.
104 ///
105 /// @note Use this method if you want to iterate over all records but do not need to store them all in memory, e.g.
106 /// because the full data set wuold be too large.
107 template <typename Callable>
108 void Each(Callable const& callable);
109
110 /// Emplaces the given list of records.
112
113 /// @brief Returns the already-loaded records, or `nullptr` when the relation is not loaded.
114 ///
115 /// Unlike `All()`, this never runs the on-demand loader: it reports what is present right now,
116 /// which is what lets the batched relation loading walk one level deeper (`With<A, B>()`).
117 ///
118 /// @return Pointer to the loaded list, or `nullptr` if the relation was never loaded.
119 [[nodiscard]] ReferencedRecordList* LoadedRecords() noexcept
120 {
121 return _records ? &*_records : nullptr;
122 }
123
124 /// Retrieves the number of records in this 1-to-many relationship.
125 [[nodiscard]] std::size_t Count() const noexcept;
126
127 /// Checks if this 1-to-many relationship is empty.
128 [[nodiscard]] bool IsEmpty() const noexcept;
129
130 /// @brief Retrieves the record at the given index.
131 ///
132 /// @param index The index of the record to retrieve.
133 /// @note This method will on-demand load the records if they are not already loaded.
134 /// @note This method will throw if the index is out of bounds.
135 [[nodiscard]] OtherRecord const& At(std::size_t index) const;
136
137 /// @brief Retrieves the record at the given index.
138 ///
139 /// @param index The index of the record to retrieve.
140 /// @note This method will on-demand load the records if they are not already loaded.
141 /// @note This method will throw if the index is out of bounds.
142 [[nodiscard]] OtherRecord& At(std::size_t index);
143
144 /// @brief Retrieves the record at the given index.
145 ///
146 /// @param index The index of the record to retrieve.
147 /// @note This method will on-demand load the records if they are not already loaded.
148 /// @note This method will NOT throw if the index is out of bounds. The behaviour is undefined.
149 [[nodiscard]] OtherRecord const& operator[](std::size_t index) const;
150
151 /// @brief Retrieves the record at the given index.
152 ///
153 /// @param index The index of the record to retrieve.
154 /// @note This method will on-demand load the records if they are not already loaded.
155 /// @note This method will NOT throw if the index is out of bounds. The behaviour is undefined.
156 [[nodiscard]] OtherRecord& operator[](std::size_t index);
157
158 /// Returns an iterator to the beginning of the record list.
159 /// @note On-demand loads the records, and therefore throws what the loader throws.
160 [[nodiscard]] iterator begin();
161 /// Returns an iterator to the end of the record list.
162 /// @note On-demand loads the records, and therefore throws what the loader throws.
163 [[nodiscard]] iterator end();
164 /// Returns a const iterator to the beginning of the record list.
165 /// @note On-demand loads the records, and therefore throws what the loader throws.
166 [[nodiscard]] const_iterator begin() const;
167 /// Returns a const iterator to the end of the record list.
168 /// @note On-demand loads the records, and therefore throws what the loader throws.
169 [[nodiscard]] const_iterator end() const;
170
171 /// Three-way comparison operator.
172 constexpr std::weak_ordering operator<=>(HasMany const& other) const noexcept = default;
173 /// Equality comparison operator.
174 constexpr bool operator==(HasMany const& other) const noexcept = default;
175 /// Inequality comparison operator.
176 constexpr bool operator!=(HasMany const& other) const noexcept = default;
177
178 struct Loader
179 {
180 std::function<size_t()> count {};
181 std::function<ReferencedRecordList()> all {};
182 std::function<void(std::function<void(ReferencedRecord const&)>)> each {};
183
184 std::weak_ordering operator<=>(Loader const& /*other*/) const noexcept
185 {
186 return std::weak_ordering::equivalent; // Loader is not comparable, so we return equivalent
187 }
188 };
189
190 /// Used internally to configure on-demand loading of the records.
191 void SetAutoLoader(Loader loader) noexcept;
192
193 private:
194 void RequireLoaded();
195
196 Loader _loader;
197 std::optional<ReferencedRecordList> _records;
198 std::optional<size_t> _count;
199};
200
201namespace detail
202{
203 template <typename T>
204 struct IsHasManyType: std::false_type
205 {
206 };
207
208 template <typename OtherRecord, auto InverseSelector>
209 struct IsHasManyType<HasMany<OtherRecord, InverseSelector>>: std::true_type
210 {
211 };
212
213} // namespace detail
214
215template <typename T>
216constexpr bool IsHasMany = detail::IsHasManyType<std::remove_cvref_t<T>>::value;
217
218template <typename OtherRecord, auto InverseSelector>
219inline LIGHTWEIGHT_FORCE_INLINE void HasMany<OtherRecord, InverseSelector>::SetAutoLoader(Loader loader) noexcept
220{
221 _loader = std::move(loader);
222}
223
224template <typename OtherRecord, auto InverseSelector>
225inline LIGHTWEIGHT_FORCE_INLINE void HasMany<OtherRecord, InverseSelector>::RequireLoaded()
226{
227 if (_records)
228 return;
229
230 // The loader is only populated by ConfigureRelationAutoLoading(). A hand-constructed record
231 // never went through it, so calling the empty std::function would be std::bad_function_call.
232 // Mirrors HasManyThrough::RequireLoaded(), which reports the same condition as
233 // SqlRequireLoadedError.
234 if (_loader.all)
235 _records = _loader.all();
236
237 if (!_records)
238 throw SqlRequireLoadedError(Reflection::TypeNameOf<std::remove_cvref_t<decltype(*this)>>);
239}
240
241template <typename OtherRecord, auto InverseSelector>
242inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::ReferencedRecordList& HasMany<
243 OtherRecord,
244 InverseSelector>::Emplace(ReferencedRecordList&& records) noexcept
245{
246 _records = { std::move(records) };
247 return *_records;
248}
249
250template <typename OtherRecord, auto InverseSelector>
251inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::ReferencedRecordList& HasMany<OtherRecord,
252 InverseSelector>::All()
253{
254 RequireLoaded();
255 return *_records; // NOLINT(bugprone-unchecked-optional-access)
256}
257
258template <typename OtherRecord, auto InverseSelector>
259template <typename Callable>
261{
262 if (!_records && _loader.each)
263 {
264 _loader.each(callable);
265 return;
266 }
267
268 for (auto const& record: All())
269 callable(*record);
270}
271
272template <typename OtherRecord, auto InverseSelector>
273inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::ReferencedRecordList const& HasMany<
274 OtherRecord,
275 InverseSelector>::All() const
276{
277 const_cast<HasMany*>(this)->RequireLoaded();
278 return *_records; // NOLINT(bugprone-unchecked-optional-access)
279}
280
281template <typename OtherRecord, auto InverseSelector>
282inline LIGHTWEIGHT_FORCE_INLINE std::size_t HasMany<OtherRecord, InverseSelector>::Count() const noexcept
283{
284 if (_records)
285 return _records->size();
286
287 if (!_count && _loader.count)
288 const_cast<HasMany<OtherRecord, InverseSelector>*>(this)->_count = _loader.count();
289
290 return _count.value_or(0);
291}
292
293template <typename OtherRecord, auto InverseSelector>
294inline LIGHTWEIGHT_FORCE_INLINE bool HasMany<OtherRecord, InverseSelector>::IsEmpty() const noexcept
295{
296 return Count() == 0;
297}
298
299template <typename OtherRecord, auto InverseSelector>
300inline LIGHTWEIGHT_FORCE_INLINE OtherRecord const& HasMany<OtherRecord, InverseSelector>::At(std::size_t index) const
301{
302 const_cast<HasMany*>(this)->RequireLoaded();
303 return *_records->at(index); // NOLINT(bugprone-unchecked-optional-access)
304}
305
306template <typename OtherRecord, auto InverseSelector>
307inline LIGHTWEIGHT_FORCE_INLINE OtherRecord& HasMany<OtherRecord, InverseSelector>::At(std::size_t index)
308{
309 RequireLoaded();
310 return *_records->at(index); // NOLINT(bugprone-unchecked-optional-access)
311}
312
313template <typename OtherRecord, auto InverseSelector>
314inline LIGHTWEIGHT_FORCE_INLINE OtherRecord const& HasMany<OtherRecord, InverseSelector>::operator[](std::size_t index) const
315{
316 const_cast<HasMany*>(this)->RequireLoaded();
317 return *(*_records)[index]; // NOLINT(bugprone-unchecked-optional-access)
318}
319
320template <typename OtherRecord, auto InverseSelector>
321inline LIGHTWEIGHT_FORCE_INLINE OtherRecord& HasMany<OtherRecord, InverseSelector>::operator[](std::size_t index)
322{
323 RequireLoaded();
324 return *(*_records)[index]; // NOLINT(bugprone-unchecked-optional-access)
325}
326
327template <typename OtherRecord, auto InverseSelector>
328inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::iterator HasMany<OtherRecord,
329 InverseSelector>::begin()
330{
331 RequireLoaded();
332 return _records->begin(); // NOLINT(bugprone-unchecked-optional-access)
333}
334
335template <typename OtherRecord, auto InverseSelector>
337{
338 RequireLoaded();
339 return _records->end(); // NOLINT(bugprone-unchecked-optional-access)
340}
341
342template <typename OtherRecord, auto InverseSelector>
343inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::const_iterator HasMany<OtherRecord,
344 InverseSelector>::begin() const
345{
346 const_cast<HasMany*>(this)->RequireLoaded();
347 return _records->begin(); // NOLINT(bugprone-unchecked-optional-access)
348}
349
350template <typename OtherRecord, auto InverseSelector>
351inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::const_iterator HasMany<OtherRecord,
352 InverseSelector>::end() const
353{
354 const_cast<HasMany*>(this)->RequireLoaded();
355 return _records->end(); // NOLINT(bugprone-unchecked-optional-access)
356}
357
358} // namespace Lightweight
This HasMany<OtherRecord> represents a simple one-to-many relationship between two records.
Definition HasMany.hpp:65
OtherRecord const & At(std::size_t index) const
Retrieves the record at the given index.
Definition HasMany.hpp:300
ReferencedRecordList::iterator iterator
Iterator type for the list of records.
Definition HasMany.hpp:84
void Each(Callable const &callable)
Iterates over the list of records and calls the given callable for each record.
Definition HasMany.hpp:260
constexpr std::weak_ordering operator<=>(HasMany const &other) const noexcept=default
Three-way comparison operator.
ReferencedRecordList::const_iterator const_iterator
Const iterator type for the list of records.
Definition HasMany.hpp:87
ReferencedRecordList * LoadedRecords() noexcept
Returns the already-loaded records, or nullptr when the relation is not loaded.
Definition HasMany.hpp:119
OtherRecord ReferencedRecord
The record type of the "many" side of the relationship.
Definition HasMany.hpp:72
std::size_t Count() const noexcept
Retrieves the number of records in this 1-to-many relationship.
Definition HasMany.hpp:282
OtherRecord const & operator[](std::size_t index) const
Retrieves the record at the given index.
Definition HasMany.hpp:314
ReferencedRecordList & All()
Retrieves the list of records as mutable reference.
Definition HasMany.hpp:252
OtherRecord value_type
Record type of the "many" side of the relationship.
Definition HasMany.hpp:81
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
Definition HasMany.hpp:219
static constexpr auto InverseSelector
Singles out the foreign key of OtherRecord that backs this relationship.
Definition HasMany.hpp:75
std::vector< std::shared_ptr< OtherRecord > > ReferencedRecordList
The list of records on the "many" side of the relationship.
Definition HasMany.hpp:78
bool IsEmpty() const noexcept
Checks if this 1-to-many relationship is empty.
Definition HasMany.hpp:294
ReferencedRecordList const & All() const
Retrieves the list of loaded records.
Definition HasMany.hpp:275
ReferencedRecordList & Emplace(ReferencedRecordList &&records) noexcept
Emplaces the given list of records.
Definition HasMany.hpp:244
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.