added object_cache and test
This commit is contained in:
parent
a2c5bca709
commit
e45c38b229
|
|
@ -0,0 +1,339 @@
|
||||||
|
#ifndef MATADOR_OBJECT_CACHE_HPP
|
||||||
|
#define MATADOR_OBJECT_CACHE_HPP
|
||||||
|
|
||||||
|
#include "matador/object/object_proxy.hpp"
|
||||||
|
#include "matador/object/object_resolver.hpp"
|
||||||
|
|
||||||
|
#include "matador/utils/identifier.hpp"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <memory>
|
||||||
|
#include <shared_mutex>
|
||||||
|
#include <typeindex>
|
||||||
|
#include <utility>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
namespace matador::object {
|
||||||
|
struct cache_entry_base {
|
||||||
|
virtual ~cache_entry_base() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gibt an, ob der Eintrag vollständig "tot" ist (weder Proxy noch Entity leben).
|
||||||
|
*
|
||||||
|
* Wird von @ref object_cache::sweep() verwendet, um Einträge ohne T-Kenntnis zu bereinigen.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual bool is_dead() const noexcept = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct cache_entry : cache_entry_base {
|
||||||
|
std::weak_ptr<object_proxy<T> > proxy;
|
||||||
|
std::weak_ptr<T> entity;
|
||||||
|
|
||||||
|
[[nodiscard]] bool is_dead() const noexcept override {
|
||||||
|
return proxy.expired() && entity.expired();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Thread-sicherer Cache für Objekt-Proxies und (optional) geladene Entities.
|
||||||
|
*
|
||||||
|
* Der Cache verwaltet Einträge pro Kombination aus Objekt-Typ @p T und Primärschlüssel/Identifier.
|
||||||
|
* Für jede (T, id)-Kombination können sowohl
|
||||||
|
* - ein Proxy (@c object_proxy<T>) als auch
|
||||||
|
* - die dazugehörige Entity (@c T)
|
||||||
|
* zwischengespeichert werden.
|
||||||
|
*
|
||||||
|
* Der Cache hält dabei nur @c std::weak_ptr auf Proxy und Entity, d.h. er verlängert deren Lebensdauer nicht.
|
||||||
|
*
|
||||||
|
* @note Thread-Safety: Alle öffentlichen Methoden sind threadsafe (intern synchronisiert).
|
||||||
|
* @note Semantik: Ein Eintrag kann aus dem Cache opportunistisch entfernt werden, sobald Proxy und Entity abgelaufen sind.
|
||||||
|
*/
|
||||||
|
class object_cache {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Erzeugt einen leeren Cache.
|
||||||
|
*/
|
||||||
|
object_cache() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Liefert einen Proxy für (T, id) und erstellt ihn bei Bedarf.
|
||||||
|
*
|
||||||
|
* Falls bereits ein lebender Proxy im Cache existiert, wird er wiederverwendet.
|
||||||
|
* Andernfalls wird ein neuer Proxy erzeugt, im Cache abgelegt und zurückgegeben.
|
||||||
|
* Falls für (T, id) bereits eine Entity vorhanden ist, wird sie an den Proxy gebunden.
|
||||||
|
*
|
||||||
|
* @tparam T Entity-Typ.
|
||||||
|
* @tparam ResolverPtr Zeiger-Typ auf einen Resolver, typischerweise
|
||||||
|
* @c std::shared_ptr<object_resolver<T>> oder @c std::weak_ptr<object_resolver<T>>.
|
||||||
|
* @param id Identifier/Primärschlüssel der Entity.
|
||||||
|
* @param resolver_ptr Resolver (shared/weak), der zur Lazy-Auflösung durch den Proxy genutzt wird.
|
||||||
|
* @return Shared-Pointer auf den (ggf. neu erstellten) Proxy.
|
||||||
|
*
|
||||||
|
* @note Diese Methode ist threadsafe.
|
||||||
|
*/
|
||||||
|
template<typename T, typename ResolverPtr>
|
||||||
|
std::shared_ptr<object_proxy<T>> acquire_proxy(utils::identifier id, ResolverPtr &&resolver_ptr) {
|
||||||
|
const auto k = make_key<T>(id);
|
||||||
|
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
|
||||||
|
auto it = map_.find(k);
|
||||||
|
if (it != map_.end()) {
|
||||||
|
// found entry, return std::shared_ptr of proxy
|
||||||
|
auto *entry = entry_cast_<T>(it->second.get());
|
||||||
|
if (auto proxy_ptr = entry->proxy.lock()) {
|
||||||
|
return proxy_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the proxy is dead, but the entity is alive, create a new proxy and attach entity
|
||||||
|
auto weak_resolver = to_weak<T>(std::forward<ResolverPtr>(resolver_ptr));
|
||||||
|
auto proxy_ptr = std::make_shared<object_proxy<T>>(weak_resolver, id);
|
||||||
|
|
||||||
|
if (auto obj = entry->entity.lock()) {
|
||||||
|
proxy_ptr->attach(std::move(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->proxy = proxy_ptr;
|
||||||
|
|
||||||
|
// return the shared_ptr of the proxy
|
||||||
|
return proxy_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// entry couldn't be found, create a new entry
|
||||||
|
auto entry_ptr = std::make_unique<cache_entry<T>>();
|
||||||
|
auto *entry = entry_ptr.get();
|
||||||
|
|
||||||
|
// create a weak resolver and shared proxy
|
||||||
|
auto weak_resolver = to_weak<T>(std::forward<ResolverPtr>(resolver_ptr));
|
||||||
|
auto proxy_ptr = std::make_shared<object_proxy<T>>(weak_resolver, id);
|
||||||
|
|
||||||
|
// lock entity and attach to proxy
|
||||||
|
if (auto obj = entry->entity.lock()) {
|
||||||
|
proxy_ptr->attach(std::move(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the new proxy into cache entry
|
||||||
|
entry->proxy = proxy_ptr;
|
||||||
|
map_.emplace(k, std::move(entry_ptr));
|
||||||
|
|
||||||
|
// return the shared_ptr of the proxy
|
||||||
|
return proxy_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Verknüpft eine geladene Entity mit einem Cache-Eintrag (Type, id).
|
||||||
|
*
|
||||||
|
* Speichert die Entity als @c weak_ptr im Cache. Falls bereits ein Proxy existiert,
|
||||||
|
* wird die Entity sofort an den Proxy gebunden.
|
||||||
|
*
|
||||||
|
* @tparam Type Entity-Typ.
|
||||||
|
* @param id Identifier/Primärschlüssel der Entity.
|
||||||
|
* @param obj Geladene Entity als @c shared_ptr.
|
||||||
|
*
|
||||||
|
* @note Diese Methode ist threadsafe.
|
||||||
|
*/
|
||||||
|
template<typename Type>
|
||||||
|
void attach_entity(const utils::identifier &id, std::shared_ptr<Type> obj) {
|
||||||
|
const auto k = make_key<Type>(id);
|
||||||
|
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
|
||||||
|
auto it = map_.find(k);
|
||||||
|
if (it == map_.end()) {
|
||||||
|
auto entry_ptr = std::make_unique<cache_entry<Type>>();
|
||||||
|
auto *entry = entry_ptr.get();
|
||||||
|
entry->entity = obj;
|
||||||
|
map_.emplace(k, std::move(entry_ptr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *entry = entry_cast_<Type>(it->second.get());
|
||||||
|
entry->entity = obj;
|
||||||
|
|
||||||
|
if (auto proxy = entry->proxy.lock()) {
|
||||||
|
proxy->attach(std::move(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
prune_if_dead(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Liefert die aktuell geladene Entity für (Type, id), falls vorhanden.
|
||||||
|
*
|
||||||
|
* @tparam Type Entity-Typ.
|
||||||
|
* @param id Identifier/Primärschlüssel der Entity.
|
||||||
|
* @return @c shared_ptr auf die Entity oder ein leerer Pointer, falls nicht geladen/abgelaufen.
|
||||||
|
*
|
||||||
|
* @note Diese Methode ist threadsafe.
|
||||||
|
*/
|
||||||
|
template<typename Type>
|
||||||
|
std::shared_ptr<Type> get_entity(const utils::identifier &id) {
|
||||||
|
const auto k = make_key<Type>(id);
|
||||||
|
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
auto it = map_.find(k);
|
||||||
|
if (it == map_.end()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *entry = entry_cast_<Type>(it->second.get());
|
||||||
|
auto entity_ptr = entry->entity.lock();
|
||||||
|
prune_if_dead(it);
|
||||||
|
return entity_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prüft, ob für (Type, id) aktuell eine lebende Entity verfügbar ist.
|
||||||
|
*
|
||||||
|
* @tparam Type Entity-Typ.
|
||||||
|
* @param id Identifier/Primärschlüssel der Entity.
|
||||||
|
* @return @c true, wenn die Entity-Referenz nicht abgelaufen ist; sonst @c false.
|
||||||
|
*
|
||||||
|
* @note Diese Methode ist threadsafe.
|
||||||
|
*/
|
||||||
|
template<typename Type>
|
||||||
|
bool is_loaded(const utils::identifier &id) {
|
||||||
|
const auto k = make_key<Type>(id);
|
||||||
|
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
auto it = map_.find(k);
|
||||||
|
if (it == map_.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *entry = entry_cast_<Type>(it->second.get());
|
||||||
|
const bool loaded = !entry->entity.expired();
|
||||||
|
prune_if_dead(it);
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void erase(utils::identifier id) {
|
||||||
|
const auto k = make_key<T>(id);
|
||||||
|
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
auto it = map_.find(k);
|
||||||
|
if (it == map_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *e = entry_cast_<T>(it->second.get());
|
||||||
|
if (auto p = e->proxy.lock()) {
|
||||||
|
p->invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
map_.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Führt einen Cleanup-Lauf aus und entfernt Einträge, deren Proxy und Entity abgelaufen sind.
|
||||||
|
*
|
||||||
|
* @return Anzahl der entfernten Einträge.
|
||||||
|
*
|
||||||
|
* @note Diese Methode ist threadsafe.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::size_t sweep() {
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
std::size_t removed = 0;
|
||||||
|
|
||||||
|
for (auto it = map_.begin(); it != map_.end(); ) {
|
||||||
|
if (it->second->is_dead()) {
|
||||||
|
it = map_.erase(it);
|
||||||
|
++removed;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct key {
|
||||||
|
std::type_index type;
|
||||||
|
utils::identifier id{};
|
||||||
|
bool operator==(key const &other) const {
|
||||||
|
return type == other.type && id == other.id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct key_hash {
|
||||||
|
size_t operator()(key const &k) const noexcept {
|
||||||
|
// (3) robustere Hash-Kombination als XOR
|
||||||
|
size_t seed = std::hash<std::type_index>()(k.type);
|
||||||
|
const size_t h2 = std::hash<utils::identifier>()(k.id);
|
||||||
|
seed ^= h2 + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static key make_key(const utils::identifier &id) {
|
||||||
|
return key{std::type_index(typeid(T)), id};
|
||||||
|
}
|
||||||
|
|
||||||
|
// (4) Debug-Absicherung für type-erased Casts
|
||||||
|
template<typename T>
|
||||||
|
static cache_entry<T> *entry_cast_(cache_entry_base *base) {
|
||||||
|
#ifndef NDEBUG
|
||||||
|
auto *p = dynamic_cast<cache_entry<T> *>(base);
|
||||||
|
assert(p && "object_cache: Type mismatch in cache entry (key/type invariant violated)");
|
||||||
|
return p;
|
||||||
|
#else
|
||||||
|
return static_cast<cache_entry<T> *>(base);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T, typename U>
|
||||||
|
using enable_if_resolver_shared_ptr_t =
|
||||||
|
std::enable_if_t<std::is_base_of_v<object_resolver<T>, U>, int>;
|
||||||
|
|
||||||
|
template<typename T, typename U>
|
||||||
|
using enable_if_resolver_weak_ptr_t =
|
||||||
|
std::enable_if_t<std::is_base_of_v<object_resolver<T>, U>, int>;
|
||||||
|
|
||||||
|
// shared_ptr<Derived> -> weak_ptr<Base>
|
||||||
|
template<typename T, typename U, enable_if_resolver_shared_ptr_t<T, U> = 0>
|
||||||
|
static std::weak_ptr<object_resolver<T>> to_weak(const std::shared_ptr<U> &s) {
|
||||||
|
return std::weak_ptr<object_resolver<T>>(std::static_pointer_cast<object_resolver<T>>(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
// weak_ptr<Derived> -> weak_ptr<Base>
|
||||||
|
template<typename T, typename U, enable_if_resolver_weak_ptr_t<T, U> = 0>
|
||||||
|
static std::weak_ptr<object_resolver<T>> to_weak(const std::weak_ptr<U> &w) {
|
||||||
|
return std::weak_ptr<object_resolver<T>>(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) Opportunistischer Cleanup: entferne Entry, wenn beide weak_ptr abgelaufen sind.
|
||||||
|
template<typename It>
|
||||||
|
bool prune_if_dead(It &it) {
|
||||||
|
if (it == map_.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (it->second && it->second->is_dead()) {
|
||||||
|
map_.erase(it);
|
||||||
|
it = map_.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void prune_if_dead_typed(typename std::unordered_map<key, std::unique_ptr<cache_entry_base>, key_hash>::iterator &it) {
|
||||||
|
(void)T{}; // T bleibt im Interface, damit bestehende Call-Sites unverändert bleiben können.
|
||||||
|
prune_if_dead(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::shared_mutex mutex_{};
|
||||||
|
std::unordered_map<key, std::unique_ptr<cache_entry_base>, key_hash> map_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Inline Anpassungen: typed prune nach Zugriff (damit wir T haben) ---
|
||||||
|
|
||||||
|
// ... existing code ...
|
||||||
|
// (Die typed prune wird oben benutzt; wir rufen sie direkt im Codepfad anstelle von prune_if_dead_)
|
||||||
|
|
||||||
|
} // namespace matador::object
|
||||||
|
#endif //MATADOR_OBJECT_CACHE_HPP
|
||||||
|
|
@ -44,6 +44,22 @@ public:
|
||||||
, pk_(primary_key_resolver::resolve_object(*obj).pk) {
|
, pk_(primary_key_resolver::resolve_object(*obj).pk) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void attach(std::shared_ptr<Type> obj) {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
obj_ = std::move(obj);
|
||||||
|
if (obj_) {
|
||||||
|
pk_ = primary_key_resolver::resolve_object(*obj_).pk;
|
||||||
|
state_.store(object_state::Persistent, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void invalidate() {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
obj_.reset();
|
||||||
|
resolver_.reset();
|
||||||
|
state_.store(object_state::Detached, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] void *raw_pointer() const { return static_cast<void *>(pointer()); }
|
[[nodiscard]] void *raw_pointer() const { return static_cast<void *>(pointer()); }
|
||||||
|
|
||||||
Type *operator->() { return pointer(); }
|
Type *operator->() { return pointer(); }
|
||||||
|
|
@ -52,7 +68,7 @@ public:
|
||||||
|
|
||||||
Type *pointer() const { return resolve(); }
|
Type *pointer() const { return resolve(); }
|
||||||
|
|
||||||
[[nodiscard]] bool empty() const { return resolver_ == nullptr; }
|
[[nodiscard]] bool empty() const { return !obj_ && resolver_.expired(); }
|
||||||
[[nodiscard]] bool valid() const { return !empty(); }
|
[[nodiscard]] bool valid() const { return !empty(); }
|
||||||
[[nodiscard]] bool has_primary_key() const { return !pk_.is_null(); }
|
[[nodiscard]] bool has_primary_key() const { return !pk_.is_null(); }
|
||||||
[[nodiscard]] const utils::identifier &primary_key() const { return pk_; }
|
[[nodiscard]] const utils::identifier &primary_key() const { return pk_; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
#ifndef MATADOR_DEPENDENCY_COLLECTOR_HPP
|
||||||
|
#define MATADOR_DEPENDENCY_COLLECTOR_HPP
|
||||||
|
|
||||||
|
#include "matador/utils/field_attributes.hpp"
|
||||||
|
#include "matador/utils/foreign_attributes.hpp"
|
||||||
|
#include "matador/utils/primary_key_attribute.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <typeindex>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace matador::query {
|
||||||
|
enum class pk_strategy_kind {
|
||||||
|
manual,
|
||||||
|
sequence,
|
||||||
|
table,
|
||||||
|
identity
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class pk_state {
|
||||||
|
unknown, // z.B. identity vor insert, oder manual aber nicht gesetzt
|
||||||
|
known // id ist gesetzt/verfügbar
|
||||||
|
};
|
||||||
|
|
||||||
|
struct fk_ref {
|
||||||
|
std::string fk_column; // z.B. "author_id" (optional, für Debug/SQL)
|
||||||
|
void* parent_object_ptr; // Adresse des referenzierten Objekts (type-erased)
|
||||||
|
std::type_index parent_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct pk_accessor {
|
||||||
|
// obj zeigt auf konkrete Entity-Instanz
|
||||||
|
bool (*is_known)(void* obj) = nullptr;
|
||||||
|
void (*set_u64)(void* obj, std::uint64_t id) = nullptr;
|
||||||
|
std::uint64_t (*get_u64)(void* obj) = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct entity_meta {
|
||||||
|
pk_strategy_kind strategy{};
|
||||||
|
pk_accessor pk;
|
||||||
|
// optional: table name, pk column name, etc.
|
||||||
|
};
|
||||||
|
|
||||||
|
struct meta_registry {
|
||||||
|
std::unordered_map<std::type_index, entity_meta> by_type;
|
||||||
|
|
||||||
|
const entity_meta& get(const std::type_index t) const {
|
||||||
|
return by_type.at(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
void register_type(entity_meta m) {
|
||||||
|
by_type.emplace(std::type_index(typeid(T)), m);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct dependency_collector {
|
||||||
|
std::vector<fk_ref> refs;
|
||||||
|
|
||||||
|
template<class V>
|
||||||
|
static void on_primary_key(const char*, V&, const utils::primary_key_attribute& /*attr*/ = utils::default_pk_attributes) {}
|
||||||
|
|
||||||
|
static void on_revision(const char*, std::uint64_t&) {}
|
||||||
|
|
||||||
|
template<class V>
|
||||||
|
static void on_attribute(const char*, V&, const utils::field_attributes &/*attr*/ = utils::null_attributes) {}
|
||||||
|
|
||||||
|
template<class Pointer>
|
||||||
|
void on_belongs_to(const char* id, Pointer& p, utils::foreign_attributes &attr) {
|
||||||
|
// Erwartung: Pointer verhält sich wie matador::object::object_ptr<T>
|
||||||
|
// also: p.get() liefert raw ptr, und Pointer::value_type / element_type ist T.
|
||||||
|
auto* raw = p.get();
|
||||||
|
if (!raw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using parent_type = typename Pointer::value_type; // falls object_ptr<T> so definiert ist
|
||||||
|
// Falls dein object_ptr anders heißt (z.B. element_type), hier anpassen.
|
||||||
|
|
||||||
|
refs.push_back(fk_ref{
|
||||||
|
id ? std::string{id} : std::string{},
|
||||||
|
static_cast<void*>(raw),
|
||||||
|
typeid(parent_type)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Pointer>
|
||||||
|
static void on_has_one(const char*, Pointer&, const auto&) {}
|
||||||
|
|
||||||
|
template<class Container>
|
||||||
|
static void on_has_many(const char*, Container&, const char*, const auto&) {}
|
||||||
|
|
||||||
|
template<class Container>
|
||||||
|
static void on_has_many_to_many(const char*, Container&, const char*, const char*, const auto&) {}
|
||||||
|
|
||||||
|
template<class Container>
|
||||||
|
static void on_has_many_to_many(const char*, Container&, const auto&) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct flush_node {
|
||||||
|
void* object_ptr = nullptr; // raw pointer (nicht owning)
|
||||||
|
std::type_index type = typeid(void);
|
||||||
|
|
||||||
|
pk_strategy_kind pk_strategy = pk_strategy_kind::manual;
|
||||||
|
pk_accessor pk{};
|
||||||
|
|
||||||
|
pk_state state = pk_state::unknown;
|
||||||
|
|
||||||
|
// eingehende Dependencies: child hängt von parents ab
|
||||||
|
std::vector<std::size_t> depends_on; // Indizes in nodes[]
|
||||||
|
bool inserted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct flush_plan {
|
||||||
|
std::vector<flush_node> nodes;
|
||||||
|
|
||||||
|
// map raw ptr -> node index
|
||||||
|
std::unordered_map<void*, std::size_t> index_by_ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
flush_plan build_nodes(const std::vector<std::pair<void*, std::type_index>>& new_objects, const meta_registry& reg) {
|
||||||
|
flush_plan plan;
|
||||||
|
plan.nodes.reserve(new_objects.size());
|
||||||
|
|
||||||
|
for (const auto& [ptr, ti] : new_objects) {
|
||||||
|
const auto& meta = reg.get(ti);
|
||||||
|
|
||||||
|
flush_node n;
|
||||||
|
n.object_ptr = ptr;
|
||||||
|
n.type = ti;
|
||||||
|
n.pk_strategy = meta.strategy;
|
||||||
|
n.pk = meta.pk;
|
||||||
|
n.state = n.pk.is_known(n.object_ptr) ? pk_state::known : pk_state::unknown;
|
||||||
|
|
||||||
|
plan.index_by_ptr.emplace(ptr, plan.nodes.size());
|
||||||
|
plan.nodes.push_back(n);
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_edges_from_process(flush_plan& plan, const matador::query::process_registry& preg) {
|
||||||
|
using namespace matador::query;
|
||||||
|
for (std::size_t i = 0; i < plan.nodes.size(); ++i) { auto& node = plan.nodes[i];
|
||||||
|
dependency_collector dc;
|
||||||
|
preg.get(node.type).collect_deps(node.object_ptr, dc);
|
||||||
|
|
||||||
|
// 1) belongs_to: node (child) depends on parent
|
||||||
|
for (const auto& ref : dc.refs) {
|
||||||
|
auto it = plan.index_by_ptr.find(ref.parent_object_ptr);
|
||||||
|
if (it == plan.index_by_ptr.end()) {
|
||||||
|
// Parent ist nicht Teil des Plans (externe persistent entity?)
|
||||||
|
// Policy: wenn parent PK known -> ok; sonst Fehler oder cascade-add
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
node.depends_on.push_back(it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) has_many: inverse; daraus macht man Kanten child -> this(parent)
|
||||||
|
for (const auto& child : dc.has_many_children) {
|
||||||
|
auto it_child = plan.index_by_ptr.find(child.child_object_ptr);
|
||||||
|
if (it_child == plan.index_by_ptr.end()) continue;
|
||||||
|
|
||||||
|
auto& child_node = plan.nodes[it_child->second];
|
||||||
|
child_node.depends_on.push_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) many-to-many: als separate work items (nicht in depends_on quetschen)
|
||||||
|
// dc.many_to_many_links -> später join-table tasks planen
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
#endif //MATADOR_DEPENDENCY_COLLECTOR_HPP
|
||||||
|
|
@ -23,7 +23,6 @@ public:
|
||||||
virtual void serialize(uint16_t &, const field_attributes &) = 0;
|
virtual void serialize(uint16_t &, const field_attributes &) = 0;
|
||||||
virtual void serialize(uint32_t &, const field_attributes &) = 0;
|
virtual void serialize(uint32_t &, const field_attributes &) = 0;
|
||||||
virtual void serialize(uint64_t &, const field_attributes &) = 0;
|
virtual void serialize(uint64_t &, const field_attributes &) = 0;
|
||||||
virtual void serialize(const char *, const field_attributes &) = 0;
|
|
||||||
virtual void serialize(std::string &, const field_attributes &) = 0;
|
virtual void serialize(std::string &, const field_attributes &) = 0;
|
||||||
virtual void serialize(null_type_t &, const field_attributes &) = 0;
|
virtual void serialize(null_type_t &, const field_attributes &) = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -149,7 +148,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
explicit identifier(const char *id)
|
explicit identifier(const char *id)
|
||||||
: id_(std::make_shared<pk<const char *> >(id)) {
|
: id_(std::make_shared<pk<std::string> >(id)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
identifier(const identifier &x);
|
identifier(const identifier &x);
|
||||||
|
|
@ -164,7 +163,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
identifier &operator=(const char *value) {
|
identifier &operator=(const char *value) {
|
||||||
id_ = std::make_shared<pk<const char *> >(value);
|
id_ = std::make_shared<pk<std::string> >(value);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,4 +219,13 @@ struct id_pk_hash {
|
||||||
/// @endcond
|
/// @endcond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
template<>
|
||||||
|
struct hash<matador::utils::identifier> {
|
||||||
|
size_t operator()(const matador::utils::identifier &id) const noexcept {
|
||||||
|
return id.hash();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace std
|
||||||
|
|
||||||
#endif //MATADOR_IDENTIFIER_HPP
|
#endif //MATADOR_IDENTIFIER_HPP
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ public:
|
||||||
void serialize(uint16_t &, const field_attributes &) override;
|
void serialize(uint16_t &, const field_attributes &) override;
|
||||||
void serialize(uint32_t &, const field_attributes &) override;
|
void serialize(uint32_t &, const field_attributes &) override;
|
||||||
void serialize(uint64_t &, const field_attributes &) override;
|
void serialize(uint64_t &, const field_attributes &) override;
|
||||||
void serialize(const char *, const field_attributes &) override;
|
|
||||||
void serialize(std::string &, const field_attributes &) override;
|
void serialize(std::string &, const field_attributes &) override;
|
||||||
void serialize(null_type_t &, const field_attributes &) override;
|
void serialize(null_type_t &, const field_attributes &) override;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ add_library(matador-core STATIC
|
||||||
../../include/matador/object/collection_utils.hpp
|
../../include/matador/object/collection_utils.hpp
|
||||||
object/collection_utils.cpp
|
object/collection_utils.cpp
|
||||||
../../include/matador/object/pk_field_locator.hpp
|
../../include/matador/object/pk_field_locator.hpp
|
||||||
|
../../include/matador/object/object_cache.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(matador-core ${CMAKE_DL_LIBS})
|
target_link_libraries(matador-core ${CMAKE_DL_LIBS})
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,6 @@ void identifier_to_value_converter::serialize(uint64_t &x, const field_attribute
|
||||||
value_ = x;
|
value_ = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
void identifier_to_value_converter::serialize(const char *x, const field_attributes &) {
|
|
||||||
value_ = x;
|
|
||||||
}
|
|
||||||
|
|
||||||
void identifier_to_value_converter::serialize(std::string &x, const field_attributes &) {
|
void identifier_to_value_converter::serialize(std::string &x, const field_attributes &) {
|
||||||
value_ = x;
|
value_ = x;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ add_executable(CoreTests
|
||||||
utils/ThreadPoolTest.cpp
|
utils/ThreadPoolTest.cpp
|
||||||
utils/VersionTest.cpp
|
utils/VersionTest.cpp
|
||||||
utils/IsDatabasePrimitiveTest.cpp
|
utils/IsDatabasePrimitiveTest.cpp
|
||||||
|
object/ObjectCacheTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(CoreTests matador-core Catch2::Catch2WithMain)
|
target_link_libraries(CoreTests matador-core Catch2::Catch2WithMain)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
#include "matador/object/object_cache.hpp"
|
||||||
|
#include "matador/object/object_resolver.hpp"
|
||||||
|
#include "matador/utils/identifier.hpp"
|
||||||
|
|
||||||
|
#include "../test/models/person.hpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace matador::test;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class dummy_resolver final : public matador::object::object_resolver<person> {
|
||||||
|
public:
|
||||||
|
explicit dummy_resolver(std::shared_ptr<person> shared, std::atomic_int &calls)
|
||||||
|
: shared_(std::move(shared))
|
||||||
|
, calls_(calls) {}
|
||||||
|
|
||||||
|
std::shared_ptr<person> resolve(const matador::utils::identifier & /*id*/) override {
|
||||||
|
++calls_;
|
||||||
|
return shared_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<person> shared_;
|
||||||
|
std::atomic_int &calls_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class start_latch {
|
||||||
|
public:
|
||||||
|
explicit start_latch(int participants)
|
||||||
|
: participants_(participants) {}
|
||||||
|
|
||||||
|
void arrive_and_wait() {
|
||||||
|
std::unique_lock<std::mutex> lock(m_);
|
||||||
|
++arrived_;
|
||||||
|
if (arrived_ >= participants_) {
|
||||||
|
open_ = true;
|
||||||
|
cv_.notify_all();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cv_.wait(lock, [&] { return open_; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex m_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
int participants_{0};
|
||||||
|
int arrived_{0};
|
||||||
|
bool open_{false};
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("object_cache: acquire_proxy returns the same proxy instance across threads", "[object][cache][threadsafe]") {
|
||||||
|
matador::object::object_cache cache;
|
||||||
|
const matador::utils::identifier id{123};
|
||||||
|
|
||||||
|
std::atomic_int calls{0};
|
||||||
|
auto entity = std::make_shared<person>();
|
||||||
|
auto resolver = std::make_shared<dummy_resolver>(entity, calls);
|
||||||
|
|
||||||
|
constexpr int threads = 16;
|
||||||
|
start_latch latch(threads);
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<matador::object::object_proxy<person>>> proxies(threads);
|
||||||
|
std::vector<std::thread> ts;
|
||||||
|
ts.reserve(threads);
|
||||||
|
|
||||||
|
for (int i = 0; i < threads; ++i) {
|
||||||
|
ts.emplace_back([&, i]() {
|
||||||
|
latch.arrive_and_wait();
|
||||||
|
proxies[i] = cache.acquire_proxy<person>(id, resolver);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (auto &t : ts) t.join();
|
||||||
|
|
||||||
|
REQUIRE(proxies[0]);
|
||||||
|
const auto *p0 = proxies[0].get();
|
||||||
|
for (int i = 1; i < threads; ++i) {
|
||||||
|
REQUIRE(proxies[i]);
|
||||||
|
REQUIRE(proxies[i].get() == p0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("object_cache: attach_entity makes is_loaded/get_entity reflect presence", "[object][cache]") {
|
||||||
|
matador::object::object_cache cache;
|
||||||
|
const matador::utils::identifier id{42};
|
||||||
|
|
||||||
|
REQUIRE_FALSE(cache.is_loaded<person>(id));
|
||||||
|
REQUIRE(cache.get_entity<person>(id) == nullptr);
|
||||||
|
|
||||||
|
auto entity = std::make_shared<person>();
|
||||||
|
entity->name = "hans";
|
||||||
|
|
||||||
|
cache.attach_entity<person>(id, entity);
|
||||||
|
|
||||||
|
REQUIRE(cache.is_loaded<person>(id));
|
||||||
|
auto got = cache.get_entity<person>(id);
|
||||||
|
REQUIRE(got);
|
||||||
|
REQUIRE(got->name == "hans");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("object_cache: erase invalidates existing proxies", "[object][cache]") {
|
||||||
|
matador::object::object_cache cache;
|
||||||
|
const matador::utils::identifier id{9};
|
||||||
|
|
||||||
|
std::atomic_int calls{0};
|
||||||
|
auto entity = std::make_shared<person>();
|
||||||
|
auto resolver = std::make_shared<dummy_resolver>(entity, calls);
|
||||||
|
|
||||||
|
auto proxy = cache.acquire_proxy<person>(id, resolver);
|
||||||
|
REQUIRE(proxy);
|
||||||
|
REQUIRE(proxy->valid());
|
||||||
|
|
||||||
|
cache.erase<person>(id);
|
||||||
|
|
||||||
|
// Proxy lebt noch (shared_ptr), aber ist invalidiert.
|
||||||
|
REQUIRE_FALSE(proxy->valid());
|
||||||
|
REQUIRE(proxy->pointer() == nullptr);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue