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

Monday, November 3, 2008

Non-ANSI Oracle ADD_MONTHS Function

Motivation


To use an Oracle function for adding months with the following characteristics:
  • When the resulting month has as many or fewer days than the initial month, and when the initial day of the month is greater than the number of days in the resulting month, then the resulting day should fall on the last day of the resulting month (this is how add_months already works).
  • When the resulting month has more days than the initial month, and when the initial day is the last day of the initial month, then the resulting day of the resulting month should be the same as the initial day (this is not how add_months works).


The Problem


As I had been using the Oracle add_months function for date calculations, I started noticing an unexpected and unintuitive result when a new date is calculated on the last day of certain months. For example,


SELECT add_months(to_date('2009-02-28','YYYY-MM-DD'), 1) FROM dual;

ADD_MONTH
---------
31-MAR-09



I would have expected the resulting date to be 28-MAR-09.

Of course, in the case where the initial month contains more days than the resulting month, I get the results that I expect.


SELECT add_months(to_date('2009-01-31','YYYY-MM-DD'), 1) FROM dual;

ADD_MONTH
---------
28-FEB-09



This feature appears to be part of the ANSI definition for interval math, but this result does not seem particularly intuitive to me.

Unfortunately, the numtoyminterval function only gives the result we expect when go from a month with fewer days to a month with more days, but when going from a month with more days to fewer, it raises an exception when calculating from the last day of the month (or from any day of the month that is greater than the number of days in the resulting month).


SELECT to_date('2009-02-28','YYYY-MM-DD') + numtoyminterval(1, 'month') FROM dual;

TO_DATE('
---------
28-MAR-09

SELECT to_date('2009-01-31','YYYY-MM-DD') + numtoyminterval(1, 'month') FROM dual
*
ERROR at line 1:
ORA-01839: date not valid for month specified



The Function


The function itself is fairly straightforward using a combination of both add_months and numtoyminterval. When we are going from a month with more days to fewer days, then add_months yields the expected result. If we are going from a date with fewer days in the months to a date with more days in the month, then using numtoyminterval is safe because there will be no overflow.


CREATE OR REPLACE FUNCTION non_ansi_add_months
( vDate DATE,
vMonths INTEGER )
RETURN DATE AS
newDate DATE;
BEGIN
newDate := add_months(vDate, vMonths);
IF to_char(vDate, 'DD') < to_char(newDate, 'DD') THEN
newDate := vDate + numtoyminterval(vMonths, 'month');
END IF;
RETURN newDate;
END non_ansi_add_months;



The Result


This function now yields the results we expect.


SELECT non_ansi_add_months(to_date('2009-02-28','YYYY-MM-DD'), 1) FROM dual;

NON_ANSI_
---------
28-MAR-09

SELECT non_ansi_add_months(to_date('2009-01-31','YYYY-MM-DD'), 1) FROM dual;

NON_ANSI_
---------
28-FEB-09



The function even works as expected when adding negative months (calculating month intervals in the past). For example,


SELECT non_ansi_add_months(to_date('2009-02-28','YYYY-MM-DD'), -1) FROM dual;

NON_ANSI_
---------
28-JAN-09

SELECT non_ansi_add_months(to_date('2009-03-30','YYYY-MM-DD'), -1) FROM dual;

NON_ANSI_
---------
28-FEB-09

Sunday, November 2, 2008

Hibernate Updates and Oracle Stored Procedures

This is a continuation of a previous post and builds on the project setup contained therein.

Motivation: It is sometimes desirable to use Oracle stored procedures for standard CUD (create, update, delete) operations; for example, when re-factoring a database to remove trigger calls, one possible solution would be to allow modifications to tables only through procedures. The operations performed by triggers can then be moved into these procedures.

Add an Update Method


We will first use Hibernate's built-in support for updating our Author domain Object. We can add a new method to the AuthorDAO to examine its behavior.


....
public void update(final Author author) {
getHibernateTemplate().update(author);
}
....



Add a Test Case


We generally should not test the frameworks that we use, but here we will be moving outside of the framework. We want to make sure that using our custom update stored procedure will not break the existing behavior that Hibernate provides to us, so we will add a new verification of update behavior to the AuthorDAOTest.


....
@Test
public void testUpdate() throws Exception {
final Author author = getAuthorDAO().findByLastNameUsingHQL("Thoreau").get(0);
author.setLastName("Miller");
getAuthorDAO().update(author);
assertEquals(1, getAuthorDAO().findByLastNameUsingHQL("Miller").size());
}
....



Write a Stored Procedure


Now we will write our custom update stored procedure. To figure out the procedure's argument signature, we can look at the Hibernate console output when we run the test case above. We should note the order of the arguments, which is alphabetic for the updated fields with the primary key in the last position.
  • Hibernate: update MY_ORCL.AUTHOR set FIRST_NAME=?, LAST_NAME=? where ID=?

This order must be maintained in our stored procedure call. We can also use this statement as the basis for the update contained in the procedure body.
We can create this stored procedure through SQLPlus.


CREATE OR REPLACE PROCEDURE update_author
( vFirstName IN author.first_name%type,
vLastName IN author.last_name%type,
vId IN author.id%type ) AS
BEGIN
UPDATE author SET first_name=vFirstName, last_name=vLastName where id=vId;
END update_author;



Call the Stored Procedure From @SQLUpdate


Now that we have a procedure in our schema, we need a way to call it from our DAO. Hibernate provides annotations specific to these CUD operations
  • @org.hibernate.annotations.SQLUpdate
  • @org.hibernate.annotations.SQLInsert
  • @org.hibernate.annotations.SQLDelete
  • @org.hibernate.annotations.SQLDeleteAll

Here, we can add the custom update to our Author domain Object, alongside the named queries from the last tutorial.


....
@Entity
@org.hibernate.annotations.NamedNativeQuery(name = "findByLastName", query = "call findByLastName(?, :vLastName)", callable = true, resultClass = Author.class)
@javax.persistence.NamedNativeQuery(name = "findByFirstName", query = "{ ? = call findByFirstName(:vFirstName) }", resultClass = Author.class, hints = { @javax.persistence.QueryHint(name = "org.hibernate.callable", value = "true") })
@org.hibernate.annotations.SQLUpdate(sql = Author.UPDATE_AUTHOR)
@Table(name = "AUTHOR", schema = "MY_ORCL")
public class Author implements java.io.Serializable {

public static final String UPDATE_AUTHOR = "call update_author(:vFirstName, :vLastName, :vId)";
....


If we run our test case again, we will see that the stored procedure is now called to perform the update.
  • Hibernate: call update_author(:vFirstName, :vLastName, :vId)


Write a Hibernate Interceptor


Suppose we would like to pass an extra parameter to the stored procedure, say a counter for the number of times this procedure has been called from a particular instance of an application. We can modify our argument list with an extra parameter, here adding the counter at the end.


CREATE OR REPLACE PROCEDURE update_author
( vFirstName IN author.first_name%type,
vLastName IN author.last_name%type,
vId IN author.id%type,
vCounter INTEGER ) AS
BEGIN
UPDATE author SET first_name=vFirstName, last_name=vLastName where id=vId;
END update_author;



We will also need to modify the call syntax where it is declared in the domain Object to accept this new parameter.


....
public class Author implements java.io.Serializable {

public static final String UPDATE_AUTHOR = "call update_author(:vFirstName, :vLastName, :vId, :vCounter)";
....



Finally, we can write an Interceptor that will set the counter value in this parameter. The Interceptor will increment a static field each time the update_author procedure is called. This counter is passed to the stored procedure.


package spring.hibernate.oracle.stored.procedures.domain;

import java.util.concurrent.atomic.AtomicLong;
import org.hibernate.EmptyInterceptor;

public class UpdateAuthorInterceptor extends EmptyInterceptor {

private static final long serialVersionUID = 2908952460484632623L;
private static final AtomicLong counter = new AtomicLong();

@Override
public String onPrepareStatement(final String sql) {
if (sql.equals(Author.UPDATE_AUTHOR)) {
return sql.replaceFirst(":vCounter", String.valueOf(counter.getAndIncrement()));
}
return super.onPrepareStatement(sql);
}
}



Now we will configure the Interceptor in the applicationContext.xml for use by the LocalSessionFactoryBean.


....
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
....
<property name="entityInterceptor" ref="updateAuthorInterceptor" />
</bean>
<bean id="updateAuthorInterceptor"
class="spring.hibernate.oracle.stored.procedures.domain.UpdateAuthorInterceptor" />
....



If we run the test case again, we will see console output indicating that Hibernate is calling the update procedure with this counter value.
  • Hibernate: call update_author(:vFirstName, :vLastName, :vId, 0)


Other CUD Operations


With these same steps, we can customize Create (@SQLInsert) and Delete (@SQLDelete and @SQLDeleteAll) operations to use stored procedures.

Friday, October 31, 2008

Spring, Hibernate and Oracle Stored Procedures

Motivation: While there are a few resources available online for calling stored procedures from Hibernate, it took me a while to stumble across one that mostly captures what I need. The intention of this blog entry is to put a similar example into my own words, to extend it slightly and hopefully to help anyone not experienced with Hibernate and Oracle to integrate Stored Procedures and Functions into an application quickly.

Setup Oracle 10g


For this example, we will be using Oracle 10g. We can initialize our schema user with SQLPlus with the following commands:
  • sqlplus connect as sysdba
  • create user my_orcl identified by my_orcl;
  • grant create session to my_orcl;
  • grant resource to my_orcl;
  • grant create table to my_orcl;


Setup a Project For Spring and Hibernate


We will download spring-framework-2.5.5-with-dependencies.zip, hibernate-distribution-3.3.1.GA-dist.zip and hibernate-annotations-3.4.0.GA.zip. We can create a standard project layout of src, test and lib folders with the following jars on the classpath:

  • spring-framework-2.5.5/dist/spring.jar
  • spring-framework-2.5.5/dist/modules/spring-test.jar
  • spring-framework-2.5.5/lib/jakarta-commons/commons-logging.jar
  • spring-framework-2.5.5/lib/jakarta-commons/commons-dbcp.jar
  • spring-framework-2.5.5/lib/jakarta-commons/commons-pool.jar
  • spring-framework-2.5.5/lib/jakarta-commons/commons-collections.jar
  • spring-framework-2.5.5/lib/dom4j/dom4j-1.6.1.jar
  • spring-framework-2.5.5/lib/log4j/log4j-1.2.15.jar
  • spring-framework-2.5.5/lib/slf4j/slf4j-api-1.5.0.jar
  • spring-framework-2.5.5/lib/slf4j/slf4j-log4j12-1.5.0.jar
  • spring-framework-2.5.5/lib/j2ee/*.jar
  • hibernate-annotations-3.4.0.GA/hibernate-annotations.jar
  • hibernate-annotations-3.4.0.GA/lib/hibernate-commons-annotations.jar
  • hibernate-distribution-3.3.1.GA/hibernate3.jar
  • hibernate-distribution-3.3.1.GA/lib/required/javassist-3.4.GA.jar
  • hibernate-distribution-3.3.1.GA/lib/required/slf4j-api-1.5.2.jar


Because we will be using Oracle Stored Procedures, we will also need a database driver such as

  • oracle/product/10.2.0/db_1/jdbc/lib/ojdbc14.jar


Create Domain Objects


We can setup our domain using annotated Java. For these examples, we need one simple domain Object.


package spring.hibernate.oracle.stored.procedures.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "AUTHOR", schema = "MY_ORCL")
public class Author implements java.io.Serializable {

private static final long serialVersionUID = 8676058601610931698L;
private int id;
private String firstName;
private String lastName;

@Id
@Column(name = "ID", nullable = false)
public int getId() {
return this.id;
}

public void setId(final int id) {
this.id = id;
}

@Column(name = "FIRST_NAME", nullable = false, length = 50)
public String getFirstName() {
return this.firstName;
}

public void setFirstName(final String firstName) {
this.firstName = firstName;
}

@Column(name = "LAST_NAME", nullable = false, length = 50)
public String getLastName() {
return this.lastName;
}

public void setLastName(final String lastName) {
this.lastName = lastName;
}
}



Create a DAO


Now that we have a domain Object, we can create a DAO for a simple operation, such as looking up Authors by last name. Fortunately, Spring provides a convenient base class for DAO operations.


package spring.hibernate.oracle.stored.procedures.dao;

import java.util.List;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import spring.hibernate.oracle.stored.procedures.domain.Author;

public class AuthorDAO extends HibernateDaoSupport {

@SuppressWarnings("unchecked")
public List<Author> findByLastNameUsingHQL(final String lastName) {
return getHibernateTemplate().find("from Author author where author.lastName = ?", lastName);
}
}



The Spring Application Context Configuration


The Spring applicationContext.xml can reside directly at the root of our src classpath, and it will contain information for configuring Spring to manage our Hibernate sessions, transactions and datasources, as well as our DAO.


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
default-autowire="constructor">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl" />
<property name="username" value="my_orcl" />
<property name="password" value="my_orcl" />
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:/hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.query.factory_class">org.hibernate.hql.classic.ClassicQueryTranslatorFactory</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean name="authorDAO"
class="spring.hibernate.oracle.stored.procedures.dao.AuthorDAO">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
</beans>



The Hibernate Configuration


The hibernate.cfg.xml can also reside directly in our src/ folder. The primary purpose of this file is to let Hibernate know about our domain class.


<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<mapping class="spring.hibernate.oracle.stored.procedures.domain.Author" />
</session-factory>
</hibernate-configuration>



Testing the Setup


Now that our project is setup, we can write a simple test to very that all of our configuration files can be properly loaded and that all of our connections work. We will begin by setting up some simple test data in the database, and then we can call our DAO method to find Authors by their last names. Again, we can take advantage of a convenient Spring base class for our test cases.


package spring.hibernate.oracle.stored.procedures.dao;

import org.junit.Test;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;

public class AuthorDAOTest extends AbstractTransactionalDataSourceSpringContextTests {

private AuthorDAO authorDAO;

@Override
protected void onSetUp() throws Exception {
super.onSetUp();
createAuthor(1, "Jules", "Verne");
createAuthor(2, "Charles", "Dickens");
createAuthor(3, "Emily", "Dickinson");
createAuthor(4, "Henry", "James");
createAuthor(5, "William", "James");
createAuthor(6, "Henry", "Thoreau");
}

@Test
public void testFindByLastNameUsingHQL() throws Exception {
assertEquals(2, getAuthorDAO().findByLastNameUsingHQL("James").size());
assertEquals(1, getAuthorDAO().findByLastNameUsingHQL("Verne").size());
assertEquals(1, getAuthorDAO().findByLastNameUsingHQL("Dickinson").size());
assertEquals(1, getAuthorDAO().findByLastNameUsingHQL("Dickens").size());
assertEquals(0, getAuthorDAO().findByLastNameUsingHQL("Whitman").size());
}

@Override
protected String[] getConfigLocations() {
return new String[] { "applicationContext.xml" };
}

public AuthorDAO getAuthorDAO() {
return authorDAO;
}

public void setAuthorDAO(final AuthorDAO authorDAO) {
this.authorDAO = authorDAO;
}

private void createAuthor(final int id, final String firstName, final String lastName) {
jdbcTemplate.execute(String.format("insert into author (id, first_name, last_name) values (%d, '%s', '%s')", id,
firstName, lastName));
}
}



Write a Stored Procedure


Because we have specified <prop key="hibernate.hbm2ddl.auto">create</prop> in our Spring configuration for Hibernate, the AUTHOR table now exists in the database. We can write a simple stored procedure to query this table.


CREATE OR REPLACE PROCEDURE findByLastName
( res OUT SYS_REFCURSOR,
vLastName IN author.last_name%type ) AS
BEGIN
OPEN res FOR
SELECT * FROM author WHERE last_name = vLastName;
END findByLastName;



Call The Stored Procedure From Hibernate


Now that we have a PL/SQL Stored Procedure, we will need a way to reference it from Hibernate. We can annotate the domain Object with such a named query.


....
@Entity
@org.hibernate.annotations.NamedNativeQuery(name = "findByLastName", query = "call findByLastName(?, :vLastName)", callable = true, resultClass = Author.class)
@Table(name = "AUTHOR", schema = "MY_ORCL")
public class Author implements java.io.Serializable {
....



Now we can add a method to our AuthorDAO for calling this Stored Procedure.


....
@SuppressWarnings("unchecked")
public List<Author> findByLastNameUsingStoredProcedure(final String lastName) {
return (List<Author>) getHibernateTemplate().execute(new HibernateCallback() {
public Object doInHibernate(final Session session) throws HibernateException, SQLException {
return session.getNamedQuery("findByLastName") //
.setParameter("vLastName", lastName) //
.list();
}
});
}
....



Finally, we will add a test case for calling the new DAO method.


....
@Test
public void testFindByLastNameUsingStoredProcedure() throws Exception {
assertEquals(2, getAuthorDAO().findByLastNameUsingStoredProcedure("James").size());
assertEquals(1, getAuthorDAO().findByLastNameUsingStoredProcedure("Verne").size());
assertEquals(1, getAuthorDAO().findByLastNameUsingStoredProcedure("Dickinson").size());
assertEquals(1, getAuthorDAO().findByLastNameUsingStoredProcedure("Dickens").size());
assertEquals(0, getAuthorDAO().findByLastNameUsingStoredProcedure("Whitman").size());
}
....



Write a PL/SQL Function


We can similarly call an Oracle Function. Here, we will use a function that locates Authors by their first names.


CREATE OR REPLACE FUNCTION findByFirstName
( vFirstName IN author.first_name%type )
RETURN SYS_REFCURSOR AS
res SYS_REFCURSOR;
BEGIN
OPEN res FOR
SELECT * FROM author WHERE first_name = vFirstName;
RETURN res;
END findByFirstName;



Call The Function From Hibernate


We can reference this function using an org.hibernate.annotations.NamedNativeQuery, but we can also make the scenario a little more interesting by instead using the javax.persistence.NamedNativeQuery annotation in conjunction with the javax.persistence.QueryHint annotation. By using this annotation, note how we have two named queries declared on a single domain Object. Also note the braces that are necessary for the query syntax of the function but are not necessary for the stored procedure call.


....
@Entity
@org.hibernate.annotations.NamedNativeQuery(name = "findByLastName", query = "call findByLastName(?, :vLastName)", callable = true, resultClass = Author.class)
@javax.persistence.NamedNativeQuery(name = "findByFirstName", query = "{ ? = call findByFirstName(:vFirstName) }", resultClass = Author.class, hints = { @javax.persistence.QueryHint(name = "org.hibernate.callable", value = "true") })
@Table(name = "AUTHOR", schema = "MY_ORCL")
public class Author implements java.io.Serializable {
....



Again, we will access this PL/SQL function through our DAO.


....
@SuppressWarnings("unchecked")
public List<Author> findByFirstNameUsingFunction(final String firstName) {
return (List<Author>) getHibernateTemplate().execute(new HibernateCallback() {
public Object doInHibernate(final Session session) throws HibernateException, SQLException {
return session.getNamedQuery("findByFirstName") //
.setParameter("vFirstName", firstName) //
.list();
}
});
}
....



And finally, we can add another simple test for this function call.


....
@Test
public void testFindByFirstNameUsingFunction() throws Exception {
assertEquals(0, getAuthorDAO().findByFirstNameUsingFunction("James").size());
assertEquals(2, getAuthorDAO().findByFirstNameUsingFunction("Henry").size());
}
....



Hopefully, this step-by-step process gives a good starting point for creating more complex stored procedure and function calls using Spring, Hibernate and Oracle.

Next: Hibernate updates and stored procedures ->

Thursday, July 24, 2008

Drop All Tables in a Single Query

Recently, I needed to find a single, simple PL/SQL command to drop all the tables from an Oracle database without explicitly naming each table and without dropping the entire schema. Application development has just started, and I need to easily rename tables and other database objects, so the simplest solution at this early stage is often just to wipe and recreate the entire schema.
One possible solution would be to login as SYS and drop the schema itself, but I need to configure a single login for any database access, i.e., the user whose database objects will be deleted.

I did not find my specific solution online, but I owe thanks to alternative approaches in some other posts.

First, make sure that you connect to Oracle as the correct user, i.e., the one whose tables will be dropped.

This command will drop all the user's tables:
BEGIN 
  FOR i IN (SELECT table_name FROM user_tables) 
    LOOP 
      EXECUTE IMMEDIATE('DROP TABLE ' || user || '.' || i.table_name || ' CASCADE CONSTRAINTS'); 
    END LOOP
END;

Similar commands can be used to drop all triggers, sequences, etc.  For example,
BEGIN 
  FOR i IN (SELECT trigger_name FROM user_triggers) 
    LOOP 
      EXECUTE IMMEDIATE('DROP TRIGGER ' || user || '.' || i.trigger_name); 
    END LOOP
END;

               and

BEGIN 
  FOR i IN (SELECT sequence_name FROM user_sequences) 
    LOOP 
      EXECUTE IMMEDIATE('DROP SEQUENCE ' || user || '.' || i.sequence_name); 
    END LOOP
END;

NB: if you drop all tables before triggers, your triggers will be renamed with special characters and the command might not work; make sure to drop triggers before tables.