Refactoring Database Access in Marauroa: Difference between revisions

From Arianne
Jump to navigation Jump to search
Content deleted Content added
imported>Hendrik Brummermann
added diff of JMaPacman
imported>Kymara
 
(76 intermediate revisions by 3 users not shown)
Line 1: Line 1:
At the moment database access in marauroa is designed for a single thread application. While this does work most of the time in turn based games (yes, Stendhal is turned based internally), it prevents doing non time critical stuff in another thread. In Stendhal we have the problem that any update or delete operation to the gameEvents table that takes more than a couple of seconds kills the server.
Database access in marauroa was designed for a single thread application. While this works most of the time in turn based games (yes, Stendhal is turn based internally), it prevents doing non time critical stuff in another thread. In Stendhal we have the problem that any update or delete operation to the gameEvents table that takes more than a couple of seconds, kills the server.


== Requirements ==
== Requirements ==
Line 17: Line 17:
== Design ==
== Design ==


[[Image:Marauroa.server.game.db.png]]
<!-- OUTDATED [[Image:Marauroa.server.game.db.png]] -->


== Concept FAQ ==
== Concept FAQ ==
Line 23: Line 23:
=== Where did IDatabase / JDBCDatabase go? ===
=== Where did IDatabase / JDBCDatabase go? ===


It was replaced by smaller classes specialized on one task each. They can be found in the package <code>marauroa.server.game.db</code> ending with "DAO".
It was replaced by smaller classes specializing in one task each. They can be found in the package <code>marauroa.server.game.db</code> and they end with "DAO".


=== What are those new ...DAO classes? How do they work? ===
=== What are those new ...DAO classes? How do they work? ===


They are "data access object". The basic idea is and has been from the start that the database related code is not in the original classes but at some central point outside the game logic. This used to be JDBCDatabase but one single class for all database operations is very unhandy. So JDBCDatabase has been splitted into a number of small classes focused on one area each: AccountDAO, CharacterDAO, GameEventDAO...
DAO stands for "data access object". The basic idea is, and has been from the start, that the database related code is not in the original classes but at some central point outside the game logic. This used to be JDBCDatabase, but one single class for all database operations is very hard to maintain. So JDBCDatabase has been split into a number of small classes focused on one area each: AccountDAO, CharacterDAO, GameEventDAO...


These classes replace the old IDatabase / JDBCDatabase and do the database stuff. All of their methods have two signatures: One with gets an DBTransaction object as first parameter and one without. This is for your convenience: In most cases it those functions are not part of a larger context so you do not have to care about transactions at all because the DAO-classes do the transaction handling on their own. There are, however, a small number of cases in which you want to do multiple calls to DAOs in one single transactions. In this case you get a DBTransaction from the TransactionPool and provide it as first parameter to DAO-methods. After you are done you must either commit or rollback your changes with the appropriate methods in the class TransactionPool.
These classes replace the old IDatabase / JDBCDatabase and do the database stuff. All of their methods have two signatures: One with a DBTransaction object as first parameter and one without. This is for your convenience: In most cases those functions are not part of a larger context so you do not have to care about transactions at all: the DAO-classes do the transaction handling on their own. There are, however, a small number of cases in which you want to do multiple calls to DAOs in one single transaction. In this case you get a DBTransaction from the TransactionPool and provide it as first parameter to DAO-methods. After you are done you must either commit or rollback your changes with the appropriate methods in the class TransactionPool.


=== I have extended the JDBCDatabase class. How does this work with DAOs? ===
=== I have extended the JDBCDatabase class. How does this work with DAOs? ===
Line 35: Line 35:
DAO classes should never be instantiated directly. Instead you should (and marauroa does) use the DAORegistry. This allows you to write a subclass of a DAO provided by marauroa and register it instead. If you are familiar with Spring, this is a similar concept. But without all the bulk of xml configuration files, parameter injection and interfaces with only one single implementation.
DAO classes should never be instantiated directly. Instead you should (and marauroa does) use the DAORegistry. This allows you to write a subclass of a DAO provided by marauroa and register it instead. If you are familiar with Spring, this is a similar concept. But without all the bulk of xml configuration files, parameter injection and interfaces with only one single implementation.


Imagine you want to subclass the CharacterDAO with your class SomeGameCharaterDAO:
Imagine you want to subclass the CharacterDAO with your class SomeGameCharacterDAO:
<source lang="java">
public class SomeGameCharacterDAO extends CharacterDAO {
public class SomeGameCharacterDAO extends CharacterDAO {
...
...
</source>

You simply register it as
You simply register it as
<source lang="java">
DAORegistry.get().register(CharacterDAO.class, new SomeGameCharaterDAO());
DAORegistry.get().register(CharacterDAO.class, new SomeGameCharacterDAO());
Note: In the register call the first parameter is the parent clase you want to replace.
</source>
Note: In the register call the first parameter is the parent class you want to replace.




=== What are those database adapters for? ===
=== What are those database adapters for? ===


The are a thin layer of database abstraction. This allows us to not only support MySQL but also database systems with don't require an external server.
They are a thin layer of database abstraction. This allows us to not only support MySQL but also database systems which don't require an external server.



=== Why is there a method getLastInsertId? ===
=== Why is there a method getLastInsertId? ===
Line 66: Line 68:
=== What happened to Transaction, JDBCTransaction, Accessor, JDBCAccess? ===
=== What happened to Transaction, JDBCTransaction, Accessor, JDBCAccess? ===


They where replaced with DBTransaction. You can get a DBTransaction from the TransactionPool. Make sure that you commit or rollback it after use.
They were replaced with DBTransaction. You can get a DBTransaction from the TransactionPool. Make sure that you commit or rollback it after use.


=== What happend to JDBCSQLHelper and StringChecker? ===
=== What happened to JDBCSQLHelper and StringChecker? ===


They have been moved to marauroa.server.db
They have been moved to marauroa.server.db
Line 79: Line 81:
== Porting of JMaPacman ==
== Porting of JMaPacman ==


The following diff shows all the changes that were required to port JMaPacman. I think it may help you to get a feeling on how to adjust your own code.
The following diff shows the complete list of changes that were required to port JMaPacman. I think it may help you to get a feeling on how to adjust your own code.
<!--

Don't use syntax highlighting here because it does not work well with the diff characters and bold formation
<pre>
<source lang="java">
public class MaPacmanRPRuleProcessor implements IRPRuleProcessor
-->
{
public class MaPacmanRPRuleProcessor implements IRPRuleProcessor
public AccountResult createAccount(String username, String password, String email) {
{
- IDatabase database = DatabaseFactory.getDatabase();
public AccountResult createAccount(String username, String password, String email) {
+ TransactionPool transactionPool = TransactionPool.get();
- Transaction trans = database.getTransaction();
+ DBTransaction trans = transactionPool.beginWork();
+ AccountDAO accountDAO = DAORegister.get().get(AccountDAO.class);
try {
- trans.begin();
- IDatabase database = DatabaseFactory.getDatabase();
- if (database.hasPlayer(trans, username)) {
+ '''TransactionPool transactionPool = TransactionPool.get();'''
+ if (accountDAO.hasPlayer(trans, username)) {
return new AccountResult(Result.FAILED_PLAYER_EXISTS, username);
}
- Transaction trans = database.getTransaction();
- database.addPlayer(trans, username, Hash.hash(password), email);
+ '''DBTransaction trans = transactionPool.beginWork();'''
+ accountDAO.addPlayer(trans, username, Hash.hash(password), email);
+ '''AccountDAO accountDAO = DAORegister.get().get(AccountDAO.class);'''
- trans.commit();
try {
+ transactionPool.commit(trans);
- trans.begin();
- if (database.hasPlayer(trans, username)) {
+ if ('''accountDAO'''.hasPlayer(trans, username)) {
return new AccountResult(Result.FAILED_PLAYER_EXISTS, username);
}
- database.addPlayer(trans, username, Hash.hash(password), email);
+ '''accountDAO'''.addPlayer(trans, username, Hash.hash(password), email);
- trans.commit();
+ '''transactionPool.commit(trans);'''
return new AccountResult(Result.OK_CREATED, username);
} catch (SQLException e1) {
- try {
return new AccountResult(Result.OK_CREATED, username);
- trans.rollback();
} catch (SQLException e1) {
- try {
- } catch (SQLException e2) {
- trans.rollback();
- //logger.error("Rollback failed: ", e2);
- } catch (SQLException e2) {
- System.out.println("Rollback failed: " + e2);
- }
- //logger.error("Rollback failed: ", e2);
+ '''transactionPool.rollback(trans);'''
- System.out.println("Rollback failed: " + e2);
- }
return new AccountResult(Result.FAILED_EXCEPTION, username);
+ transactionPool.rollback(trans);
}
return new AccountResult(Result.FAILED_EXCEPTION, username);
}
}
}
public CharacterResult createCharacter(String username, String character, RPObject tmpl) {
- IDatabase database = DatabaseFactory.getDatabase();
+ '''TransactionPool transactionPool = TransactionPool.get();'''
- Transaction trans = database.getTransaction();
+ '''DBTransaction trans = transactionPool.beginWork();'''
+ CharacterDAO characterDAO = DAORegister.get().get(CharacterDAO.class);
try {
- trans.begin();
- if (database.hasCharacter(trans, username, character)) {
+ if ('''characterDAO'''.hasCharacter(trans, username, character)) {
return new CharacterResult(Result.FAILED_PLAYER_EXISTS, character, tmpl);
}
@@ -196,18 +190,13 @@
- database.addCharacter(trans, username, character, object);
+ '''characterDAO'''.addCharacter(trans, username, character, object);
- trans.commit();
+ '''transactionPool.commit(trans);'''
return new CharacterResult(Result.OK_CREATED, character, object);
} catch (Exception e1) {
//logger.warn("SQL exception while trying to create a new character: ", e1);
- try {
- trans.rollback();
- } catch (SQLException e2) {
- //logger.error("Rollback failed: ", e2);
- System.out.println("Rollback failed: " + e2);
- }
+ '''transactionPool.rollback(trans);'''
return new CharacterResult(Result.FAILED_EXCEPTION, character, tmpl);
}
}




[[Category:Marauroa]]
public CharacterResult createCharacter(String username, String character, RPObject tmpl) {
- IDatabase database = DatabaseFactory.getDatabase();
- Transaction trans = database.getTransaction();
+ TransactionPool transactionPool = TransactionPool.get();
+ DBTransaction trans = transactionPool.beginWork();
+ CharacterDAO characterDAO = DAORegister.get().get(CharacterDAO.class);
try {
- trans.begin();
- if (database.hasCharacter(trans, username, character)) {
+ if (characterDAO.hasCharacter(trans, username, character)) {
return new CharacterResult(Result.FAILED_PLAYER_EXISTS, character, tmpl);
}
@@ -196,18 +190,13 @@
object.put("y", 0);
object.put("dir", -1);
object.put("score", 0);
- database.addCharacter(trans, username, character, object);
+ characterDAO.addCharacter(trans, username, character, object);
- trans.commit();
+ transactionPool.commit(trans);
return new CharacterResult(Result.OK_CREATED, character, object);
} catch (Exception e1) {
//logger.warn("SQL exception while trying to create a new character: ", e1);
- try {
- trans.rollback();
- } catch (SQLException e2) {
- //logger.error("Rollback failed: ", e2);
- System.out.println("Rollback failed: " + e2);
- }
+ transactionPool.rollback(trans);
return new CharacterResult(Result.FAILED_EXCEPTION, character, tmpl);
}
}
</pre>

Latest revision as of 09:39, 3 May 2010

Database access in marauroa was designed for a single thread application. While this works most of the time in turn based games (yes, Stendhal is turn based internally), it prevents doing non time critical stuff in another thread. In Stendhal we have the problem that any update or delete operation to the gameEvents table that takes more than a couple of seconds, kills the server.

Requirements

Hard Requirements

  • multi thread support
  • parallel transactions in different threads

Would be nice

  • a package of small classes instead of huge JDBCDatabase / StendhalPlayerDatabase
  • an small abstraction layer to support different databases beside mysql (small as in "not hibernate")
  • no extra dependencies on new libraries
  • easier way to include variables into sql statements

Design

Concept FAQ

Where did IDatabase / JDBCDatabase go?

It was replaced by smaller classes specializing in one task each. They can be found in the package marauroa.server.game.db and they end with "DAO".

What are those new ...DAO classes? How do they work?

DAO stands for "data access object". The basic idea is, and has been from the start, that the database related code is not in the original classes but at some central point outside the game logic. This used to be JDBCDatabase, but one single class for all database operations is very hard to maintain. So JDBCDatabase has been split into a number of small classes focused on one area each: AccountDAO, CharacterDAO, GameEventDAO...

These classes replace the old IDatabase / JDBCDatabase and do the database stuff. All of their methods have two signatures: One with a DBTransaction object as first parameter and one without. This is for your convenience: In most cases those functions are not part of a larger context so you do not have to care about transactions at all: the DAO-classes do the transaction handling on their own. There are, however, a small number of cases in which you want to do multiple calls to DAOs in one single transaction. In this case you get a DBTransaction from the TransactionPool and provide it as first parameter to DAO-methods. After you are done you must either commit or rollback your changes with the appropriate methods in the class TransactionPool.

I have extended the JDBCDatabase class. How does this work with DAOs?

DAO classes should never be instantiated directly. Instead you should (and marauroa does) use the DAORegistry. This allows you to write a subclass of a DAO provided by marauroa and register it instead. If you are familiar with Spring, this is a similar concept. But without all the bulk of xml configuration files, parameter injection and interfaces with only one single implementation.

Imagine you want to subclass the CharacterDAO with your class SomeGameCharacterDAO: <source lang="java">

    public class SomeGameCharacterDAO extends CharacterDAO {
    ...

</source> You simply register it as <source lang="java">

    DAORegistry.get().register(CharacterDAO.class, new SomeGameCharacterDAO());

</source> Note: In the register call the first parameter is the parent class you want to replace.


What are those database adapters for?

They are a thin layer of database abstraction. This allows us to not only support MySQL but also database systems which don't require an external server.

Why is there a method getLastInsertId?

It is a replacement for "select LAST_INSERT_ID() as inserted_id" which is a MySQL "feature". By using getLastInsertId() instead of that SQL statement, your code will work with other database systems as well (unless you use other system specific functions).


Porting FAQ

Does this mean I have to adjust my game?

That depends. If you did not do any low level database stuff, there should be no changes required. As a rule of thumb: If your code compiles with the new version of Marauroa, you are fine.

What happened to IDatabase/JDBCDatabase?

It was replaced by smaller DAO classes. See above for how to use them.

What happened to Transaction, JDBCTransaction, Accessor, JDBCAccess?

They were replaced with DBTransaction. You can get a DBTransaction from the TransactionPool. Make sure that you commit or rollback it after use.

What happened to JDBCSQLHelper and StringChecker?

They have been moved to marauroa.server.db

I provided my own subclass of IDatabase/JDBCDatabase

Please have a look at the ...DAO classes in the marauroa.server.game.db package and split your sub class accordingly. After that is done you need to register them in the DAORegistry with the class of the original DAO and an instance of your class.


Porting of JMaPacman

The following diff shows the complete list of changes that were required to port JMaPacman. I think it may help you to get a feeling on how to adjust your own code.

 public class MaPacmanRPRuleProcessor implements IRPRuleProcessor
   {
   public AccountResult createAccount(String username, String password, String email) {

-    IDatabase database = DatabaseFactory.getDatabase();
+    TransactionPool transactionPool = TransactionPool.get();

-    Transaction trans = database.getTransaction();
+    DBTransaction trans = transactionPool.beginWork();
+    AccountDAO accountDAO = DAORegister.get().get(AccountDAO.class);

     try {
-      trans.begin();
 
-      if (database.hasPlayer(trans, username)) {
+      if (accountDAO.hasPlayer(trans, username)) {
         return new AccountResult(Result.FAILED_PLAYER_EXISTS, username);
       }
 
-      database.addPlayer(trans, username, Hash.hash(password), email);
+      accountDAO.addPlayer(trans, username, Hash.hash(password), email);
 
-      trans.commit();
+      transactionPool.commit(trans);
 
       return new AccountResult(Result.OK_CREATED, username);
     } catch (SQLException e1) {

-      try {
-        trans.rollback();
-      } catch (SQLException e2) {
-        //logger.error("Rollback failed: ", e2);
-        System.out.println("Rollback failed: " + e2);
-      }
+      transactionPool.rollback(trans);

       return new AccountResult(Result.FAILED_EXCEPTION, username);
     }
   }


   public CharacterResult createCharacter(String username, String character, RPObject tmpl) {

-    IDatabase database = DatabaseFactory.getDatabase();
+    TransactionPool transactionPool = TransactionPool.get();

-    Transaction trans = database.getTransaction();
+    DBTransaction trans = transactionPool.beginWork();
+    CharacterDAO characterDAO = DAORegister.get().get(CharacterDAO.class);

     try {
-      trans.begin();
 
-      if (database.hasCharacter(trans, username, character)) {
+      if (characterDAO.hasCharacter(trans, username, character)) {
         return new CharacterResult(Result.FAILED_PLAYER_EXISTS, character, tmpl);
       }

@@ -196,18 +190,13 @@

-      database.addCharacter(trans, username, character, object);
+      characterDAO.addCharacter(trans, username, character, object);

-      trans.commit();
+      transactionPool.commit(trans);

       return new CharacterResult(Result.OK_CREATED, character, object);
     } catch (Exception e1) {
       //logger.warn("SQL exception while trying to create a new character: ", e1);

-      try {
-        trans.rollback();
-      } catch (SQLException e2) {
-        //logger.error("Rollback failed: ", e2);
-        System.out.println("Rollback failed: " + e2);
-      }
+      transactionPool.rollback(trans);

       return new CharacterResult(Result.FAILED_EXCEPTION, character, tmpl);
     }
   }