Lightweight 0.20260625.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 must declare one `BelongsTo` pointing back at the record owning this relationship,
22/// and one pointing at the referenced record. Both are located by matching the relationship *type*.
23///
24/// When the join record cannot be resolved that way - most notably a self-referential many-to-many,
25/// where both of its foreign keys point at the *same* table - name the two foreign key columns
26/// explicitly:
27///
28/// @code
29/// struct Friendship;
30/// struct Human
31/// {
32/// Field<int, PrimaryKey::AutoAssign> id;
33/// HasManyThrough<Human, Friendship, SqlRealName { "a_id" }, SqlRealName { "b_id" }> friends;
34/// };
35/// struct Friendship
36/// {
37/// Field<int, PrimaryKey::AutoAssign> id;
38/// BelongsTo<&Human::id, SqlRealName { "a_id" }> a;
39/// BelongsTo<&Human::id, SqlRealName { "b_id" }> b;
40/// };
41/// @endcode
42///
43/// @tparam ReferencedRecordT The record type on the "many" side of the relationship.
44/// @tparam ThroughRecordT The join record type.
45/// @tparam TheOwnerSelector Singles out the join record's foreign key pointing at the *owning* record.
46/// @tparam TheReferencedSelector Singles out the join record's foreign key pointing at @p ReferencedRecordT.
47///
48/// @see DataMapper, Field, HasMany, RelationSelector
49/// @ingroup DataMapper
50template <typename ReferencedRecordT,
51 typename ThroughRecordT,
52 auto TheOwnerSelector = AutoDetectRelation,
53 auto TheReferencedSelector = AutoDetectRelation>
55{
57 "The selector template arguments of HasManyThrough must be foreign key column names "
58 "(a SqlRealName) or std::nullopt to resolve the relationship automatically.");
59
60 public:
61 /// The record type of the "through" side of the relationship.
62 using ThroughRecord = ThroughRecordT;
63
64 /// The record type of the "many" side of the relationship.
65 using ReferencedRecord = ReferencedRecordT;
66
67 /// Singles out the join record's foreign key pointing at the record owning this relationship.
68 static constexpr auto OwnerSelector = TheOwnerSelector;
69
70 /// Singles out the join record's foreign key pointing at @ref ReferencedRecord.
71 static constexpr auto ReferencedSelector = TheReferencedSelector;
72
73 /// The list of records on the "many" side of the relationship.
74 using ReferencedRecordList = std::vector<std::shared_ptr<ReferencedRecord>>;
75
76 /// Value type for range-based iteration.
78 /// Iterator type for the list of records.
79 using iterator = ReferencedRecordList::iterator;
80 /// Const iterator type for the list of records.
81 using const_iterator = ReferencedRecordList::const_iterator;
82
83 /// Retrieves the list of loaded records.
84 [[nodiscard]] ReferencedRecordList const& All() const noexcept;
85
86 /// Retrieves the list of records as mutable reference.
87 [[nodiscard]] ReferencedRecordList& All() noexcept;
88
89 /// Emplaces the given list of records into this relationship.
91
92 /// Retrieves the number of records in this relationship.
93 [[nodiscard]] std::size_t Count() const;
94
95 /// Checks if this relationship is empty.
96 [[nodiscard]] bool IsEmpty() const;
97
98 /// @brief Retrieves the record at the given index.
99 ///
100 /// @param index The index of the record to retrieve.
101 /// @note This method will on-demand load the records if they are not already loaded.
102 /// @note This method will throw if the index is out of bounds.
103 [[nodiscard]] ReferencedRecord const& At(std::size_t index) const;
104
105 /// @brief Retrieves the record at the given index.
106 ///
107 /// @param index The index of the record to retrieve.
108 /// @note This method will on-demand load the records if they are not already loaded.
109 /// @note This method will throw if the index is out of bounds.
110 [[nodiscard]] ReferencedRecord& At(std::size_t index);
111
112 /// @brief Retrieves the record at the given index.
113 ///
114 /// @param index The index of the record to retrieve.
115 /// @note This method will on-demand load the records if they are not already loaded.
116 /// @note This method will NOT throw if the index is out of bounds. The behaviour is undefined.
117 [[nodiscard]] ReferencedRecord const& operator[](std::size_t index) const;
118
119 /// @brief Retrieves the record at the given index.
120 ///
121 /// @param index The index of the record to retrieve.
122 /// @note This method will on-demand load the records if they are not already loaded.
123 /// @note This method will NOT throw if the index is out of bounds. The behaviour is undefined.
124 [[nodiscard]] ReferencedRecord& operator[](std::size_t index);
125
126 /// Returns an iterator to the beginning of the record list.
127 [[nodiscard]] iterator begin() noexcept;
128 /// Returns an iterator to the end of the record list.
129 [[nodiscard]] iterator end() noexcept;
130 /// Returns a const iterator to the beginning of the record list.
131 [[nodiscard]] const_iterator begin() const noexcept;
132 /// Returns a const iterator to the end of the record list.
133 [[nodiscard]] const_iterator end() const noexcept;
134
135 /// Default three-way comparison operator.
136 std::weak_ordering operator<=>(HasManyThrough const& other) const noexcept = default;
137
138 struct Loader
139 {
140 std::function<size_t()> count;
141 std::function<ReferencedRecordList()> all;
142 std::function<void(std::function<void(ReferencedRecord const&)>)> each;
143 };
144
145 /// Used internally to configure on-demand loading of the records.
146 void SetAutoLoader(Loader loader) noexcept
147 {
148 _loader = std::move(loader);
149 }
150
151 /// Reloads the records from the database.
152 void Reload()
153 {
154 _count = std::nullopt;
155 _records = std::nullopt;
156 RequireLoaded();
157 }
158
159 /// @brief Iterates over all records in this relationship.
160 ///
161 /// @param callable The callable to invoke for each record.
162 /// @note This method will on-demand load the records if they are not already loaded,
163 /// but not hold them all in memory.
164 template <typename Callable>
165 void Each(Callable const& callable)
166 {
167 if (!_records && _loader.each)
168 {
169 _loader.each(callable);
170 return;
171 }
172
173 for (auto const& record: All())
174 callable(*record);
175 }
176
177 private:
178 void RequireLoaded()
179 {
180 if (_records)
181 return;
182
183 if (_loader.all)
184 _records = _loader.all();
185
186 if (!_records)
187 throw SqlRequireLoadedError(Reflection::TypeNameOf<std::remove_cvref_t<decltype(*this)>>);
188 }
189
190 Loader _loader;
191
192 std::optional<size_t> _count;
193 std::optional<ReferencedRecordList> _records;
194};
195
196namespace detail
197{
198 template <typename T>
199 struct IsHasManyThroughType: std::false_type
200 {
201 };
202
203 template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
204 struct IsHasManyThroughType<HasManyThrough<ReferencedRecordT, ThroughRecordT, OwnerSelector, ReferencedSelector>>:
205 std::true_type
206 {
207 };
208
209} // namespace detail
210
211template <typename T>
212constexpr bool IsHasManyThrough = detail::IsHasManyThroughType<std::remove_cvref_t<T>>::value;
213
214template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
217{
218 const_cast<HasManyThrough*>(this)->RequireLoaded();
219
220 return _records.value();
221}
222
223template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
226{
227 RequireLoaded();
228
229 return _records.value(); // NOLINT(bugprone-unchecked-optional-access)
230}
231
232template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
234 ReferencedRecordT,
235 ThroughRecordT,
236 OwnerSelector,
237 ReferencedSelector>::Emplace(ReferencedRecordList&& records) noexcept
238{
239 _records = { std::move(records) };
240 _count = _records->size();
241 return *_records;
242}
243
244template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
246{
247 if (_records)
248 return _records->size();
249
250 if (!_count)
252 _loader.count();
253
254 return _count.value_or(0);
255}
256
257template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
262
263template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
266{
267 return *All().at(index);
268}
269
270template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
276
277template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
283
284template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
290
291template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
297
298template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
304
305template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
311
312template <typename ReferencedRecordT, typename ThroughRecordT, auto OwnerSelector, auto ReferencedSelector>
318
319} // namespace Lightweight
This API represents a many-to-many relationship between two records through a third record.
ReferencedRecordList::iterator iterator
Iterator type for the list of records.
iterator end() noexcept
Returns an iterator to the end of the record list.
ReferencedRecordT ReferencedRecord
The record type of the "many" side of the relationship.
iterator begin() noexcept
Returns an iterator to the beginning of the record list.
bool IsEmpty() const
Checks if this relationship is empty.
ReferencedRecordList & Emplace(ReferencedRecordList &&records) noexcept
Emplaces the given list of records into this relationship.
static constexpr auto OwnerSelector
Singles out the join record's foreign key pointing at the record owning this relationship.
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::const_iterator const_iterator
Const iterator type for the list of records.
ReferencedRecordList const & All() const noexcept
Retrieves the list of loaded records.
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
ReferencedRecord const & operator[](std::size_t index) const
Retrieves the record at the given index.
ThroughRecordT ThroughRecord
The record type of the "through" side of the relationship.
static constexpr auto ReferencedSelector
Singles out the join record's foreign key pointing at ReferencedRecord.
std::vector< std::shared_ptr< ReferencedRecord > > ReferencedRecordList
The list of records on the "many" side of the relationship.
void Each(Callable const &callable)
Iterates over all records in this relationship.
ReferencedRecord const & At(std::size_t index) const
Retrieves the record at the given index.
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:132
constexpr std::nullopt_t AutoDetectRelation
Selector value meaning "resolve the relationship automatically; the match must be unique".
Definition Record.hpp:117