Project

General

Profile

Feature #10930

Support Polymorphism with Dbo Hierarchy

Added by Christian Meyer 18 days ago. Updated 13 days ago.

Status:
New
Priority:
Normal
Assignee:
-
Target version:
-
Start date:
09/10/2022
Due date:
% Done:

0%

Estimated time:

Description

There have been questions about how to implement this in the Forum

https://redmine.emweb.be/boards/2/topics/18066
https://redmine.emweb.be/boards/2/topics/17642
https://redmine.emweb.be/boards/2/topics/298

I also would like to have the ability to inherit from a Dbo-Table-Class, save the base portion of data within that table and add another table for the derived portion of data.

At the same time, It would help to return a polymorphic pointer to the derived class, if the corresponding base-class was extracted from the db.

I did some thinking, and a lot of reading through the codebase, but a 1-1 relation is impossible without knowing the derived class in the base. At the moment.

What I'd like to propose is a relation schema that might be able to include those features, merely by expanding the types of relations...

This would enable the use of Base Dbo-Table-Classes by themselfes, as well as enable the reuse in other Dbo-Table-Classes, Data and all.

As this is purley theoretical and have not been able to test it, I hope I can convey my thoughts about a possible implementation well enough.

new Functions:
Wt::Dbo::baseClass(a); -> to be added in every Class that wants to enable Polymorphism
Wt::Dbo::extends<BaseClass>(a); -> to be added in Derived classes.
Derived from Multiple Dbo-Classes: add another extends().
Want to enable deriving from this: add baseClass()

In Effect:

  • Dbo::baseClass():
    • creates a table <tableName>_Base
  • Dbo::extends()
    • adds Fields and constraints to the extended _Base table
    • creates a table <tableName>_Derived
    • adds Fields and constraints for every extended <BaseClass> to _Derived table

The tables for the relations could look like this:

create table "<tableName>_Base" (
  "base_id" bigint not null,
  "derived_<derived_tablename>_id" bigint, /* add field and constraint for every derived class: */
  constraint "fk_<tablename>_<derived_tablename>_key1" foreign key ("base_<tablename>_id") references "<tablename>" ("id") on delete cascade,
  constraint "fk_<tablename>_<derived_tablename>_key2" foreign key ("derived_<derived_tablename>_id") references "<derived_tablename>" ("id") on delete cascade
  /* Not sure on the primary key definition here */
);

create table "<tableName>_Derived" (
  "derived_id" bigint not null,
  "base_<base_tablename>_id" bigint, /*<add field and constraint for every base class: */
  constraint "fk_<base_tablename>_<tablename>_key1" foreign key ("derived_<tablename>_id") references "<tablename>" ("id") on delete cascade,
  constraint "fk_<base_tablename>_<tablename>_key2" foreign key ("base_<base_tablename>_id") references "<base_tablename>" ("id") on delete cascade
  /* Not sure on the primary key definition here */
);

With an Example Structure like this:

class User
{
protected:
    std::string email;

public:
    template <class Action>
    void persist(Action &a)
    {
        Wt::Dbo::field(a, email, "email");
        Wt::Dbo::baseClass(a);
    }
};
class Customer : public User
{
protected:
    std::string address;

public:
    template <class Action>
    void persist(Action &a)
    {
        Wt::Dbo::field(a, address, "address");
        Wt::Dbo::extends<User>(a);
    }
};
class Manager : public User
{
protected:
    std::string department;

public:
    template <class Action>
    void persist(Action &a)
    {
        Wt::Dbo::field(a, department, "department");
        Wt::Dbo::extends<User>(a);
    }
};

The Tables could look like that:
Table: User_Base

base_id derived_Customer_id derived_Manager_id
2 1 null
3 null 1

Table: Customer_Derived

derived_id base_User_id
1 2

Table: Manager_Derived

derived_id base_User_id
1 3

Having those extra Tables should help to get all the data together.
I think that might be relatively "easy" to implement. Unfortunately I don't see where or how this might be needed in the Source.
And it would help to have an adaption for Dbo::ptr<C>::get() to return the polymorphic Element pointer.

Those tables should enable multiple parents and deriving over multiple steps as well, at least in theory.
Looking forward to a possible implementation, or some thoughts about how to do it differently.

#1

Updated by Christian Meyer 15 days ago

There are issues with the self() function if Inheriting from a Wt::Dbo::Dbo

class BaseClass : public Wt::Dbo::Dbo<BaseClass
{
};
class NextClass : public BaseClass
{
Wt::Dbo::ptr<NextClass> me(){ return self(); } // call to self is ambiguous
};
class NextDboClass : public BaseClass, public Wt::Dbo::Dbo<NextDboClass>
{
Wt::Dbo::ptr<NextDboClass> me(){ return self(); } // call to self is ambiguous // in both variations
};
error: reference to 'self' is ambiguous
/usr/local/include/Wt/Dbo/ptr_impl.h:692:8: note: candidates are: 'Wt::Dbo::ptr<C> Wt::Dbo::Dbo<C>::self() const [with C = NextClass]'
  692 | ptr<C> Dbo<C>::self() const
      |        ^~~~~~
/usr/local/include/Wt/Dbo/ptr_impl.h:692:8: note:                 'Wt::Dbo::ptr<C> Wt::Dbo::Dbo<C>::self() const [with C = BaseClass]'

error: request for member 'meta_' is ambiguous
  502 |   static void setMeta(C& obj, MetaDbo<C> *m) { obj.meta_ = m; }
      |                                                ~~~~^~~~~
/usr/local/include/Wt/Dbo/ptr.h:494:15: note: candidates are: 'Wt::Dbo::MetaDbo<NextClass>* Wt::Dbo::Dbo<NextClass>::meta_'
  494 |   MetaDbo<C> *meta_;
      |               ^~~~~
/usr/local/include/Wt/Dbo/ptr.h:494:15: note:                 'Wt::Dbo::MetaDbo<BaseClass>* Wt::Dbo::Dbo<BaseClass>::meta_'
#2

Updated by Christian Meyer 13 days ago

I managed to implement something:

// ConnectorTable.h
template <class Base, class Derived>
class ConnectorTable
{
protected:
  Wt::Dbo::ptr<Base> basePtr;
  Wt::Dbo::ptr<Derived> derivedPtr;

public:
  static std::string fieldName()
  {
    return Wt::WString("derived_{1}_to_{2}")
    .arg(Base::refName())
    .arg(Derived::refName())
    .toUTF8();
  }
  static std::string refName()
  {
    return Wt::WString("{1}_{2}_Connector")
    .arg(Base::refName())
    .arg(Derived::refName())
    .toUTF8();
  }

  ConnectorTable() { }
  ConnectorTable(Wt::Dbo::ptr<Base> bPtr, Wt::Dbo::ptr<Derived> dPtr)
    : basePtr(bPtr), derivedPtr(dPtr) {}

  virtual ~ConnectorTable() {}

  Wt::Dbo::ptr<Derived> getDerived() const { return derivedPtr; }
  Wt::Dbo::ptr<Base> getBase() const { return basePtr; }

  template <class Action>
  void persist(Action &a)
  {
    Wt::Dbo::belongsTo(
        a, 
        basePtr, 
        "base", 
        Wt::Dbo::OnDeleteCascade | Wt::Dbo::OnUpdateCascade
    );
    Wt::Dbo::belongsTo(
        a, 
        derivedPtr, 
        fieldName(),
        Wt::Dbo::OnDeleteCascade | Wt::Dbo::OnUpdateCascade
    );
  }
};

// Extra Functions to get direct Relation Objects

template <class Base, class Derived>
Wt::Dbo::ptr<Derived> getDerived(Wt::Dbo::ptr<Base> bPtr)
{
  Wt::Dbo::Transaction t(dataSession());
  auto query = t.session().find<ConnectorTable<Base, Derived>>().where("base_id = ?").bind(bPtr);
  auto result = query.resultValue(); // either 1 or 0 results

  if(!result)
    return {};

  return result->getDerived();
}

template <class Base, class Derived>
Wt::Dbo::ptr<Base> getBase(Wt::Dbo::ptr<Derived> dPtr)
{
  Wt::Dbo::Transaction t(dataSession());

  auto query = 
    t.session().find<ConnectorTable<Base, Derived>>().where(
        ConnectorTable<Base,Derived>::fieldName() + "_id = ?"
    ).bind(dPtr);

  auto result = query.resultValue();

  if(!result)
    return {};

  return result->getBase();
}

This class can be defined for Dbo at the same time as the Derived Class and then added in the mapClass calls

//Derived.h

#include "Base.h"
#include "ConnectorTable.h"

class Derived;

typedef ConnectorTable<Base, Derived> BaseDerivedConnector

class Derived : public Base
{
  Derived() {}
  static std::string refName() { return {"Derived"}; }

  virtual Wt::Dbo::ptr<Base> getBase() const;

  template <class Action>
  void persist(Action &a);

private:
  Wt::Dbo::weak_ptr<BaseDerivedConnector> baseConPtr;
  Wt::Dbo::ptr<Base> basePtr;

protected:
  bool has_derived = false;
  void updateValues(const Derived* values);
};

DBO_EXTERN_TEMPLATES(BaseDerivedConnector)
DBO_EXTERN_TEMPLATES(Derived)

With a few tweaks in the persist function:

// Derived.cpp
template <class Action>
void Derived::persist(Action &a)
{
  if (typeid(a) == typeid(Wt::Dbo::SessionAddAction))
  {
    // Insert action
    // Add the current Object as a Base
    basePtr = dataSession().addNew<Base>(this);
  }

  // Define the fields and connections
  Wt::Dbo::hasOne(
    a, 
    baseConPtr,        
    BaseSpecialConnector::fieldName()
  );
  Wt::Dbo::belongsTo(a, basePtr);

  if (typeid(a) == typeid(Wt::Dbo::LoadDbAction<Derived>))
  {
    // Load action, after the fields Definition
    // Fill the current Base Object with Values from DB
    Base::updateValues(basePtr.get());
    // like Copy Constructor, but just updating Values
  }
}

The Only important functions within each Class that wants to enable the use of Inheritance with this ConnectTable Template are:

private:
  Wt::Dbo::ptr<BaseClass> basePtr; // might be optional, but easier to set Internal Values
protected:
  bool has_derived; // To be set from next Inheriting // enables looping through selection and looking for more
  void updateValues(const Class* values); // called from next Inheriting
public:
  static std::string refName(); //with the Name of The Class // Called for RelationNames

Also available in: Atom PDF