added object_cache and test

This commit is contained in:
Sascha Kühl 2026-02-22 22:36:42 +01:00
parent a2c5bca709
commit e45c38b229
9 changed files with 668 additions and 9 deletions

View File

@ -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

View File

@ -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_; }

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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})

View File

@ -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;
} }

View File

@ -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)

View File

@ -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);
}