Neue C++ ORM Bibliothek "oos" v0.2.1



  • Mechanics schrieb:

    Hmm, das versteh ich nicht ganz. Wann musst du irgendwelche Felder ändern? Ich dachte, die Transaktionen laufen über die Datenbank. Wenn ein Datensatz in der Datenbank nicht geändert werden konnte, dann geht es eben nicht und die Transaktion schlägt fehl. An welcher Stelle musst du hier die Instanz der Klasse verändern?

    Der ObjectStore funktioniert auch In-Memory, also wenn keine Datenbank genutzt wird. Demnach muss ich Transaktionen (commit, rollback) auch im Framework abbilden. Ich habe den ObjektStore ohne eine Datenbankanbindung geplant und umgesetzt (quasi proof of concept).

    Mechanics schrieb:

    Ich glaube eben, dass der Qt Overhead nicht unbedingt nötig ist. Wie gesagt, ich hab mal was ähnliches geschrieben, ohne Qt. Ich hab aber auch boost::function verwendet, mit C++11 wäre nicht mal das nötig.

    Mir ist klar, dass ich den QT Overhead nicht brauche. Wollte damit nur zum Ausdruck bringen, dass ich QT nicht in meinem Framework nutzen möchte.

    Mechanics schrieb:

    Kennst du boost::serialize? Da kann man den Stream Operator auch einfach außerhalb der Klasse implementieren und muss die Klasse nicht verändern.

    Jepp, habe ich mir schon angeschaut. Finde das Konzept recht interessant.

    Mechanics schrieb:

    Was du mit dem Ansatz wahrscheinlich nicht hinbekommst, ist lazy loading. Das könntest du mit deinen eigenen Containern wie object_list wahrscheinlich realisieren. Ohne solche Hilfsmittel wird es in C++ wohl wirklich schwierig.
    Du könntest vielleicht zwei Varianten anbieten (klar, ist viel Aufwand und ein größerer Umbau), die jetzige, und eine, bei der man beliebige Klassen speichern kann. Dann könnte man mit deiner Bibliothek ganz normale Klassen speichern (ich glaube, ich bin nicht der einzige, der das wichtig findet), und wenn man dann Gefallen daran gefunden hat und fortgeschrittene Features nutzen möchte, könnte man für einige Klassen DTOs definieren, die dann von deiner Basisklasse erben und die speziellen Container benutzen.

    Lazy loading finde ich persönlich interessanter und wichtiger, als zwei Varianten anzubieten. Vielleicht gibt es für jeden Teilaspekt (Serialisierung, Basisklasse, Transkationen, Lazyloading) eine gut Lösung. Dann könnte ich das nach und nach umsetzen.

    Ich werde mir mal eine Roadmap überlegen und die "Future Plans" auf der Projektseite dadurch ersetzen.

    Gruß
    Sascha



  • Moin Zussel,

    ich sitze derzeit an einer ähnlichen Geschichte zur (De-)Serialisierung von Objekten.

    Das Laden und Speichern von einzelnen Objekten in Datenbanken läuft, es gibt also durchaus auch eine nutzbare Implementierung.

    zussel schrieb:

    virtual void deserialize(oos::object_reader &rdr)
    {
    oos::object::deserialize(rdr);
    rdr.read("name", name_);
    rdr.read("age", age_);
    rdr.read("mate", mate_);
    }

    virtual void serialize(oos::object_writer &wrt) const
    {
    oos::object::serialize(wrt);
    wrt.write("name", name_);
    wrt.write("age", age_);
    wrt.write("mate", mate_);
    }

    Ich habe Deine OOS-Lib noch nicht ausprobiert, mir fiel nur der oben stehende Code ins Auge.

    Ich habe die (De-)Serialisierung aus dem Objekt rausgenommen. Das Objekt muss, genau wie bei Dir, von einer Basisklasse abgeleitet sein. Anschließend gebe ich die Memberpointer der Member, die ich serialisieren möchte, in einem Konfigurationsobjekt bekannt. Die Funktionalität zum (De-)Serialisieren ist damit konfigurierbar, ich kann also einen Objekttyp unterschiedlich mappen und ich muss keine (De-)Serialisisungs-Methoden einfügen. Unterschiedliche Konfigurationen können also Teile des Objektes auch in unterschiedlichen Tabellen speichern.
    Eigentlich muss ich neben der Ableitung gar nichts einfügen, weil ich die Konfiguration auch außerhalb der Klasse mache. Es sind also keine zusätzlichen Methoden im zu mappenden Objekt erforderlich.

    zussel schrieb:

    So, ich hoffe, ich konnte den einen oder anderen Neugierig machen. Ich würde mich über konstruktive Kritik, Anregungen und natürlich auch Anerkennung 🙂 freuen.

    Ich habe es mir noch nicht groß angeguckt, aber ein Rollback bekomme ich derzeit noch nicht hin. ^^
    Aber das ist derzeit auch nicht mein primäres Problem.

    Sieht aber auf den ersten Blick nicht verkehrt aus. Also schonmal ein anerkennendes Nicken. 😉



  • Moin Xin

    Xin schrieb:

    ich sitze derzeit an einer ähnlichen Geschichte zur (De-)Serialisierung von Objekten.

    Das Laden und Speichern von einzelnen Objekten in Datenbanken läuft, es gibt also durchaus auch eine nutzbare Implementierung.

    Irgendwie programmiert man doch sehr oft immer das gleiche - und man freut sich wie ein Schnitzel, wenn der Code dann (auch wieder) funktioniert 😉

    Back to topic: Das Laden und Speichern, insbesondere von komplexen, verschachteteln Objekten (und wenn sie dann auch noch Listen/Vektoren enhalten), finde ich schon ziemlich komplex.

    Lädst Du die Tabellen nacheinander, oder die Objekte? Falls Du die Tabellen lädst, wie hast Du das Henne ein Problem gelöst, wenn ein Objekt einen anderes enthält dessen Tabelle Du noch nicht geladen hast. Bei mir war das ein guten Stück arbeit.

    Xin schrieb:

    Ich habe Deine OOS-Lib noch nicht ausprobiert, mir fiel nur der oben stehende Code ins Auge.

    Ich habe die (De-)Serialisierung aus dem Objekt rausgenommen. Das Objekt muss, genau wie bei Dir, von einer Basisklasse abgeleitet sein. Anschließend gebe ich die Memberpointer der Member, die ich serialisieren möchte, in einem Konfigurationsobjekt bekannt. Die Funktionalität zum (De-)Serialisieren ist damit konfigurierbar, ich kann also einen Objekttyp unterschiedlich mappen und ich muss keine (De-)Serialisisungs-Methoden einfügen. Unterschiedliche Konfigurationen können also Teile des Objektes auch in unterschiedlichen Tabellen speichern.
    Eigentlich muss ich neben der Ableitung gar nichts einfügen, weil ich die Konfiguration auch außerhalb der Klasse mache. Es sind also keine zusätzlichen Methoden im zu mappenden Objekt erforderlich.

    Ja ich wurde ja auch von einem anderen Poster darauf hingewiesen und Dein Einwand, dass das Serialisieren dadurch konfigurierbar wird (man kann sich sogar verschiedenen Konfigurationen für ein und denselben Objekttyp vostellen) ist nicht von der Hand zu weisen.

    Ich denke ich werde das in meiner Roadmap entsprechend einplanen!

    zussel schrieb:

    So, ich hoffe, ich konnte den einen oder anderen Neugierig machen. Ich würde mich über konstruktive Kritik, Anregungen und natürlich auch Anerkennung 🙂 freuen.

    Xin schrieb:

    Ich habe es mir noch nicht groß angeguckt, aber ein Rollback bekomme ich derzeit noch nicht hin. ^^
    Aber das ist derzeit auch nicht mein primäres Problem.

    Commit und Rollback habe ich anhand von Observer- und Visitor-Pattern gelöst. Wenn Du mehr Details haben möchtest, sag Bescheid.

    Xin schrieb:

    Sieht aber auf den ersten Blick nicht verkehrt aus. Also schonmal ein anerkennendes Nicken. 😉

    Vielen Dank und Dir auch weiterhin viel Erfolg bei Deiner Bibliothek (möchtest Du die dann eigentlich auch veröffentlichen)!

    Sascha



  • zussel schrieb:

    Back to topic: Das Laden und Speichern, insbesondere von komplexen, verschachteteln Objekten (und wenn sie dann auch noch Listen/Vektoren enhalten), finde ich schon ziemlich komplex.

    Ich habe zwei Implementationen, die erste lädt Listen und Vektoren, allerdings sind das nicht std::vector und std::list, sondern selbstgeschriebene. Ich arbeite meistens mit selbstgeschriebenen Containern. Die erste wurde allerdings durch den Konstruktor des Objektes konfiguriert, so dass jedes Objekt eine eigene Konfiguration besaß. Das ließ zwar sehr viele Möglichkeiten zur Individuellen Bearbeitung (die keiner braucht), kostet aber Speicherplatz.

    Die zweite Implementation ersetzt schließlich die erste, sobald sie alle Fähigkeiten der ersten übernehmen kann.

    zussel schrieb:

    Lädst Du die Tabellen nacheinander, oder die Objekte?

    Ich ziehe die Objekte aus der Tabelle. Die kompletten Tabellen lade ich nicht, sonst bräuchte ich keine Datenbank. 😉

    zussel schrieb:

    zussel schrieb:

    So, ich hoffe, ich konnte den einen oder anderen Neugierig machen. Ich würde mich über konstruktive Kritik, Anregungen und natürlich auch Anerkennung 🙂 freuen.

    Xin schrieb:

    Ich habe es mir noch nicht groß angeguckt, aber ein Rollback bekomme ich derzeit noch nicht hin. ^^
    Aber das ist derzeit auch nicht mein primäres Problem.

    Commit und Rollback habe ich anhand von Observer- und Visitor-Pattern gelöst. Wenn Du mehr Details haben möchtest, sag Bescheid.

    Ich bin noch nicht sicher, ob ich das zurzeit brauche. YAGNI?

    Im Prinzip will ich vergleichsweise einfache Objekte laden und speichern, also wirklich nur Transfer aus der Datenbank und wieder rein. Mir geht es mehr darum, dass ich nicht umständlich die C++-Datenstrukturen auf die Datenbank abbilden muss, sondern einfach meine Datenstrukturen schreibe, konfiguriere und die Datenbank und die ganze sich damit automatisch ergibt.

    zussel schrieb:

    Xin schrieb:

    Sieht aber auf den ersten Blick nicht verkehrt aus. Also schonmal ein anerkennendes Nicken. 😉

    Vielen Dank und Dir auch weiterhin viel Erfolg bei Deiner Bibliothek (möchtest Du die dann eigentlich auch veröffentlichen)!

    Meine "Bibliothek" besteht inzwischen aus einer Reihe von Libs. Die Datenbankgeschichte ist eher eine Randnotiz der IO-Lib.
    Über eine Veröffentlichung denke ich nach, aber dafür müsste ich da noch einiges erst wieder rauspulen und ändern, da ich wie gesagt, nicht unbedingt mit Standard-Containern unterwegs bin. Das ganze ist also abhängig von anderen Libs von mir.

    zussel schrieb:

    Sascha

    Auch so 😉



  • zussel schrieb:

    Mein nächster Ansatz würde in die Richtung RAII gehen. Dann würde das ungefähr so aussehen:

    {
      oos::transaction tr(db);
      // beim anlegen der Transaktion wird automatisch ein begin() aufgerfufen
      ostore.insert(new person("George")); 
      ostore.insert(new person("Jane"));
      ostore.insert(new person("Tim")); 
      ostore.insert(new person("Walter"));
      // wird der Scope verlassen wird beim zerstören der Transaktion
      // automatisch commit() und je nachdem ein rollback() aufgerufen.
    }
    

    Das klappt so nicht. Siehe http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3614.pdf.

    Wie wäre es mit:

    #define OOS_TRANSACTION \
            for(bool _once = true; _once; _once = false) \
                    for(oos::transaction _transaction; _once; _once = false, _transaction.commit())
    

    oos::transaction muss dann im Dtor ein rollback machen, sofern nicht commitet wurde.

    OOS_TRANSACTION
    {
            ostore.insert(new person("George")); 
    
            if(rand() % 42 == 0)
                    break; // nope, transaktion abbrechen
    }
    


  • ***** Es gibt 'ne neue Version, siehe ersten Post *****



  • Ist leider unbenutzbar, weil nicht exception safe. Da sind überall Leaks und wer weiß was noch alles.



  • TyRoXx schrieb:

    Ist leider unbenutzbar, weil nicht exception safe. Da sind überall Leaks und wer weiß was noch alles.

    Hallo TyRoXx,

    danke für die Rückmeldung. Die Aussagen sind mir aber etwas Pauschal.

    Warum ist Sie unbenutzbar? Eine Bibliothek muss nicht unbedingt exception safe sein (ist aber ein noch nicht erreichtes Ziel). Welche Stellen sind Dir denn aufgefallen?

    Wo sind die Memory Leaks? Alle Unit Test mit valgrind sagen mir das Gegenteil. Was hast Du ausprobiert und wo trat das Problem auf?

    Was willst Du mit "was weiß ich noch alles" sagen?

    Mein Ziel ist eine saubere, leicht zu benutzende Bibliothek, daher ist es für mich hilfreicher, wenn auf konkrete Probleme gezeigt wird. Daran kann ich arbeiten.

    Könntest Du Deine Kritik deshalb etwas konkretisieren?



  • zussel schrieb:

    Eine Bibliothek muss nicht unbedingt exception safe sein (ist aber ein noch nicht erreichtes Ziel).

    Muss sie nicht. Aber wenn sie es nicht ist, wird kein vernünftiger Programmierer sie verwenden.

    zussel schrieb:

    Wo sind die Memory Leaks? Alle Unit Test mit valgrind sagen mir das Gegenteil. Was hast Du ausprobiert und wo trat das Problem auf?

    So gut wie jedes new in deinem Code wird zu einem Leck sobald eine Ausnahme fliegt und das manuelle delete verhindert. Du wirfst selbst Ausnahmen, also geht es nicht nur um hypothetische bad_alloc s.

    zussel schrieb:

    Was willst Du mit "was weiß ich noch alles" sagen?

    Wenn du es nicht hinbekommst alle Ressourcen wieder freizugeben, sind da vielleicht noch schlimmere Fehler. Vielleicht kann eine Ausnahme an einer bestimmten Stelle eins deiner Objekte in einem ungültigen Zustand zurücklassen. So ein Zustand könnte von einem bösartigen Angreifer ausgenutzt werden.

    zussel schrieb:

    Mein Ziel ist eine saubere, leicht zu benutzende Bibliothek, daher ist es für mich hilfreicher, wenn auf konkrete Probleme gezeigt wird. Daran kann ich arbeiten.

    Könntest Du Deine Kritik deshalb etwas konkretisieren?

    Hier ist diverse Kritik, die mir spontan einfällt:

    class OOS_API transaction : public object_observer
    {
    [...]
    private:
      static long id_counter;
    };
    
    [...]
    id_ = ++transaction::id_counter;
    

    Diese globale Variable könnte in mehreren Threads verwendet werden.
    Deine Transaktionen sind also nicht Thread-safe.

    void sql::append(const condition &c)
    {
      field_ptr f(new field(c.column().c_str(), c.type(), host_field_vector_.size(), true));
    
      /*std::list<token*>*/ token_list_.push_back(new condition_token(c)); //leak
      host_field_map_.insert(std::make_pair(c.column(), f));
      host_field_vector_.push_back(f);
    }
    

    (Die Kommentare sind von mir.)
    Wenn das erste push_back fehlschlägt, geht das übergebene Objekt verloren.
    Außerdem sehe ich in der Datei keinen Grund für std::list , den ineffizientesten Container in C++.

    table::~table()
    {
      if (insert_) {
        delete insert_;
        delete update_;
        delete delete_;
        delete select_;
      }
    }
    

    Ich frage mich, ob das if da irgendeinen Zweck erfüllt oder ob es wirklich redundant ist. Es könnte ja sein, dass update_ etc nicht gelöscht werden sollen, falls insert_==nullptr . Ganz abgesehen, dass delete immer eine schlechte Idee ist.

    if (mysql_real_connect(&mysql_, host.c_str(), user.c_str(), (has_pwd ? passwd.c_str() : 0), db.c_str(), 0, NULL, 0) == NULL) {
        printf("Connection Error %u: %s\n", mysql_errno(&mysql_), mysql_error(&mysql_));
        exit(1);
      }
    

    Das ist keine sinnvolle Fehlerbehandlung.

    MYSQL* mysql_database::operator()()
    {
      return &mysql_;
    }
    

    Aus verschiedenen Gründen ist das eine schlechte Idee. Bitte selber recherchieren.

    info_[index].buffer = new char[sizeof(long)];
    memset(info_[index].buffer, 0, sizeof(long));
    // das gleiche wie:
    info_[index].buffer = new char[sizeof(long)]();
    

    Wenn schon new[] , dann wenigstens richtig.

    session::~session()
    {
      if (impl_) {
        if (type_ == "memory") {
          delete impl_;
        } else {
          database_factory::instance().destroy(type_, impl_);
        }
      }
    }
    

    Das sieht unnötig kompliziert aus.

    void session::push_transaction(transaction *tr)
    {
      if (!transaction_stack_.empty()) {
    //    cout << "unregister transaction observer [" << transaction_stack_.top() << "]\n";
        ostore_.unregister_observer(transaction_stack_.top());
      }
      transaction_stack_.push(tr);
    //  cout << "register transaction observer [" << tr << "]\n";
      ostore_.register_observer(tr);
    }
    

    Man sieht sehr oft dieses cout -Debugging. Das erhöht nicht gerade mein Vertrauen in die Korrektheit des Programms. Wenn du etwas mal nicht sinnvoll mit einem Test abdecken kannst, nimm einen Debugger.

    /**
       * Returns the classname of the object
       * 
       * @return Name of the object type
       */
    	const char* classname() const;
    [...]
    const char* object::classname() const
    {
      if (proxy_ && proxy_->node) {
        return proxy_->node->type.c_str();
    //    return proxy_->node->producer->classname();
      } else {
        return 0;
      }
    }
    

    Die Dokumentation ist nicht nur wortkarg und wenig nützlich. Sie lügt mich sogar an. nullptr ist für mich kein "Name". Man hätte hier deutlich hinschreiben müssen, dass nullptr zurückkommen kann. Oder man hätte einfach const std::string * zurückgeben können.

    class OOS_API object : public object_atomizable
    {
    public:
    [...]
      /**
       * Modify the char attribute assigning
       * the new given char value to attributes
       * reference.
       *
       * @param attr Pointer to character string to change.
       * @param max_size The maximum capacity of the destinition array.
       * @param val New stringto set.
       * @param size The length of the new string.
       * @throw std::logic_error.
       */
      void modify(char *attr, int max_size, const char *val, int size)
      {
        if (max_size < size) {
          throw std::logic_error("not enough character size");
        }
        mark_modified();
    #ifdef WIN32
        strcpy_s(attr, max_size, val);
    #else
        strcpy(attr, val);
    #endif
      }
    

    Ich habe nicht den blassesten Schimmer wozu das gut sein soll.
    Das einzige was die Methode mit dem object zu tun hat, ist der Aufruf von mark_modified . Was soll der Rest da drin? Warum strcpy ? Warum int statt size_t ?
    EDIT: Ah, die Methode wird im Beispiel verwendet. Ist als Syntactic Sugar nicht so schlecht.

    #ifdef WIN32
      #ifdef oos_EXPORTS
        #define OOS_API __declspec(dllexport)
        #define EXPIMP_TEMPLATE
      #else
        #define OOS_API __declspec(dllimport)
        #define EXPIMP_TEMPLATE extern
      #endif
      #pragma warning(disable: 4251)
    #else
      #define OOS_API
    #endif
    

    Das findet sich in vielen Headern wieder. Hätte man das nicht in einem zentralen Header unterbringen können?

    template < typename T >
      bool assign(const T &val)
      {
        delete [] data_;
        data_ = new char[sizeof(T)];
        memcpy(data_, &val, sizeof(T));
        size_ = sizeof(T);
        return false;
      }
    

    Das ist ein Beispiel für so eine typische Sicherheitslücke, von denen man immer liest. Wenn das new fehlschlägt, zeigt data_ auf bereits freigegebenen Speicher. Wenn du Pech hast, kann ein guter Hacker das beliebig ausnutzen. In diesem Fall dürfte es wegen der festen und vermutlich geringen Größe der Allokation schwierig sein das bad_alloc herbeizuführen, aber es könnte problematischere Vorkommen dieses Fehlers in deinem Programm geben.
    Unschön ist außerdem, dass da drei mal sizeof(T) steht statt ein mal sizeof(val) . Solche Redundanz ist völlig unnötig.
    Warum baust du überhaupt std::vector nach?

    Es gibt mehrere leere Dateien, zum Beispiel src/object/attribute_serializer.cpp . Es sind unbeabsichtigte Dateien eingecheckt, zum Beispiel src/tools/byte_buffer.o oder diverse Makefile s.



  • Halllo TyRoXx,

    das nenn' ich doch mal konkret! Offensichtlich habe ich mich mehr auf die Funktionalität konzentriert als auf die Sicherheit. Daher werde ich im nächsten Release hauptsächlich alle Unzulänglichkeiten und Sicherheitslücken entfernen.

    Du bist wahrscheinlich der erste, der sich meinen Code mal genauer angesehen hat. Dafür ein dickes Dankeschön 👍 ! Und ich hoffe, dass Du im nächsten Release so gut wie nichts mehr finden wirst.

    Beste Grüße
    zussel



  • Und bitte nicht einfach durch den Code gehen und versuchen die ganzen Leaks mit 30 try-catch Blocks zu entfernen, sondern RAII nutzen. Überall da, wo in deinem Code eine Ressource außerhalb eines Konstruktors angefordert wird, oder außerhalb eines Destruktors freigegeben wird, hast du ein Problem. Du hast in vielen Fällen sogar schon ein Problem wenn du mehrere solcher Ressourcen auf einmal anforderst. Am besten du hälst dich an diese Regel: Ein Objekt (mit Konstruktor und Destruktor) pro Ressource. Dann kann wenig schief gehen. Und mache dir gedanken über Datentypen. int size? Kann size wirklich negativ sein? Dann bitte unsigned. Ist unsigned wirklich immer groß genug? (64-bit System?) Vielleicht doch lieber std::size_t. printf will niemand sehen in einer Bibliothek, genau so wenig wie exit. Beides ist tabu, nur Rückgabewerte und Exceptions sind erlaubt. (Oder Parameter.)



  • Hallo cooky451

    Auch Dir danke für die Hinweise! Diese werde ich auch beherzigen. Ich hoffe, ich kann meine Bibliothek damit schnell stabil machen. Einen guten Funktionsumfang hat sie in meinen Augen schon.

    Gruß
    zussel



  • ***** Es gibt 'n Bugfix Release, siehe ersten Post *****


Anmelden zum Antworten