Object Oriented Database Design & Implementation -Part II
OODB and Java
4
White

Joe viết ngày 04/08/2018

Chao cac ban,

Design & Implementation of Data Integrity and Data Consistency

In Part I we design and implement our own file system with the following entry-format (or record format):

0            2              6                k           n
+-----------+--------------+----------------+-------------+
! KeyLength ! ObjectLength ! Key/ObjectName ! Object      !
+-----------+--------------+----------------+-------------+
2 bytes KeyLength: max. 64K
4 bytes ObjectLength: max 2GB
k bytes Key/ObjectName: max. 64 KB
n bytes Object: max. 2GB
  • Key or ObjectName-Length (2 bytes) indicates the size of object name. Max. 64KB or a string of 64KB
  • ObjectLength (4 bytes) is the size of the object itself in bytes. Each object can have max. 2 GB (BigData)
  • Key/ObjectName is the readable name of the following object. It must be unique.
  • Object with the size of max. 2G Bytes. An object could be a serialized JAVA object, or a byte-array which could be object of other OOPL, a content of an image/sound file, or an URL link to another object, etc.

Note: if object byte array is the content of a file (non-serialized, e.g. image) it's better to use the file name as key/objectName (e.g. JoePicture.jpg). However, image/sound files are usually big it's better to keep them where they are and use their names as referenced objects (e.g. absolute path or URL if they are on the WEB).

Then we apply the GZIP compress technology to our dbFile in order to reduce their physical size (up to 60% -see screenshot of Part I).

Depending on the "UserRight" open() automatically creates an empty DB file if there isn't a dbFile under the given dbName. The UserRight could be: ReadOnly, ReadWrite and ReadWriteDelete (I'll come back to this issue later). ReadOnly users can only open existed dbFile. If the dbFile under the given dbName exists its content will be loaded into an ODMS cache (HashMap). Nevertheless, the caching mechanism (or algorithm) should be modified or devised if your dbFile grows too big (e.g. hundreds of objects with the size of some GB) or your DB server's scarce of RAM capacity.

Data Integrity and Data Consistency depend on the way how we access the data, how we make sure that the data are consistent, persistent (i.e. integrity) within a multiuser environment. Before we go into details let's complete the accessing mechanism of our DB file. We set the next convention for the naming storage: for the ease of use we decide that our DB files should be grouped in a common directory -says: [ current Directory ] /odb/ [ dbName ] or [ current Directory ] \obb\ [ dbName ] on WINDOWS, where dbName should be without suffix. Example: dbFile is "employee".

   current/working directory is: C:\links\odb
   the common directory will be: C:\links\odb\odb
   our dbFile for "employee" is: C:\links\odb\odb\employee

The complete ODMS.java. This implemenation is an example showing you how the basics of an Object Data Management System is implemented. You can enhance or modify it to suit your requirements. However, you have to mention the copyright (C) of the author Joe Nartca and Kipalog.com as the source. The author and Kipalog.com are NOT responsible for any loss or damage of your data. You're on your own.

package odb;

import java.io.*;
import java.util.*;
import java.util.zip.*;
/**
Object Data Management System. Auto-Create
Joe Nartca (c)
*/
public class ODMS {
  /**
  Constructor
  @param dbName String the object file name
  */
  public ODMS(String dbName) {
    cnt = 0;
    closed = true;
    changed = false;
    keys = new ArrayList<String>();
    nKeys = new ArrayList<String>();
    oldKeys = new ArrayList<String>();
    cache = new HashMap<String, byte[]>();
    oCache = new HashMap<String, byte[]>();    
    this.dbName = dbName;
  }
  /**
  isExisted
  @param key String the querry key
  @return boolean true if querried key exists
  */
  public boolean isExisted(String key) {
    return keys.contains(key);
  }
  /**
  readObject
  @param key String key of object to be read
  @return byte array
  @exception Exception thrown by JAVA
  */
  public synchronized byte[] readObject(String key) throws Exception {
    if (closed) throw new Exception("Can't read in closed state.");
    byte[] obj = cache.get(key);
    if (obj != null) return obj;
    throw new Exception("Unknown key:"+key);
  }
  /**
  deleteObject
  @param key String key of object to be deleted
  @exception Exception thrown by JAVA
  */
  public synchronized void deleteObject(String key) throws Exception {
    if (closed) throw new Exception("Can't delete in closed state.");
    oCache.put(key, cache.remove(key));
    nKeys.remove(key);
    oldKeys.add(key);
    keys.remove(key);
    changed = true;
    ext = false;
    ++cnt;
  }
  /**
  modifyObject
  @param key String the key
  @param Obj modified byte array
  @exception Exception thrown by JAVA
  */
  public synchronized void modifyObject(String key,byte[] Obj) throws Exception {
    if (closed) throw new Exception("Can't write in closed state.");
    if (cache.get(key) == null) throw new Exception("Unknown key:"+key);
    oCache.put(key, cache.get(key));
    cache.put(key, Obj);
    changed = true;
    ext = false;
    ++cnt;
  }
  /**
  addObject
  @param key String the key
  @param Obj Object to be written
  @exception Exception thrown by JAVA
  */
  public synchronized void addObject(String key, byte[] Obj) throws Exception {
    if (closed) throw new Exception("Can't write in closed state.");
    if (keys.contains(key)) throw new Exception(key+" exists. No write.");
    if (!changed) ext = true;
    cache.put(key, Obj);
    nKeys.add(key);
    keys.add(key);
    ++cnt;
  }
  /** 
  rollback() rollbacks the LAST modified/added action
  @param key String, key of object to be rollbacked
  @return boolean true if rollback is successful, false: unknown key, no rollback
  */
  public synchronized boolean rollback(String key) {
    if (cnt > 0) {
      if (nKeys.contains(key)) {
        nKeys.remove(key);
        cache.remove(key);
        keys.remove(key);
      } else if (oldKeys.contains(key)) {
        cache.put(key, oCache.remove(key));
        oldKeys.remove(key);
        keys.add(key);
      } else { // modified/replaced
        cache.put(key, oCache.remove(key));
      }
      --cnt;
      if (cnt <= 0) {
        cnt = 0;
        ext = false;
        changed = false;
      }
      return true;
    }
    return false;
  }
  // format: KeyLength ObjectLength Key      Object
  //         2 bytes   4 bytes      n bytes  n bytes
  /** 
  save() saves the current cache permenently (or commits all pending transactions)
  @exception Exception thrown by JAVA
  */
  public synchronized void save( ) throws Exception {
    if (cnt > 0) {
      try (GZIPOutputStream go = new GZIPOutputStream(new FileOutputStream(dbName, ext))) {
        ArrayList<String> Lst = ext? nKeys : keys; // extended or rewrite ?
        ByteArrayOutputStream bo = new ByteArrayOutputStream(Lst.size());
        byte[] le = new byte[6]; byte[] obj;
        for (String k : Lst) { // key Length
          le[0] = (byte)((k.length() >> 8) & 0xFF);
          le[1] = (byte) (k.length() & 0xFF);
          obj = cache.get(k); // Object Length
          le[2] = (byte)((obj.length >> 24) & 0xFF);
          le[3] = (byte)((obj.length >> 16) & 0xFF);
          le[4] = (byte)((obj.length >> 8)  & 0xFF);
          le[5] = (byte) (obj.length & 0xFF);
          bo.write(le); // save lengthes
          bo.write(k.getBytes()); // key
          bo.write(obj); // and object
        }
        go.write(bo.toByteArray());
        go.flush( );
        go.close( );
      } catch (Exception e) {
        throw new Exception("Unable to close "+dbName);
      }
    }
    cnt = 0;
    ext = false;
    changed = false;
  }
  /**
  getKeys() returns an ArrayList of all ODB keys
  @return ArrayList of strings (as keys)
  */
  public ArrayList<String> getKeys() {
    return keys;
  }
  /**
  ODMS close() must be invoked before application terminates. Otherwise data could be lost
  @exception Exception thrown by JAVA
  */
  public synchronized void close() throws Exception {
    try {
      save();
      keys.clear();
      nKeys.clear();
      cache.clear();
      oCache.clear();
      oldKeys.clear();
    } catch (Exception ex) {
      throw new Exception("Can't close "+dbName);
    }
    closed = true;
  }
  /**
  ODMS open() must be invoked before any IO action can be invoked
  @exception Exception thrown by JAVA
  */
  // format: KeyLength ObjectLength Key      Object
  //         2 bytes   4 bytes      n bytes  n bytes
  public synchronized void open( ) throws Exception {
    if (!(new File(dbName)).exists()) {
      try (FileOutputStream fo = new FileOutputStream(dbName, false)) {
        fo.close();
      } catch (Exception ex) {
        throw new Exception("Unable to create OODB:"+dbName);
      }
    } else try (FileInputStream fi = new FileInputStream(dbName)) {
      if (fi.available() > 0) { // autoCreate: empty odb
        GZIPInputStream gi = new GZIPInputStream(fi);
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        byte[] rec = new byte[MAX_BUF]; // 64K
        // cache all keys
        int p;
        for (p = gi.read(rec); p != -1; p = gi.read(rec)) bo.write(rec, 0, p);
        rec = bo.toByteArray();
        gi.close();
        bo.close();
        //
        p = 0;
        while (p < rec.length) {
          // compute key length
          int kL = (int)((rec[p++]&0xFF) << 8)+
                   (int)((rec[p++]&0xFF));
          // and object length
          int oL = (int)((rec[p++]&0xFF) << 24)+
                   (int)((rec[p++]&0xFF) << 16)+
                   (int)((rec[p++]&0xFF) << 8)+
                   (int)((rec[p++]&0xFF));
          // retrieve object
          byte[] obj = new byte[oL];
          String k = new String(rec, p, kL);
          //
          keys.add(k);
          System.arraycopy(rec, p+kL, obj, 0, oL);        
          cache.put(k, obj);
          p += (kL+oL);
        }
      }
    } catch (Exception ex) {
      throw new Exception("Cannot open:"+dbName);
    }
    closed = false;
  }
  // Data area
  private String dbName;
  private int MAX_BUF = 65536, cnt;
  private boolean closed, changed, ext = false;
  private HashMap<String, byte[]> cache, oCache;
  private ArrayList<String> keys, oldKeys, nKeys;
}

Probably you wonder where are the LOCK and UNLOCK mechanism, am I right ? YES, ODMS is designed and implemented on individual DB file. And LOCK and UNLOCK mechanism belong to a multiuser environment. Meaning that LOCK-UNLOCK have to be handled differently and independent from each particular ODMS. To do that, we have to design and implement a "Manager" that controls and coordinates the opened ODMSes and their associated users. Fig. 3 shows you how an ODManager is designed and implemented

Fig. 3
ALT

The task of an ODManager is to manage and to coordinate the ODMS activities between DB accesses of different DB files and different users. Further, it has to care about the integrity and consistency of all in-used DB files and to synchronize all accessing activities of users by providing the following basic functions:

  • CONNECTION and DISCONNECTION of DB users. If a new user successfully signs in (CONNECT) ODManager firstly checks for the specified dbName whether its associated ODMS is already opened and in used, or a new ODMS for the specified dbName has to be instantiated and opened. In both cases the new user is then assigned to this specific ODMS (as sharer or initiator). The reverse is a disconnection of user from the specified ODMS, and the counter of shared users (of each ODMS) is decremented. If the ODMS counter ends up in zero this ODMS (or dbFile) is closed and released.
  • LOCK and UNLOCK mechanism (integrity & consistency & persistency). Any modification (replace/delete) of an object is preconditioned that the key of this object has been previously locked (integrity). A locked object is inaccessible for other users as long as its associated key stays locked (consistency). An UNLOCK action releases the locked object and issues a commit for this object (persisteny). A rollback after UNLOCK is no more possible.
  • LOGGING facility (tracing of problems and abuses). This facility enables us to trace back, for example, the events happened with the dbFiles (e.g. who did what and what was changed.) Currently only OPEN and CLOSE are logged by ODManager. However, READ, WRITE, DELETE and REPLACE could be easily implemented as well.
  • PANIC SHUTDOWN is for the rescuing purpose of uncommited data in case of power down or whatever. The panic shutdown() starts to close all opened ODMSes and DB server (see example as below).

The hooked-up shutdown could be implemented as following

  /*
  hook the Shutdown watching dog.
  */
  private void hookShutdown(){
    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
        try {
          running = false;
          odMgr.shutdown(); // <<<-- shutdown ODManager
          dbSvr.close();    // <<<-- close DB server
          pool.shutdown();  // <<<-- shutdown the ThreadPool
          System.out.println("OODB server is shutdowned.");
        } catch (Exception ex) { }
      }
    });
  }

Note: to exploit the multi-core Parallelism JAVA provides two kinds of working pools: ForkJoinPool and StealingPool. Because our implementation bases on threads so we work with the ExecutorService StealingPool to run the threads.

An ODManager implementation ODManager.java. This implemenation is an example showing you how the basics of an Object Data Manager are implemented. ODManager controls and synchronizes the data traffic between DB users and dbFiles. You can enhance or modified it to suit your requirements.

Note: However, you have to mention the copyright (C) of the author Joe Nartca and Kipalog.com as the source. The author and Kipalog.com are NOT responsible for any loss or damage of your data. You're on your own.

package odb;

import java.io.*;
import java.util.*;
import java.util.zip.*;
import java.util.concurrent.*;
//
import java.nio.*;
import java.nio.channels.*;
/**
Object Data Manager
@author Joe Nartca (c)
*/
public class ODManager {
  /**
  contructor
  @param logger OutputStream, logging stream
  */
  public ODManager(OutputStream logger) {
   /**
  contructor
  @param config String, ODB config fileName
  @param logger OutputStream, logging stream
  */
  public ODManager(String config, OutputStream logger) throws Exception {
    Properties prop = new Properties();
    prop.load(new FileInputStream(config));
    dbPath = prop.getProperty("ODB_PATH");
    kOwner = new HashMap<String, HashMap<String, String>>();
    dbOwner = new HashMap<String, ArrayList<String>>();
    users = new HashMap<String, ArrayList<String>>();
    dbActive = new HashMap<String, Integer>();
    odmsLst = new HashMap<String, ODMS>();
    dbLst = new ArrayList<String>();
    this.logger = logger;
  }
  /**
  connect() connect to the specified dbName  (abs.path, without any suffix). If suffix is given
  <br>it will be cut off
  @param uID    String, userID
  @param dbName String, the DB name
  @exception Exception thrown by JAVA
  */
  public void connect(String uID, String dbName) throws Exception {
    int i = dbName.lastIndexOf("."); // cut off the suffix
    dbName = i > 0? dbName.substring(0, i) : dbName;
    if (!odmsLst.containsKey(dbName)) { // new or shared ?
      logging(dbName+" is opened.");
      ODMS odms = new ODMS(dbPath+File.separator+dbName);
      odmsLst.put(dbName, odms);
      dbActive.put(dbName, 1);
      dbLst.add(dbName);
      odms.open();
    } else {
      int cnt = dbActive.get(dbName) + 1;
      logging(dbName+" is shared by "+cnt+" users.");
      dbActive.put(dbName, cnt);
    }
    ArrayList<String> lst = dbOwner.get(uID);
    if (lst == null) lst = new ArrayList<String>();
    lst.add(dbName); // load dbName
    dbOwner.put(uID, lst);

  }
  /**
  getKeys() return all keys of the specified dbName
  @param uID    String, userID
  @param dbName String, dbName to be unlocked
  @return ArrayList of String, null if uID is not owner of or shared with dbName
  */
  public synchronized ArrayList<String> getKeys(String uID, String dbName) {
    if (isOwner(uID, dbName)) return odmsLst.get(dbName).getKeys();
    return null;
  }
  /**
  add() object at the given key to dbName
  @param uID    String, userID
  @param dbName String, dbName
  @param key String, the key name
  @param obj byte array of Serialized object to be added
  @exception Exception thrown by JAVA
  */
  public synchronized void add(String uID, String dbName, String key, byte[] obj) throws Exception {
    if (!isOwner(uID, dbName)) throw new Exception(uID+" or "+dbName+" is unknown.");
    odmsLst.get(dbName).addObject(key, obj);
    putKey(uID, key);
  }
  /**
  replace() object with obj at the given key to dbName
  @param uID    String, userID
  @param dbName String, dbName
  @param key String, the key name
  @param obj byte array of Serialized object to be replaced
  @exception Exception thrown by JAVA
  */
  public synchronized void replace(String uID, String dbName, String key, byte[] obj) throws Exception {
    if (!isOwner(uID, dbName)) throw new Exception(uID+" or "+dbName+" is unknown.");
    odmsLst.get(dbName).modifyObject(key, obj);
    putKey(uID, key);
  }
  /**
  delete() object at the given key from dbName
  @param uID    String, userID
  @param dbName String, dbName
  @param key String, the key name
  @exception Exception thrown by JAVA
  */
  public synchronized void delete(String uID, String dbName, String key) throws Exception {
    if (!isOwner(uID, dbName)) throw new Exception(uID+" or "+dbName+" is unknown.");
    odmsLst.get(dbName).deleteObject(key);
    putKey(uID, key);
  }
  /**
  read() object at the given key from dbName
  @param uID    String, userID
  @param dbName String, dbName
  @param key String, the key name
  @return byte array of serialized object
  @exception Exception thrown by JAVA
  */
  public byte[] read(String uID, String dbName, String key) throws Exception {
    if (isOwner(uID, dbName)) return odmsLst.get(dbName).readObject(key);
    throw new Exception(uID+" or "+dbName+" is unknown.");
  }
  /**
  rollback() obj of key of dbName of the LAST modified/added action
  @param uID    String, userID
  @param dbName String
  @param key String
  @return boolean true if rollback is successful, false: nothing to rollback
  @exception Exception thrown by JAVA
  */
  public synchronized boolean rollback(String uID, String dbName, String key) throws Exception {
    if (!isOwner(uID, dbName)) throw new Exception(uID+" or "+dbName+" is unknown.");
    ArrayList<String> kLst = users.get(uID);
    if (kLst != null && kLst.contains(key)) {
      kLst.remove(key);
      users.put(uID, kLst);
      return odmsLst.get(dbName).rollback(key);
    }
    return false;
  }
  /**
  rollback() ALL objects of dbName of the LAST modified/added actions
  @param uID    String, userID
  @param dbName String
  @return boolean true if rollback is successful, false: nothing to rollback
  @exception Exception thrown by JAVA
  */
  public synchronized boolean rollback(String uID, String dbName) throws Exception {
    if (!isOwner(uID, dbName)) throw new Exception(uID+" or "+dbName+" is unknown.");
    ArrayList<String> kLst = users.get(uID);
    if (kLst != null) { 
      boolean rt = false;
      for (String k : kLst) if (odmsLst.get(dbName).rollback(k)) rt = true;
      kLst.clear(); // clear rollback key-list
      users.put(uID, kLst);
      return rt;
    }
    return false;
  } 
  /**
  close() close an open db
  @param uID    String, userID
  @param dbName String
  @exception Exception thrown by JAVA
  */
  public synchronized void close(String uID, String dbName) throws Exception {
    if (!isOwner(uID, dbName)) throw new Exception(uID+" or "+dbName+" is unknown.");
    cleanUp(dbName);
    users.remove(uID);
  }
  /**
  disconnect() closes all ODB of uID from the given list 
  @param uID String, userID
  @param lst ArrayList of opened ODBs
  */
  public synchronized void disconnect(String uID, ArrayList<String> lst) {
    for (String dbN : lst) try {
      cleanUp(dbN);
    } catch (Exception ex) { }
    ArrayList<String> kLst = users.get(uID);
    if (kLst != null) { // clear Rollback
      kLst.clear();
      users.put(uID, kLst);
    }
  }
  /**
  shutdown() close ALL open db
  @exception Exception thrown by JAVA
  */
  public synchronized void shutdown( ) throws Exception {
    logging("ODManager is going down...");
    for (String dbName : dbLst) odmsLst.get(dbName).close();
    dbActive.clear();
    odmsLst.clear();
    dbOwner.clear();
    kOwner.clear();
    dbLst.clear(); 
    users.clear();
  } 
  /**
  isExisted() return true if key of dbName exists
  @param uID String, UserID
  @param dbName String
  @param key String
  @return boolean
  */
  public synchronized boolean isExisted(String uID, String dbName, String key) {
    if (isOwner(uID, dbName)) return odmsLst.get(dbName).isExisted(key);
    return false;
  }
  /**
  isLocked() return true if key of dbName is locked
  @param uID String, UserID
  @param dbName String
  @param key String
  @return boolean
  */
  public synchronized boolean isLocked(String uID, String dbName, String key) {
    if (!isOwner(uID, dbName) || !odmsLst.get(dbName).isExisted(key)) return false;
    HashMap<String, String> kLst = kOwner.get(dbName);
    if (kLst != null) {
      String u = kLst.get(key);
      if (u != null && u.equals(uID)) return true;
    }
    return false;
  }
  /**
  lock() return true if key of dbName is successfully locked
  @param uID String, UserID
  @param dbName String
  @param key String
  @return boolean
  */
  public synchronized boolean lock(String uID, String dbName, String key) {
    if (!isOwner(uID, dbName) || !odmsLst.get(dbName).isExisted(key)) return false;
    HashMap<String, String> kLst = kOwner.get(dbName);
    if (kLst == null) kLst = new HashMap<String, String>();
    else { // get the owner of this key
      String user = kLst.get(key);
      if (user != null && !user.equals(uID)) return false;
    }
    kLst.put(key, uID);
    kOwner.put(dbName, kLst);
    return true;
  }
  /**
  unlock() return true if key of dbName is successfully unlosked
  @param uID String, UserID
  @param dbName String
  @param key String
  @return boolean
  */
  public synchronized boolean unlock(String uID, String dbName, String key) {
    if (!isOwner(uID, dbName) || !odmsLst.get(dbName).isExisted(key)) return false;
    HashMap<String, String> kLst = kOwner.get(dbName);
    if (kLst != null) {
      String u = kLst.get(key);
      if (u != null && u.equals(uID)) {
        kLst.remove(key, uID);
        kOwner.put(dbName, kLst);
        return true;
      }
    }
    return false;
  }
  /**
  logging() logs the message
  @param msg String message
  */
  public synchronized void logging(String msg) {
    try {
      logger.write((msg+System.lineSeparator()).getBytes());
    } catch (Exception ex) {
      System.out.println("Unable to log \""+msg+"\"");
    }
  } 
  //
  private synchronized void cleanUp(String dbN) throws Exception {
    int cnt = dbActive.get(dbN) - 1;
    if (cnt > 0) {
      logging(dbN+" is now shared by "+cnt+" user"+(cnt == 1?".":"s."));
      dbActive.put(dbN, cnt);
    } else {
      logging(dbN+" is closed.");
      odmsLst.get(dbN).close();
      dbActive.remove(dbN);
      odmsLst.remove(dbN);
      dbLst.remove(dbN);
    }
  }
  //
  private synchronized void putKey(String uID, String key) {
    ArrayList<String> kLst = users.get(uID);
    if (kLst == null) kLst = new ArrayList<String>();
    if (!kLst.contains(key)) {
      kLst.add(key);
      users.put(uID, kLst);
    }
  }
  // check ownership
  private boolean isOwner(String uID, String dbName) {
    ArrayList<String> lst = dbOwner.get(uID);
    if (lst != null && lst.contains(dbName)) return true;
    return false;
  }
  // data area
  public  String dbPath;
  private OutputStream logger;
  private ArrayList<String> dbLst;
  private HashMap<String, ODMS> odmsLst;
  private HashMap<String, Integer> dbActive;
  private HashMap<String, HashMap<String, String>> kOwner; 
  private HashMap<String, ArrayList<String>> users, dbOwner;
}

End of Part II

Next: Multiuser Environment - Networking ODB-Server

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Joe

28 bài viết.
207 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
27 11
Fuzzy Logic and Machine Learning Hi First of all: I apologize everyone for my writing in English. I come to this site because someone of Daynhauh...
Joe viết 12 tháng trước
27 11
White
25 11
You're a fresh graduate and work for more than 12 months in an IT company with some boring coding tasks... The tasks are unchallenging. Day in, day...
Joe viết 1 tháng trước
25 11
White
23 14
Chao cac ban To the Admins: if you think that this posting breaks some rules of your site please just delete it. NO need to send me a feedback. Th...
Joe viết 21 ngày trước
23 14
Bài viết liên quan
White
11 9
Chao cac ban, Weeks ago I wrote a blog Object Data Management System (ODMS) The foundation of OO Database (OODB)](https://kipalog.com/posts/Object...
Joe viết 19 ngày trước
11 9
White
5 1
Chao cac ban, Today I lead you into the Client world. A partial world of the myterious ClientServer castle. We have completed the Server part with...
Joe viết 13 ngày trước
5 1
White
4 2
Chao cac ban, This is the last part of the series "Object Database Design & Implementation". The basic ODMSfunctions (READ, WRITE, DELETE, REPLACE...
Joe viết 12 ngày trước
4 2
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
28 bài viết.
207 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!