Jeff's Blog

Musings about software development, Java, OO, agile, life, whatever.


Friday, February 03, 2006 
Database TDD Part 28: Generating Database Tables

Somewhere between last week's blog entry and this one, I decided it was long past time for my annual machine rebuild. It doesn't take long for a Windows machine to get overloaded with cruft. Some of it I put there on purpose, some of it I didn't. Attempts at routine maintenance ultimately can't hold up to all the things that want to invade your system.

So I put everything, or so I thought, in one spot to be backed up. Actually I've gotten my machine to a point where almost everything critical is in one place: under my profile directory. My Eclipse workspace, my documents, my FitNesse installation (I think I need to figure out how to install the product elsewhere and have it point to the profile directory for its documents), my email, my CVS repository, and just about anything else I want to have each time I rebuild a machine. That includes a list of software to download and install, not the software itself (unless I'm married to a specific version). It amounts to only about half a gig, enough to back up on a CD.

There's always something I forget to centralize or back up. This time I forgot to back up my database. There's no valuable data in it currently; about the only thing I wanted to keep was the schema for this blog series. Oh well.

After spending a bunch of time rebuilding the machine (there are so many pieces of software I don't remember having, but I did, and they need to be there), I'm now ready to get back to the blog series. I remembered my mistake when I reopened the Eclipse project. Many of the tests don't run, since there is no database! I was going to tackle something different in this blog installment, but I guess now is as good as any time to deal with the relationship between the application and the database.

If you're fortunate enough to have control of the database for your application needs, there's little reason to design it any differently than the application's object structure (performance might be one reason). You might be in a less fortunate situation, where you have a high-falutin' DBA dictating what you're going to have to work with.

If you're not stuck with someone else's mandate, you can treat the database schema and persistable class definitions as different views on the same entity. One can generate the other, and vice versa. Since I no longer have a schema, I'm going to have the application generate it.

I scraped together the following test:

package schema;

import junit.framework.*;

public class TableGeneratorTest extends TestCase {
   public void testSimple() {
      TableGenerator<Simple> generator = new TableGenerator<Simple>();
      generator.create(new SimpleAccess());

      SimpleAccess access = new SimpleAccess();
      final String name = "abc";
      Simple simple = new Simple(name);
      access.save(simple);
      Simple retrieved = access.find(name);
      assertEquals(name, retrieved.getName());
   }
}
This test required me to build SimpleAccess and Simple:

Simple

package schema;

import persistence.*;

class Simple implements Persistable {
   public static final String NAME = "name";
   private String name;

   Simple(String name) {
      this.name = name;
   }

   public Object get(String key) {
      return name;
   }

   public String getName() {
      return name;
   }
}

SimpleAccess

package schema;

import java.util.*;

import persistence.*;
import persistence.types.*;

class SimpleAccess extends DataAccess<Simple> {
   private static Column[] COLUMNS = new Column[] {
      new StringColumn(Simple.NAME)
   };

   public String getTable() {
      return "testsimple";
   }

   public String getKeyColumn() {
      return Simple.NAME;
   }

   public Simple create(Map<String, Object> row) {
      return new Simple((String)row.get(Simple.NAME));
   }

   public Column[] getColumns() {
      return COLUMNS;
   }
}
Simple enough. (Sorry!)

Hmm. As soon as I started to code a solution, I realized that I wanted to break it down and code a test to prove I can build the correct SQL. Moving over to SqlTest, I wrote:

public void testCreateTable() {
   String statement = String.format(
         "create table t (a varchar(%d),b varchar(%<d))",
         StringColumn.DEFAULT_WIDTH);
   assertEquals(statement, sql.create());
}
I got it to pass with this code in Sql:
public String create() {
   Transformer columnDefinition = new Transformer() {
      public String transform(Object input) {
         Column column = (Column)input;
         String declaration = String.format("varchar(%d)", StringColumn.DEFAULT_WIDTH);
         return String.format("%s %s", column.getName(), declaration);
      }};
   String columnDefinitions = StringUtil.commaDelimit(columns, columnDefinition);
   return String.format("create table %s (%s)", table, columnDefinitions);
}
Which, of course, only supports fields of type String. I'll fix that, but for now, it passes and meets the needs of TableGenerator, which only demonstrates generating tables with VARCHAR columns (I'll fix that too).

For now, I'm ok with assuming all Strings are to be stored with a default width in the database. I'll define DEFAULT_WIDTH in StringColumn as:

public static final int DEFAULT_WIDTH = 32;

With the Sql modification done, I coded TableGenerator.

package schema;

import persistence.*;
import sql.*;

public class TableGenerator<T extends Persistable> {
   public void create(DataAccess<T> access) {
      String sql = new Sql(access.getTable(), access.getColumns()).create();
      new JdbcAccess().execute(sql);
   }
}
Wow. I'm pretty thrilled about how easy it is to code new functionality. That's a very simple class and method, maybe it belongs elsewhere. On the Persister class, perhaps? Maybe later; I need to get the rest of my unit tests working by building their tables.

One minor detail before I fix the problem with data types: I can only run the test once. I've got to ensure we delete the test table. Looking at JdbcAccessTest, its setUp and tearDown code creates and drops a table. I think it's time to add those as capabilities to JdbcAccess itself. Further, I need to be able to try and drop a table without having it throw an exception if no such table exists.

JdbcAccessTest

public class JdbcAccessTest extends TestCase {
   ...
   private JdbcAccess access;

   protected void setUp() {
      access = new JdbcAccess();
      access.execute(String.format("create table %s (%s varchar(1))", TABLE,
            COLUMN_NAME));
   }

   protected void tearDown() {
      access.dropIfExists(TABLE);
   }

   public void testDrop() {
      access.drop(TABLE);
      try {
         access.drop(TABLE);
      }
      catch (JdbcException expected) {
         Exception e = (Exception)expected.getCause();
         assertTrue(e.getMessage(), e.getMessage().indexOf("Unknown table") != -1);
      }
   }

   public void testDropIfExists() {
      access.dropIfExists(TABLE);
      try {
         access.drop(TABLE);
      }
      catch (JdbcException expected) {
         Exception e = (Exception)expected.getCause();
         assertTrue(e.getMessage(), e.getMessage().indexOf("Unknown table") != -1);
      }
      access.dropIfExists(TABLE); // shouldn't throw exception
   }
   ...
I always post my blogs and then review them quickly to correct any formatting errors. In doing so, I notice that testDrop and testDropIfExists contain the same try/catch code. I've already posted the code, but I'm going to make a refactoring change that will appear in the next drop.

JdbcAccess

public void drop(String table) {
   execute("drop table " + table);
}

public void dropIfExists(String table) {
   try {
      drop(table);
   }
   catch (JdbcException expected) {
      Exception e = (Exception)expected.getCause();
      if (e.getMessage().indexOf("Unknown table") == -1)
         throw expected;
   }
}
TableGeneratorTest now looks like this:

TableGeneratorTest

package schema;

import persistence.*;
import junit.framework.*;

public class TableGeneratorTest extends TestCase {
   private JdbcAccess jdbc;
   private SimpleAccess access;

   protected void setUp() {
      jdbc = new JdbcAccess();
      access = new SimpleAccess();
      jdbc.dropIfExists(access.getTable());
   }

   protected void tearDown() {
      jdbc.drop(access.getTable());
   }

   public void testSimple() {
      TableGenerator<Simple> generator = new TableGenerator<Simple>();
      generator.create(new SimpleAccess());

      final String name = "abc";
      Simple simple = new Simple(name);
      access.save(simple);
      Simple retrieved = access.find(name);
      assertEquals(name, retrieved.getName());
   }
}

Maybe I could put the TableGenerator code into JdbcAccess. No, not a good idea. That would create a dependency of JdbcAccess on the classes Persistable and DataAccess. Best to keep JdbcAccess as ignorant of anything else in the system as possible.

One final note: I moved the source folders into a single Eclipse source directory, src. I also included a lib directory with the MySQLConnector JAR file. If you download the code archive, your best bet might be to start afresh with a new project.

OK, now time to go back to SqlTest and get the create method to support IntegerColumn. Right now the tests all use the COLUMNS constant, currently defined as:

private static Column[] COLUMNS = { new StringColumn("a"),
      new StringColumn("b") };
I'll change that to:
private static Column[] COLUMNS = { new StringColumn("a"),
      new IntegerColumn("b") };
...and see what breaks. Two test methods break. I change testInsert to:
public void testInsert() {
   final Object[] values = { "1", 2 };
   assertEquals("insert into t (a,b) values ('1',2)", sql.insert(values));
}
In testCriteria, I have to switch around the Equals and Like criteria clauses.
public void testCriteria() {
   int value = 1;
   String pattern = "p%";
   Criteria criteria = new And(new Equals(COLUMNS[1], value), new Like(
         COLUMNS[0], pattern));
   String sqlString = sql.select(criteria);
   assertEquals("select a,b from t where (b=1) and (a like 'p%')",
         sqlString);
}
Now I can fix testCreateTable (which I just now chose to rename to testCreate).
public void testCreate() {
   String statement = String.format(
         "create table t (a varchar(%d),b integer)",
         StringColumn.DEFAULT_WIDTH);
   assertEquals(statement, sql.create());
}
I can get that to quickly pass with this nasty bit of code:
public String create() {
   Transformer columnDefinition = new Transformer() {
      public String transform(Object input) {
         Column column = (Column)input;
         String declaration = null;
         if (column instanceof StringColumn)
            declaration = String.format("varchar(%d)", StringColumn.DEFAULT_WIDTH);
         else
            declaration = "integer";
         return String.format("%s %s", column.getName(), declaration);
      }};
   String columnDefinitions = StringUtil.commaDelimit(columns, columnDefinition);
   return String.format("create table %s (%s)", table, columnDefinitions);
}
Horrors. That defeats the point of the having the Column type hierarchy in the first place. I'll move things over.

Here are all the changes to the Column type hierarchy to support deriving a declaration from a given Column object:

Column

package persistence.types;

public interface Column {
   String getName();
   String sqlValue(Object object);
   String declaration();
}

StringColumnTest

public void testDeclaration() {
   assertEquals(String.format("%s varchar(%s)", ColumnTest.NAME,
         StringColumn.DEFAULT_WIDTH), column.declaration());
}

StringColumn

public String declaration() {
  return String.format("%s varchar(%s)", super.getName(), DEFAULT_WIDTH);
}

IntegerColumnTest

public void testDeclaration() {
   assertEquals(String.format("%s integer", ColumnTest.NAME), column
         .declaration());
}

IntegerColumn

public String declaration() {
   return String.format("%s integer", super.getName());
}

Now I can fix the code in the create method.

public String create() {
   Transformer columnDefinition = new Transformer() {
      public String transform(Object input) {
         return ((Column)input).declaration();
      }};
   String columnDefinitions = StringUtil.commaDelimit(columns, columnDefinition);
   return String.format("create table %s (%s)", table, columnDefinitions);
}

OK, last step. I wrote this crummy little utility class to generate the tables I need:

package schema;

import domain.*;

public class TableCreator {
   public static void main(String[] args) {
      new TableGenerator<Customer>().create(new CustomerAccess());
      new TableGenerator<User>().create(new UserAccess());
   }
}
I'm not sure where to put this class or what to do with. I don't think I want to run it all the time, i.e. as part of my JUnit runs. Or maybe I do. For now, I don't care. I run TableCreator. I run my unit tests. All green! Time to get ready for the weekend.

code


Comments:
Good afternoon many have machines which not insuranced,
you have a unique opportunity only today to receive insurance the machine free of charge
auto insurance
auto insurance
cars insurance
cars insurance
texas car insurance
texas car insurance
car insurance quotes
car insurance quotes
car insurance policy
car insurance policy
xanax
phentermine
buy tramadol
 
drew wore overwhelmed hear being intense anymore innocence willingly you hentai
strings position doing felt sakura naruto hentai
Lord face spoils knowing sucked bluntly heading adult picture
strokes beyond lay rise pulling bow pics sexy wow
feeding sexier position nails free Evil pics nude
pull lock home Bratz sexy
allow made completely you experienced curve turn wrapped futurama spreading sex
gray gleaming reposition eliciting contraction lap grasp Please Fututrama adult
point Please sincere by want lessen juices naruto manga
pretend who yes teen titans porn comic
desire gleaming Dear feeding daughters answered action world warcraft
cocks since hazel remove kim possible cartoon sex
turned skin ear God thumbs gentle whisper dish murrr after Tinker Bell
clit needlessly porn with scent hentai futurama amy
 
Post a Comment

Links to this post:

Create a Link



<< Home

RSS Feed (XML)

Archives

February 2004   March 2004   May 2004   September 2004   October 2004   January 2005   February 2005   September 2005   October 2005   November 2005   December 2005   January 2006   February 2006   March 2006   June 2006   August 2006   January 2007   February 2007   March 2007   April 2007   September 2007   October 2007   November 2007   December 2007   January 2008  

This page is powered by Blogger. Isn't yours?