4

I taught a course years ago, and then I used some JDBC exercises using what I now know is the Active Record Pattern.

I would like to modernize the exercise by changing the Active Record Pattern so classes don't do their own persistence. But I would not like to use any persistence or ORM framework for teaching purposes.

A sample class is Vehicle:

  • It receives a Connection and plate number in the constructor.
  • In the constructor it queries the database and populates the class members and a boolean member existInDB is set to true.
  • If the plate is not found in the DB, existInDB remains false.
  • You can change the state of the object with setters.
  • If you call save() it either inserts in the database if existInDB is false or updates a row in the database if existInDB is false.
  • After refactoring the class would not be a simple data transfer object because it has a doSomeBusinessThing() method that does some business thing.
  • All other classes like Maker, Owner, also use the Active Record Pattern, but we can leave them alone for now.

This is not production code. It's Java SE course material. I don't want to introduce frameworks to people who are learning the language and/or OOP principles for the first time. One requirement of my question is that it must be POJO-based. Maybe using a design pattern but no external frameworks or ORM.

I would like to read your advice on how to do such a refactoring.

The vehicle class:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Vehicle {

    private String plate;
    private Model model;
    private Owner owner;
    private String color;
    private String status;
    private int year;
    private boolean existInDB= false;
    private Connection conn;
    private ResultSet rs;

    public Vehicle(Connection con_,String plate_) throws SQLException {
        String sql=
        "select \n"+ 
        "       v.plate, \n" +
        "       v.model_id, \n"+
        "       v.owner_id, \n"+
        "       v.color, \n"+
        "       v.status, \n"+
        "       v.year \n"+
        "from \n"+
        "     vehicle v \n"+
        "where \n"+
        "     v.plate = ? \n";

        PreparedStatement ps = con_.prepareStatement(sql,ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE);
        ps.setString(1, plate_);
        rs = ps.executeQuery();

        plate = plate_;
        conn = con_;

        if (rs.next()){         
            try {
                model = new Model(conn,rs.getString("model_id"));
            } catch (UnknownMakerException e) {
                e.printStackTrace();
            } catch (UnknownModelException e) {
                e.printStackTrace();
            }
            try {
                owner = new Owner(conn,rs.getInt("owner_id"));
            } catch (UnknownOwnerException e) {
                e.printStackTrace();
            }
            color = rs.getString("color");
            status = rs.getString("status");
            year = rs.getInt("year");
            existInDB = true;   
        }

    }

    public void save() throws SQLException{
        if (existsInDB()){
            rs.absolute(1);
            rs.updateString("plate",plate);
            rs.updateString("model_id", model.getID());
            rs.updateInt("owner_id", owner.getID());
            rs.updateString("color", color);
            rs.updateString("status", status);
            rs.updateInt("year", year);
            rs.updateRow();             
        } else {
            rs.moveToInsertRow();
            rs.updateString("plate", plate);
            rs.updateString("model_id", model.getID());
            rs.updateInt("owner_id", owner.getID());
            rs.updateString("color", color);
            rs.updateString("status", status);
            rs.updateInt("year", year);
            rs.insertRow();     
        }


    }

    public String getPlate() {
        return plate;
    }

    public Model getModel() {
        return model;
    }

    public Owner getOwner() {
        return owner;
    }

    public String getColor() {
        return color;
    }

    public String getStatus() {
        return status;
    }

    public int getYear() {
        return year;
    }

    public boolean existsInDB() {
        return existInDB;
    }

    public void setPlate(String plate) {
        this.plate = plate;
    }

    public void setModel(String  modelID_) throws SQLException, UnknownModelException {
        try {
            this.model = new Model(conn,modelID_);
        } catch (UnknownMakerException e) {
            e.printStackTrace();
        }
    }

    public void setOwner(int ownerID) throws SQLException, UnknownOwnerException {
        this.owner = new Owner(conn,ownerID);
    }

    public void setColor(String color) {
        this.color = color;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public void setYear(int year) {
        this.year = year;
    }

    protected void finalize() throws Throwable,  SQLException
    {
        try {
            PreparedStatement stmt = (PreparedStatement) rs.getStatement();
            rs.close();
            stmt.close();
            stmt = null;
        } finally {
            super.finalize();
        }
    }   

    public String getDescription(){
        return
        this.getModel().getMaker().getName() + " " +
        this.getModel().getName() + " " +
        this.getColor() + " " +
        this.getYear();

    }

    public String toString(){
        return
        "\nVEHICLE" +
        "\nPlate: " +this.getPlate() + 
        "\nModel ID: " +this.getModel().getID() + 
        "\nOwner ID: " +this.getOwner().getID() +
        "\nColor: " +this.getColor() +
        "\nStatus: " +this.getStatus() +
        "\nYear: " +this.getYear();
    }

    public void doSomeBusinessThing(){
        /* some important business logic just to clarify 
         * that this is not a simple data transfer object
         */

    }

}
  • 1
    Before you actually do this, will you gain anything by doing so, other than "modernization?" – Robert Harvey Aug 19 '16 at 21:44
  • @RobertHarvey Separation of concerns and learning how to convert one thing using one pattern into one using the other pattern that complies with separation of concerns so I can teach it in my course. – Tulains Córdova Aug 19 '16 at 22:12
  • what about using a micro ORM such as dapper(github.com/StackExchange/dapper-dot-net)? – Sufian Saory Aug 30 '16 at 23:58
  • 1
    @SufianSaory That's no production code. It's a Java SE course. I don't want to introduce frameworks to people who is learning the language and/or OOP principles. One requirement of my question is that it must be POJO based. Maybe using a design pattern but no external frameworks. – Tulains Córdova Aug 31 '16 at 0:01
  • 1
    I just remembered that one day I read this post exposing an alternative to do the persistence from somebody hating ORM and ActiveRecord. – Spotted Aug 31 '16 at 5:49
2
+50

Leaving as much of your original code as possible, I would refactor the Vehicle class so that it is constructed using a new Importer interface and saved using a new Exporter interface.

public class Vehicle {

    // fields omitted

    public Vehicle(Importer importer) {
        plate = importer.plate();
        model = importer.model();
        owner = importer.owner();
        color = importer.color();
        status = importer.status();
        year = importer.year();
    }

    public interface Importer {
        String plate();
        Model model();
        Owner owner();
        String color();
        String status();
        int year();
    }

    public void save(Exporter exporter) {
        exporter.plateIs(plate)
                .modelIs(model)
                .ownerIs(owner)
                .colorIs(color)
                .statusIs(status)
                .yearIs(year)
                .export();
    }

    public interface Exporter {
        Exporter plateIs(String plate);
        Exporter modelIs(Model model);
        Exporter ownerIs(Owner owner);
        Exporter colorIs(String color);
        Exporter statusIs(String status);
        Exporter yearIs(int year);
        void export();
    }

    // rest of class unchanged
}

At this point the class no longer does its own persistence and we can write importers and exporters for various persistence technologies. I would now write a SqlVehicleImporter (using your original SQL code)...

public class SqlVehicleImporter implements Vehicle.Importer {

    private String plate;
    private Model model;
    private Owner owner;
    private String color;
    private String status;
    private int year;

    public SqlVehicleImporter(Connection con_, String plate_) throws SQLException {
        String sql=
                "select \n"+
                        "       v.plate, \n" +
                        "       v.model_id, \n"+
                        "       v.owner_id, \n"+
                        "       v.color, \n"+
                        "       v.status, \n"+
                        "       v.year \n"+
                        "from \n"+
                        "     vehicle v \n"+
                        "where \n"+
                        "     v.plate = ? \n";

        PreparedStatement ps = con_.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE);
        ps.setString(1, plate_);
        ResultSet rs;
        rs = ps.executeQuery();

        plate = plate_;

        if (rs.next()){
            try {
                model = new Model(con_,rs.getString("model_id"));
            } catch (UnknownMakerException e) {
                e.printStackTrace();
            } catch (UnknownModelException e) {
                e.printStackTrace();
            }
            try {
                owner = new Owner(con_,rs.getInt("owner_id"));
            } catch (UnknownOwnerException e) {
                e.printStackTrace();
            }
            color = rs.getString("color");
            status = rs.getString("status");
            year = rs.getInt("year");
        }

        rs.close();
        ps.close();
    }

    // interface implementation omitted
}

...and a SqlVehicleExporter.

public class SqlVehicleExporter implements Vehicle.Exporter {

    private final Connection connection;

    // fields omitted

    public SqlVehicleExporter(Connection connection) {
        this.connection = connection;
    }

    public Vehicle.Exporter plateIs(String plate) {
        this.plate = plate;
        return this;
    }

    // repetitive interface methods omitted

    public void export() {
        String sql=
                "select \n"+
                        "       v.plate, \n" +
                        "       v.model_id, \n"+
                        "       v.owner_id, \n"+
                        "       v.color, \n"+
                        "       v.status, \n"+
                        "       v.year \n"+
                        "from \n"+
                        "     vehicle v \n"+
                        "where \n"+
                        "     v.plate = ? \n";

        try {
            PreparedStatement ps = connection.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
            ps.setString(1, plate);
            ResultSet rs = ps.executeQuery();
            boolean existsInDB = rs.next();

            if (existsInDB) {
                rs.absolute(1);
                rs.updateString("plate", plate);
                rs.updateString("model_id", model.getID());
                rs.updateInt("owner_id", owner.getID());
                rs.updateString("color", color);
                rs.updateString("status", status);
                rs.updateInt("year", year);
                rs.updateRow();
            } else {
                rs.moveToInsertRow();
                rs.updateString("plate", plate);
                rs.updateString("model_id", model.getID());
                rs.updateInt("owner_id", owner.getID());
                rs.updateString("color", color);
                rs.updateString("status", status);
                rs.updateInt("year", year);
                rs.insertRow();
            }
        } catch(SQLException error) {
            error.printStackTrace();
        }
    }
}

At this point there is code that is common between the importer and exporter so it might be a good idea to combine them.

  • Is that Java? I don't recognize some of the syntax. – Tulains Córdova Sep 2 '16 at 15:01
  • It was written using Java 1.7. Which parts of the syntax are you referring to? – John Nash Sep 2 '16 at 15:22
  • Maybe I'm not yet familiarized with the way multiple methods are called in the save method in Vehicle in a single instruction. Also the the setters in the Exporter interface has no void return type, they seem to return Exporter. Only save returns void. – Tulains Córdova Sep 2 '16 at 15:33
  • Yes, I wrote the Exporter interface to support this sort of method chaining (see Fluent interface). If you prefer, the code in the save method could call each method of the exporter separately as single statements and each method in the exporter interface could have a void return type. – John Nash Sep 2 '16 at 19:58
  • No. That's OK. My ignorance. I guess I was stuck in Java 1.5 because of my job. I'm catching up with additions to Java syntax in newer versions. – Tulains Córdova Sep 2 '16 at 20:01
2

Here my approach. Based on this premises:

I would like to modernize the exercise by changing the Active Record Pattern so classes don't do their own persistence

I would not like to use any persistence or ORM framework for teaching purposes.

I don't want to introduce frameworks to people who is learning the language and/or OOP principles for the first time. One requirement of my question is that it must be POJO-based. Maybe using a design pattern but no external frameworks or ORM

Separation of concerns

Seems evident that @Tulains would like to teach that in OOP, separation of concerns matters. This pattern design will make think on "who is responsable for each task". It will lead us to code more components (indeed) but they are going to be simpler an easier to understand.

1. POJO First concern. The POJO itself. Like POJO means (Plain Old Java Object) it's just a mere data container, which, in our case, it also have some features like doSomeBusinessThing. It's important to point out here to students that they should realize how Vehicle no longer care about how-when Model and Onwer are obtained or persisted... It's not its problem :-). It's annother's concern

public class Vehicle {

   private String plate;
   private Model model;
   private Owner owner;
   private String color;
   private String status;
   private int year;
   private boolean existInDB;

   public Vehicle(String plate){
      this.plate = plate;
      existInDB = false;
   }

   //Define as many constructs you need here... We only need one atm

   //Getters and setters here...
   ...
   //getDescriptions and toString methods here
   ...

   public void doSomeBusinessThing(){
   }
}

NOTE: I guess some people will argue with me about why I kept existInDB instead of implement an specific DAO method for such purpose. I will leave that refactor to @Tulains. He can introduce what change are need and its implications (for every insert or update, another query should be executed in order to check if vehicle exist or not...)

2. DAO Second concern, the database access. Everything realted to the data base access is good to keep it simple and reusable. Here we face the first issue. PreparedStatements are different for each POJO (select, inserts and updates) so I will make one DAO per Entity. I will just code one of them. The 3 DAOs are very similar. Once implemented, @Tulains, you have an excelent situation to introduce inheritance through abstract classes.

I could also make DAO to return Vehicle but then, VehicleDAO would be forced to have access to Model and Onwer data. I have to be consequent with my design. So I will let other components to deal with Model and Owner data.

public class VehicleDAO {
      private DataSource ds;
      private static final String SELECT_VEHICLE_STMNT = "select ...";
      private static final String INSERT_VEHICLE_STMNT = "insert ...";
      private static final String UPDATE_VEHICLE_STMNT = "update ...";
      public VehicleDAO (DataSource ds){
         this.ds = ds;
      }

      public Map<String,Object> find(Stirng plate) throws VehicleNotExistException, SQLException{
          // ... PreparedStatement here
          // ... dump rs data into a Map
          // ... if rs.next() fails. Then throw VehicleNotExistException
          // ... rs.close();
          //I do return a Map because I want they know that the responsable of RS is the DAO. It is also responsable of to close it
          return map;
      }

      public void saveOrUpdate(Vehicle vehicle) throws SQLException {
         if(vehicle.existsInDB()){
             //PreparedStatement for upate here
         }else{
             //PreparedStatement for insert here
         }             
         // I have decided to use preparedStatements insted of ResulSet API like it was in the original code. I have been thinking on myBatis which works with prepared statements all the way            
      }

      private Connection getConnection(){
          //creates a new connection or recover an existing one.
      }
}

3. App

In my previous answer, the next component was a sort of "Service" which would be located somewhere in a business layer. Due to the purpose of this post is to introduce Java to beginners I have removed such component because there're too many implicit concepts of design behind and it could lead students to confusion. So I have turned that service into a main class. The goal is to explain the basics of java and jdbc.

public class MyApp {

      private static VehicleDAO vDao;
      private static ModelDAO mDao;
      private static OwnerDAO oDao;

      public static void main(String[] arg){
            bootstrap();
            demoRead();
            demoSaveOrUpdate();
            shutdown();
      }

      public static void demoSaveOrUpdate() throws Exception{
            Map<String, Object> vehicleMap = vDao.find("plate");
            Vehicle vehicle = new Vehicle(vehicleMap.get("plate"));

            if(vehicleMap.get("model_ID") != null){
                Map<String, Object> modelMap = mDao.find(vehicleMap.get("model_ID"));
                Model model = new Model(modelMap.get("id"));
                model.set...
                //and so on..
                vehicle.setModel(model);
            }

            if(vehicleMap.get("owner_ID") != null){
                Map<String, Object> ownerMap = oDao.find(vehicleMap.get("owner_ID"));
                Owner owner = new Owner(ownerMap.get("id"));
                owner.set...
                //and so on..
                vehicle.setOwner(owner);
            }

            model.setYear(modelMap.get("year"));
            //and so on...

            //Final result
            System.out.println("Vehicle description: " + vehicle.getDescription());                

      }

      public static void demoSaveOrUpdate() throws Exception{
           Vehicle vehicle = new Vehicle("plate");
           vehicle.setModel(new Model());
           vehicle.getModel().setName("ModelA");
           //and so on...

           vehicle.setOwner(new Owner());
           vehicle.getOwner().setName("OwnerName");
           //and so on...

           vehicle.setColor("color");
           vehicle.setYear(2016);
           //and so on...

           //First we persist model and owner to garantee 
           //data integrity at DB
           mDao.saveOrUpdate(vehicle.getModel());               
           oDao.saveOrUpdate(vehicle.getOwner());
           //once Owner and Model are persisted, we can persist Vehicle
           vDao.saveOrUpdate(vehicle);               

      }

      private static shutdown() throws Exception {
           //set DataSource to null
           //set daos to null
      }

      private static void bootstrap() throws Exception {
           //Initialize DataSource
           //Initialize daos   
      }

  }    

Comments

  • existInDB: I don't like this attribute, but I have explained why I kept it.

  • Generic DAO vs Specific DAO: One DAO "to rule'em all" is doable using modern ORMs, but In this case we have to type SQL sentence for each entity. I have decided to keep every data access separated. ModelDAO and OnwerDAO are very similiar to VehicleDAO.

  • The awful Map: I guess somebody's eyes are bleeding out due to this point. But there's a reason. I could not return Vehicle (I already explained my reasons) and I could not return ResultSet because everything realted to DB (it includes JDBC API an its usage) is DAO's responsability. I don't like to see JDBC elements sacatered all around the code. It's a big deal better to move an awful Map than a ResultSet (IMO) because the implications behind a ResultSet.

  • Owner and Model are have many more attributes than just the name. That's why in my design I have a setModel(Model model); and a setOwner(Owner owner); . I'd get rid of setModelName and setOwnerName since I don't want to mutate just the names of those entities. – Tulains Córdova Aug 31 '16 at 13:04
  • So for every entity I will need three classes, the class itself, the builder and the DAO? – Tulains Córdova Aug 31 '16 at 13:07
  • In this case, yes. 1 DAO per entity, 1 builder per entity is up to you. Decide if there's complexity enough at the time of create a vehicle. Like a said, builder is dispensable. it's a plus. One builder per entity may lead into confusion. Someone may think that it's required, but its not. The point is "who does the new instances" and "who assembly them into vehicle". If you want to do it easier, remove builder and do has may "new" at MyBusinessService component. Then you will see what Im trying to point out with builders ;-) – Laiv Aug 31 '16 at 13:10
  • Just a comment. Feel free to edit my response and make it suitable for you and your purpose. Use it as your 'draft'. I hope it helps – Laiv Aug 31 '16 at 13:15
  • Just to clarify: all three classes, Vehicle, Builder and DAO are POJOS. A POJO is a class that is based solely on the Java Standard Edition API (as oppossed to using the Java Enterprise Edition with includes Javabeans, JSP, etc., or using external frameworks like HIbernate, etc.). So all three are POJOS. – Tulains Córdova Aug 31 '16 at 13:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.