Showing posts with label sql. Show all posts
Showing posts with label sql. Show all posts

Friday, October 28, 2011

MyBatis and Oracle Database

While I was using MyBatis with the Oracle Database, I ran into two problems:

  1. Generating primary keys when inserting database rows
  2. Mapping Java boolean type to a database type
The next sections describe how I solved these problems.

Inserting new rows

Unlike other databases like MySQL, the Oracle database does not provide the functionality to generate primary keys automatically when inserting rows. Normally, I would create a sequence together with a trigger in the Oracle database. The sequence is a thread-safe number generator, and the trigger is custom code that is called on certain type of events. In this case an "insert" event, which will update the row with the primary key.

With MyBatis, we still need the sequence. The trigger functionality is provided by MyBatis. Let's get started by creating an example class in Java. I left the "getters" and "setters" out, but we still need them in the actual code ofcouse.

public class Client {
    private Integer id;
    private String name;

    // Setters and Getters for the attributes here.
}

Next, we have to prepare the database by create a table that corresponds with the "Client" class, and a sequence.

CREATE TABLE client (
    id INTEGER primary key,
    name VARCHAR2(100)
);

CREATE SEQUENCE client_seq
    START WITH 1
    INCREMENT BY 1
    NOCACHE
    NOCYCLE;

MyBatis uses "mapper" interfaces (defined and provided by us) to map the class to actual SQL-statements. Additional configuration can be provided by either XML, or annotations. When the configuration is simple, I prefer using annotations. Complex configurations are often only possible by using XML. The following mapper interface provides mapping for inserting the "Client" class. The interface is only used by MyBatis, we don't have to provide an actual implementation of it.

public interface ClientMapper {
  @Insert("INSERT INTO client (id, name) VALUES (#{id}, #{name})")
  @SelectKey(
            keyProperty = "id",
            before = true,
            resultType = Integer.class,
            statement = { "SELECT client_seq.nextval AS id FROM dual" })
  public Integer insertClient(Client client);
}

The "@SelectKey" annotation tells MyBatis to get the primary key value from the sequence, and use it as primary key when executing the insert statement.

Mapping boolean types

The final example illustrates how we deal with boolean types when using MyBatis. We extend the "Client" class with a boolean attribute "admin".

public class Client {
    private Integer id;
    private String name;
    private boolean admin;

    // Setters and Getters for the attributes here.
}

Add a "VARCHAR(1)" column that acts like a boolean attribute to the corresponding "Client" database table. The column value "Y" corresponds to boolean value true, and "N" to boolean false.

CREATE TABLE client (
    id INTEGER primary key,
    name VARCHAR2(100),
    admin varchar(1)
);

MyBatis doesn't know how the Java boolean is mapped to a database type, as there's no primitive database boolean type. We have to provide MyBatis with a converter, which implements the "TypeHandler" interface. This converter will then be used to convert a Java boolean to a database type, and vice versa.

package com.javaeenotes;

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

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

public class BooleanTypeHandler implements TypeHandler {

    private static final String YES = "Y";
    private static final String NO = "N";


    @Override
    public Boolean getResult(ResultSet resultSet, String name)
            throws SQLException {

        return valueOf(resultSet.getString(name));
    }


    @Override
    public Boolean getResult(CallableStatement statement, int position)
            throws SQLException {

        return valueOf(statement.getString(position));
    }


    @Override
    public void setParameter(
            PreparedStatement statement,
            int position,
            Boolean value,
            JdbcType jdbcType) throws SQLException {

        statement.setString(position, value.booleanValue() ? YES : NO);
    }


    private Boolean valueOf(String value) throws SQLException {

        if (YES.equalsIgnoreCase(value)) {
            return new Boolean(true);
        } else if (NO.equalsIgnoreCase(value)) {
            return new Boolean(false);
        } else {
            throw new SQLException("Unexpected value "
                    + value
                    + " found where "
                    + YES
                    + " or "
                    + NO
                    + " was expected.");
        }
    }
}

Now, configure MyBatis to use the type handler for booleans in the mapper interface where needed.


    @Insert("INSERT INTO client (id, name, admin) VALUES (#{id}, #{name}, #{admin,typeHandler=com.javaeenotes.BooleanTypeHandler})")
    @SelectKey(
            keyProperty = "id",
            before = true,
            resultType = Integer.class,
            statement = { "SELECT client_seq.nextval AS id FROM dual" })
    public Integer insertClient(Client client);

    @Select("SELECT * FROM client WHERE id = #{clientId}")
    @Results(
            value = {
                @Result(
                    property = "id",
                    column = "id"),

                @Result(
                    property = "name",
                    column = "name"),

                @Result(
                    property = "admin",
                    column = "admin",
                    typeHandler = BooleanTypeHandler.class)
             })
    public Client getClient(int clientId);

    @Update("UPDATE client SET name = #{name}, admin = #{admin,typeHandler=com.javaeenotes.BooleanTypeHandler} WHERE id = #{id}")
    public void updateClient(Client client);

Friday, January 21, 2011

Converting a database CLOB into a String

I've seen code that converts database CLOBs to String by using CharacterStreams and BufferedReaders to read CLOBs in a loop until the CLOB is fully read. A simpler way is using the getSubString() method of the Clob object. While knowing the length or size of the CLOB, you can read the whole CLOB into a String like this:

DataSource ds = ...
Connection c = null;
PreparedStatement s = null;
ResultSet r = null;

try {
c = ds.getConnection();
s = c.prepareStatement("SELECT clobColumn FROM someTable");
r = s.executeQuery();

while (r.next()) {
Clob clob = r.getClob(1);

if (clob != null) {

if ((int) clob.length() > 0) {
String s = clob.getSubString(1, (int) clob.length());
// Do something with string.
}
}
}
} catch (SQLException e) {
// Handle exception.
} finally {
// Close Connection, PreparedStatement, and ResultSet.
}

Beware if the data in the CLOB is really large (larger than Integer.MAX_VALUE). In that case, you'll still need a streaming method.

Saturday, November 13, 2010

Saving large XMLTypes in Oracle databases

If you try to save a large XMLType that is over 4k characters long, you'll get this error:

ORA-01461:
can bind a LONG value only for insert into a LONG column

Oracle database has troubles binding large strings. One solution is to use a temporary clob to store the XML data, and bind that to the SQL-statement.

Connection con = ...
String xml = "<test>test</test>";
oracle.sql.CLOB clob = null;

clob = CLOB.createTemporary(con, true,
CLOB.DURATION_SESSION);
clob.open(CLOB.MODE_READWRITE);
clob.setString(1, xml);
clob.close();
clob.freeTemporary();

PrepareStatement s = con.prepareStatement(
"INSERT INTO table (xml) VALUES(XMLType(?))"
);

s.setObject(1, clob);

Using this method, you can store large XML-documents as XMLType in the Oracle database. But there is still a limitation with the XMLType. The XMLType cannot store any text-nodes and attributes values larger than 64k. Oracle suggests to use a XMLType CLOB-storage column to store really large XML-documents. But then, we cannot use native XML-parsing functions, which defeats the purpose of the XMLType column...

In newer versions of Oracle XML DB, this limit has been lifted.

References:

Tuesday, August 17, 2010

Getting Oracle SQL result set pages

When you want to display the result of a query that returns a large number of rows, it's common to divide the complete result in pages, and display them one page at a time. To offer the user a way of navigating through the pages, there are clickable page numbers or "next"/"previous" buttons at the bottom of the page. This technique is called result set paging.

To implement this efficiently, the paging should also be built into the query. Because it's not efficient when we use a query to return the complete result from the database, and discard rows we don't want to display afterwards.

Let's say that you want to page the following query:

SELECT col1, col2
FROM example_table
WHERE col2 = 'some_condition'
ORDER BY col1 DESC

Before we can divide the result set, we need to number the rows of the result set. The pseudo column "rownum", provided by Oracle databases, numbers the rows of the result. The first row of the result is always 1.

Using this knowledge, we can retrieve the "page" we want using the WHERE clause with the corresponding start row and end row numbers. In the example below, we want to fetch rows from number 201 to 300.

SELECT *
FROM (SELECT r.*, ROWNUM AS row_number
FROM (SELECT col1, col2
FROM example_table
WHERE col2 = 'some_condition'
ORDER BY col1 DESC) r)
WHERE row_number >= 201 AND row_number <= 300;

The reason why we wrap the original query in another SELECT query, is that the database assigns ROWNUMs before the ordering, which makes our ordering ineffective for paging. To make sure the ROWNUMs are ordered, we retrieve the ROWNUM column in the outer SELECT query. The database will then assign the ROWNUM in the already correctly ordered result set.

This query can be optimized for newer Oracle databases by removing the top outer SELECT clause, while leaving the WHERE clause intact.