Just listen to Alex

July 22, 2010

Hibernate data import, big sessions and slow flushing

Filed under: programming — Tags: , — bosmeeuw @ 8:03 pm

Have you ever done something like this with JPA/Hibernate?

for(Map<String,String> line : someCsvDataSource) {
	em.persist(createSomeDataObjectFromLine(line));
	em.flush();
}

If you’re creating more than a few hundred lines that way, you’ll soon see a drastic slowdown, with Hibernate eventually taking more than a second per line to do its work. The reason for this is that the Hibernate session becomes larger and larger as you persist more objects. This, in turn will cause the em().flush() to become really slow.

The fix for this is clearing your session every once in a while, like this:

int index = 0;

for(Map<String,String> line : someCsvDataSource) {
	em.persist(createSomeDataObjectFromLine(line));
	em.flush();
	
	index++;
	
	if(index % 100 == 0) {
		em.clear();
	}
}

Of course, this will “detach” any persistent objects you might have kept a reference to. Also note you absolutely need to do a manual flush before you clear your persistence context, else your data will be plain lost.

By the way: don’t use FlushMode.AUTO, it’s way too slow for practical use. You’ll be mad at yourself for having relied on it when you need to get rid of it to save your server’s performance.

Advertisements

January 16, 2010

Warning your web app users about navigating away while they have unsaved changes

Filed under: programming — Tags: , — bosmeeuw @ 12:09 pm

Have you ever spent a considerable amount of time filling in a <form> on a web page, only to accidently press “Back” or otherwise navigate away from the page, losing all the information you’ve just inputted in your form?

There’s an easy way to warn users about navigating away after they’ve inputted data on a web page. Just attach an “onchange” listener to every input element on the page, and set window.onbeforeunload() to nag the user if they try to navigate away. Clear the nagger the moment a form is submitted (and the user’s changes are sent to the server).

Here’s the code, using jQuery:

$(function() {
	$('input, select, textarea').change(function() {
		window.onbeforeunload = function() {
			return "Are you sure you want to leave this page? You have unsaved changes!";
		}
	})

	$('form').submit(function() {
		window.onbeforeunload = function() { };
	})
})

If you’re using Prototype, use this (a little more verbose):

document.observe('dom:loaded', function() {
	$$('input', 'select', 'textarea').each(function(input) {
		input.observe('change',function() {
			window.onbeforeunload = function() {
				return "Are you sure you want to leave this page? You have unsaved changes!";
			}
		})
	})

	$$('form').each(function(form) {
		form.observe('submit',function() {
			window.onbeforeunload = function() { };
		})
	})
)

July 1, 2009

Fake Java properties and how they improve JPA

Filed under: programming — Tags: , — bosmeeuw @ 5:52 pm

It doesn’t look like Java will be getting real property support in our lifetimes. This is too bad, because being able to refer to properties of an object in a type-safe way is really valuable when developing applications with a large domain. Take a gander a this JavaBean:

@Entity
public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Let’s say I’ve developed a 50.000 class, 10 million lines of code application using this JavaBean as my domain model. I’m of course using JPA. My business logic is riddled with heavily complex JPQL statements such as this one:

Person leaderOfTheFreeWorld = em.createQuery(
  "FROM Person p WHERE p.name = :name"
).setParameter("name", "Barack Obama").getSingleResult();

Now the users demand the impossible, and tell me they want to keep the first name and last name of people in seperate fields. After adding a “firstName” field to my JavaBean and maybe migrating the data, I need to change my heavily complex JPQL query to this:

Person leaderOfTheFreeWorld = em.createQuery(
   "FROM Person p WHERE p.name = :name AND p.firstName = :firstName"
).setParameter("name", "Obama").setParameter("firstName", "Barack").getSingleResult();

That’s fine really, but remember I have 50.000 classes with 10 million lines of code, and I need to make sure all queries that use the “name” field are updated. What do I do? A text search for “name”? I’m guessing a lot of hits are coming my way. How do I make sure I’ve covered all references to the name field? The answer is, of course: fake properties!

Let’s change the original JavaBean to this:

public class Person {
    
    private String name;
    public static final String NAME = "name";
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Notice the public static final reference to my field. Now look at this type safe way to refer to my model in JPQL:

em.createQuery(
   "FROM " + Person.class.getName()  + " p WHERE p." + Person.NAME + " = :name"
).setParameter("name", "Barack Obama").getSingleResult();

What does that get you?

  1. Autocompletion: Your IDE autocompletes the “Person.class” and “Person.NAME” parts of the query. No having to look at the model class to remember if the field was called “name” or “birthGivenPersonalDiscriminator”
  2. Navigability: you can jump to the declaration of the Person from within your query
  3. Refactor safety: you can rename the Person class, and the name field. Of course for the name field, you will need to modify your static reference as well. But it’s right there under your field declaration, so it’s very hard to forget this
  4. Traceability: you can ask your IDE to show you all instances where the “Person.NAME” property is accessed and make sure you have seen all usages if you need to do some refactoring or just need to know where something is used.
  5. Compile time safety: delete the name field on person (and the static reference), and your compiler will show you were this will cause problems
  6. You can use the property reference for other things, such as form field binding, i18n for field labels, ..

What are the downsides to this approach?

  1. The query looks uglier / harder to read. Of course, if you were using Hibernate Criteria or some kind of JPQL builder to avoid messy string concatenation, the property access would blend in much better and be easier on your fingers.
  2. You will have a static field for every field in your models, which is a little bit cluttering when reading the source code. If this bothers you, you can put the property references in a seperate class like PersonProps.
  3. You need to add the static fields for every field, which is a bit of extra typing and you’re lazy. I’m also lazy, so I made a little eclipse template to automate this somewhat:

eclipse-propref

You can add this to eclipse by going to Java >> Editor >> Templates, and adding a template named “propref” for context “Java” with this pattern:

${line_selection};

public static final String ${newName} = "${word_selection}"

Now when you’re in your model, select the field declaration, press control space, and select “propref”.

Yes, this method is complete overkill for a one-bean application, it’s just an example.

January 17, 2009

Easily repeating HTML form sections without (much) javascript

Filed under: programming, Uncategorized — Tags: , , — bosmeeuw @ 2:49 pm

If you’ve written more than a few data-driven web applications, you’ve surely encountered data models where a “master object” has multiple “child objects”.

In this example, we’ll use a book which can have many authors.
You could have the user enter the data for the book, and then add the authors one by one using multiple HTTP requests. Of course, this way the user needs to make many round trips to the server, which might slow him down considerably.

So you accomodate the user and write some javascript to enable him to add the X authors before posting anything to the server, yielding something like this:

Screenshot of expanding form (icons by famfamfam.com)

Screenshot of expanding form (icons by famfamfam.com)

There’s many approaches to programming this feature, the worst one being manually concatening HTML in a Javascript function and appending it to a container’s innerHTML attribute.

Below, you can find some code which allows you to create a repeating form like the one in the screenshot with just 3 lines of javascript. The idea is to create your repeating form element in plain HTML, and then attach some javascript behaviour to it which enables the user to repeat the form element. The Prototype Javascript library is required.

Here’s the code for the form:

<h1>Add a book</h1>

<form method="POST" action="/books/save_book">
   <table class="horizontal-layout">
      <tr>
         <td class="left">
            <h2>Data</h2>

            <table class="form">
               <tr>
                  <th>Title</th>
                  <td>
                     <input type="text" name="book&#91;title&#93;" />
                  </td>
               </tr>
               <tr>
                  <th>Description</th>
                  <td>
                     <textarea name="book&#91;description&#93;" class="medium"></textarea>
                  </td>
               </tr>
               <tr>
                  <th></th>
                  <td>
                     <input type="submit" class="submit" value="Save book &amp; authors" />
                  </td>
               </tr>
            </table>
         </td>
         <td>
            <h2>
               Authors
               <img src="/images/icons/page_white_add.png" id="add-author" class="link" />
            </h2>


            <div>
               <div
                  id="author-element"
                  class="expandable-form-entry"
                  style="line-height: 2em;"
               >
                  <input type="hidden" name="author&#91;#index&#93;&#91;id&#93;" />

                  <img src="/images/icons/page_white_delete.png" class="delete-entry link" />

                  <strong>Author name:</strong>
                  <br />
                  <input type="text" class="required" name="authors&#91;#index&#93;&#91;name&#93;" />

                  <br />

                  <strong>Author Remarks:</strong>
                  <br />
                  <textarea class="small" name="author&#91;#index&#93;&#91;remarks&#93;"></textarea>
               </div>
            </div>
         </td>
      </tr>
   </table>
</form>

<script type="text/javascript">
   var authorsExpander = new ExpandingFormElement({
      entryModel: 'author-element',
      addEntryLinkElement: 'add-author',
      deleteEntryElementClass: 'delete-entry',
      deletionConfirmText: "Are you sure you want to delete author?"
   })
</script>

Notice the input elements for the authors are named “author[#index][field_name]”. Each input name must contain the string “#index”, as the javascript code will replace this by the correct index of the element in the form. So if the user adds three authors, the third element will contain elements “author[2][name]” and “author[2][remarks]”, which you can easily save to your database server side.

The javascript at the bottom couples the expanding element behaviour and has these options:

  • entryModel: the id of the element you want to use as the model for the repeating element
  • addEntryLinkElement: the element the user will click to add a new entry
  • deleteEntryElementClass: the css class of the element the user can click to remove and added entry
  • deletionConfirmText: the text to confirm deletion of an entry (leave empty if you don’t want to ask for confirmation)

After you’ve saved the data to your database, the user might want to edit the data. This means you need to re-populate the data back to the form. This can be done using the addEntry() method of ExpandingFormElement, which can take a hash containing the data for the entry. The keys of the hash must correspond with the field name (the part after the [#index]). In a ruby on rails application, you could populate the data like this:

var authorsExpander = new ExpandingFormElement({
  entryModel: 'author-element',
  addEntryLinkElement: 'add-author',
  deleteEntryElementClass: 'delete-entry',
  deletionConfirmText: "Are you sure you want to delete author?"
})

<% @book.authors.each do |author| %>
	authorsExpander.addEntry(<%= author.attributes.to_json %>)
<% end %>

In PHP, you might use the json_encode() function on an associative array containing your author data.

Here’s the javascript code for the ExpandingFormElement class. The Prototype javascript library is required.

var ExpandingFormElement = Class.create({
    initialize: function(options) {
        this.options = options

        this.entryModel = $(options.entryModel)
        this.container = $(this.entryModel.parentNode)

        this.container.cleanWhitespace()

        if(this.container.childNodes.length > 1) {
            throw new Error("The container (parentNode) of the entryModel must contain only the entryModel, and no other nodes (put it in a <div> of its own). The container has " + this.container.childNodes.length + " elements after white space removal.")
        }

        this.entryModel.remove()

        $(options.addEntryLinkElement).observe('click',function() {
            this.addEntry()
        }.bind(this));
    } ,

    addEntry: function(values) {
        var copiedElement = this.entryModel.cloneNode(true)

        this.observeCopiedElement(copiedElement)

        var index = this.getNumberOfEntries()

        this.replaceInputNamesInElement(copiedElement, index)

        this.container.appendChild(copiedElement);

        if(values != null) {
            this.setEntryValues(copiedElement, values)
        }
    } ,

    setEntryValues: function(element, values) {
       $H(values).each(function(entry) {
          var input = this.getInputFromElementByName(element, entry.key)

          if(input) {
              input.value = entry.value;
          }
       }.bind(this));
    } ,

    getInputFromElementByName: function(element, name) {
        var matchedInput = null;

        var inputs = element.select('input','textarea','select')

        inputs.each(function(input) {
           if(input.name.indexOf("[" + name + "]") != -1) {
               matchedInput = input;

               return $break;
           }

           return null;
        });

        return matchedInput;
    } ,

    getNumberOfEntries: function() {
        return this.container.childNodes.length
    } ,

    observeCopiedElement: function(element) {
        var deleteEntryElement;

        if((deleteEntryElement = element.down('.' + this.options.deleteEntryElementClass))) {
            deleteEntryElement.observe('click',function() {
                if(this.options.deletionConfirmText) {
                    if(confirm(this.options.deletionConfirmText)) {
                        element.remove()
                    }
                }
                else {
                    element.remove()
                }
            }.bind(this))
        }
    } ,

    replaceInputNamesInElement: function(element, index) {
        $(element).select("input","textarea","select").each(function(input) {
            input.name = input.name.replace("#index",index)
        }.bind(this))
    }
});

Here’s the CSS I used for this example:

div.expandable-form-entry {
    position: relative;
    border-top: 1px dotted silver;
    padding-top: 1em;
    margin-bottom: 1em;
}

div.expandable-form-entry img.delete-entry {
    position: absolute;
    right: 0;
}

December 22, 2008

Finding out exactly which entities are causing a ConstraintViolationException (JPA)

Filed under: programming — Tags: , , — bosmeeuw @ 8:27 pm

Say you have an application using JPA. Your users can use and administrate lists of entities. They probably have the ability to permanently delete entities from the database.

If the entity the user wants to delete is used on another entity, the database should throw you a nice ConstraintViolationException, which JPA will (curiously) wrap in an EntityExistsException. You can catch this exception and display a nice informational message saying “You can’t delete this item because it’s already been used!”. This is better than just showing them the error the database spat out.

But what if you have a many entities, which have many relations between eachother? The user might want to find out just where their item is being used. And you might not feel like writing code to scan all your tables to find the item refering to the entity that’s being deleted. Below, I will explain how you can find out which entities are blocking the deletion of an arbitrary entity using some Reflection and ClassPath scanning.

Let’s say you have a service method like this:

public void deleteUser(long id) {
	User entity = em.find(User.class, id);
	em.remove(entity);
}

First, you’ll need to catch the Runtime Exception JPA will throw when encountering a constraint violation. Your service method will throw a YourApplicationException, which you will handle upstream and display as a nice error message.

public void deleteUser(long id) throws YourApplicationException {
	try {
		User entity = em.find(User.class, id);
		em.remove(entity);
	}
	catch (EntityExistsException e) {
		throw new YourApplicationException("You tried to delete an item which is in use.", e);
	}
}

To find out which entities are refering to the item we are deleting, we will inspect the database error message and extract the table name of the refering entity. Note the datbase message is specific to the DBMS you are using. I’m using PostgreSQL, if you are using a different DBMS you will probably need to tweak the pattern to match the message your DBMS is spitting out. Once we have the tablename, we will match it to an entity class by scanning the classpath. After that, we fill find out which field on the target class is of the same type as the entity being deleted (using reflection). We then make a query using JPA to fetch the referening entities. We put these entities in the exception, so it can be caught upstream and the entities can be displayed in a list.

Here’s the code for all of this:

public void delete(long id) throws YourConstraintViolationException {
	try {
		User entity = em.find(User.class, id);
		em.remove(entity);
	}
	catch (EntityExistsException e) {
		//need to rollback the transaction because we'll be doing a query later on
		em.getTransaction().rollback();
		em.getTransaction().begin();
		
		List<Object> linkedEntities = null;
		
		linkedEntities = findLinkedEntitiesFromContraintViolation(User.class, id, (ConstraintViolationException) e.getCause());
		
		throw new YourConstraintViolationException(linkedEntities, e);
	}
}

@SuppressWarnings("unchecked")
private List<Object> findLinkedEntitiesFromContraintViolation(Class<?> deletedEntityClass, long deletedEntityId, ConstraintViolationException e) {
	//unravel the exception so we have the SQLException
	BatchUpdateException batchUpdateException = (BatchUpdateException) e.getCause();
	SQLException sqlException = batchUpdateException.getNextException();
	
	List<Object> entities = new ArrayList<Object>();
	
	//match the database error message to find out the refering table name
	Matcher matcher = Pattern.compile("referenced from table \"(.*?)\"").matcher(sqlException.getMessage());
	if(matcher.find()) {
		String tableName = matcher.group(1);
		
		//we need to specify the base package all our entities reside under (possibly in sub-packages), and pass the ClassLoader of one of the entity classes to make the classpath scanning easier
		Class<?> entityClass = ClassUtils.findClassByCaseInsensitiveName(User.class.getClassLoader(), "your.entities.package",tableName);
		
		//check out which fields could possibly refer to the deleted class
		for(Field field : ClassUtils.getAllDeclaredFields(entityClass)) {
			if(field.getType().isAssignableFrom(deletedEntityClass)) {
				//fetch the refering entities using a JPA query and add them to the result
				String query = "FROM " + entityClass.getSimpleName() + " obj WHERE obj." + field.getName() + ".id = :deleted_id";
				List resultList = em.createQuery(query).setParameter("deleted_id",deletedEntityId).setMaxResults(10).getResultList();
				
				entities.addAll(resultList);
			}
		}
	}
	
	return entities;
}

The YourConstraintViolationException could look something like this:

public class YourConstraintViolationException extends Exception {
    
    private List<Object> linkedEntities;

    public YourConstraintViolationException(List<Object> linkedEntities, Throwable cause) {
        super(cause);
        
        this.linkedEntities = linkedEntities;
    }

    public List<Object> getLinkedEntities() {
        return linkedEntities;
    }

}

The code calling your service method might look this this:

try {
	userService.delete(someUserId);
}
catch(YourConstraintViolationException e) {
	out.write("<b>There was an error deleting the user. The user is in use on these items:</b>");
	
	out.write("<ul>");
	for(Object linkedEntity : e.getLinkedEntities()) {
		out.write("<li>" + linkedEntity.toString() + "</li>");
	}
	out.write("</ul>");
}

The actual classpath scanning is going on inside ClassUtils. This class will do its work both when your entities are normal .class files on disk, and when they’re packaged inside a jar. Here’s the code for ClassUtils.java:

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ClassUtils {

    public static Class<?> findClassByCaseInsensitiveName(ClassLoader classLoader, String basePackage, String className) throws ClassNotFoundException, IOException, URISyntaxException {
	    URL packageUrl = classLoader.getResource(basePackage.replace(".","/"));
	    
	    Matcher matcher = Pattern.compile("(file:/.*?.jar)!(.*)").matcher(packageUrl.getFile());
        
	    if(matcher.find()) {
	        String jarFileUrl = matcher.group(1);
	        
	        return findClassByCaseInsentiveNameInJar(basePackage, new File(new URI(jarFileUrl)), className);
	    }
	    else {
    	    File packageFolder = new File(packageUrl.getFile());
    	    
    	    return findClassByCaseInsentiveNameInDirectory(basePackage, packageFolder, className);
	    }
	}

    private static Class<?> findClassByCaseInsentiveNameInJar(String basePackage, File jarFilePath, String className) throws IOException, ClassNotFoundException {
        JarFile jarFile = new JarFile(jarFilePath);
        
        String packagePath = basePackage.replace(".","/");
        
        String patternString = Pattern.quote(packagePath) + ".*/" + className + "\\.class";
        
        Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
        
        ArrayList<JarEntry> entries = Collections.list(jarFile.entries());
        
        for(JarEntry entry : entries) {
            Matcher matcher = pattern.matcher(entry.getName());
            
            if(matcher.matches()) {
                String fullClassName = entry.getName().replace("/",".").replace(".class","");
                
                return Class.forName(fullClassName);
            }
        }        
        
        return null;
    }

    private static Class<?> findClassByCaseInsentiveNameInDirectory(String packageName, File packageFolder, String className) throws ClassNotFoundException {
        for(File file : packageFolder.listFiles()) {



            if(file.getName().toLowerCase().equals(className + ".class")) {
                String fullClassName = packageName + "." + file.getName().replace(".class","");
                return Class.forName(fullClassName);
            }
            
            if(file.isDirectory()) {
                String subPackageName = packageName + "." + file.getName();
                Class<?> foundClass = findClassByCaseInsentiveNameInDirectory(subPackageName, file, className);
                
                if(foundClass != null) {
                    return foundClass;
                }
            }
        }
        
        return null;
    }

    public static List<Field> getAllDeclaredFields(Class<?> className) {
        List<Field> fields = new ArrayList<Field>();
        
        Class<?> superClass = className;
        
        do {
            for(Field field : superClass.getDeclaredFields()) {
                fields.add(field);
            }
            
            superClass = superClass.getSuperclass();
        }
        while(superClass != null);
        
        return fields;
    }
}

August 23, 2008

Adding hideable columns to plain HTML Tables using Prototype

Filed under: programming — Tags: — bosmeeuw @ 1:17 pm

I present to you a bit of Javascript code which enables users to change which columns should be visible or hidden in plain HTML tables. It is built using Prototype 1.6.

Click here to see a simple demo. You can grab the javascript code and default style from the demo page.

You enable the behaviour like this:

new AdjustableTableFields($('your-table-id'));

Note that the demo page enables the behaviour by table class name (in a ‘dom:loaded’ event handler).

There’s a few options for you to enjoy:

Add an attribute named “cookie-name” to your table element to have the browser remember the selected columns:

<table class="adjustable-table" cookie-name="demo">

Add an attribute named “unhidable” to the right th element to ensure the user cannot hide a certain column:

<th unhidable="true">ID</th>

Add an attribute named “display-name” to the right th element to use a different name on the checkbox than the TH content:

<th display-name="Fav. Dict.">Favorite Dictator</th>

That’s all there is to it, have fun!

July 12, 2008

Batch downloading cover art with PHP and Google Image Search

Filed under: programming — Tags: — bosmeeuw @ 2:58 pm

Do you happen to have come by a large collection of MP3’s, ordered in folders by album? Are they named something like “Artist Name – Album Name”? Would you like to download the cover for each album so you can browse them like this in explorer?

Screenshot

Yes? Then put the wad of PHP code below into a file (named get_covers.php or whatever you like) in the root folder of your MP3’s, and execute it using PHP. This script will do a google image search for every folder which doesn’t have a folder.jpg and download the resulting image into folder.jpg, providing a nice thumbnail for Windows Explorer. For 99% of my ~500 albums, this worked perfectly.

This script downloads arbitrary image results from Google to your hard drive, directly from the resulting websites, without any attempt at filtering out harmful results. The results may include corrupt or virus infected images and pictures of naked ladies, which can seriously harm your computer.

<?php

$folders = glob('*');

$ctx = stream_context_create(array(
    'http' => array(
        'timeout' => 10
        )
    )
); 

foreach($folders as $folder) {
	if(!is_dir($folder)) {
		echo "Skipping {$folder}\n";
		continue;
	}

	if(is_file($folder."/folder.jpg")) {
		echo "Already have cover for {$folder}\n";
		continue;
	}

	$album = str_replace('_',' ',$folder);

	echo "Checking {$album}.. ";

	$googleUrl = "http://images.google.com/images?um=1&hl=en&safe=off&imgsz=medium&q=".urlencode($album);

	$contents = file_get_contents($googleUrl,0,$ctx);
	
	if(preg_match_all('/imgurl\\\\x3d(.*?)\\\\x26/i',$contents,$matches)) {
		foreach($matches[1] as $image) {
			if($imageContents = file_get_contents($image,0,$ctx)) {
				file_put_contents("{$folder}/folder.jpg",$imageContents);
				echo "Found image {$image}\n";
				break;
			}
		}
	}
	else {
		echo "nothing found!\n";
	}
}
?>

December 18, 2007

Object-oriented approach to ActiveRecord#find

Filed under: programming — Tags: , — bosmeeuw @ 7:44 pm

ActiveRecord is great! Only it would be nice if there would be an Object Oriented way to build the options given to the ActiveRecord#find method. Think HQL queries versus Criteria objects in Hibernate. If you’re asking yourself why someone would go and complicate a beautiful thing like the syntax to the find options, consider the following example. It’s a standard scenario where one is listing products based on a few search criteria entered by the user.

def list_products
	conditions = []

	if params[:filter_category] != ""
		conditions << "category_id = " + params&#91;:filter_category&#93;
	end

	if params&#91;:only_in_stock&#93; == "1"
		conditions << "stock > 0"
	end

	conditions_sql = conditions.join(' AND ')

	@products = Product.find(:conditions => conditions_sql)
end

This doesn’t look too bad, but there is a problem. The user can fiddle with the request and put SQL injection code in the filter_category parameter. This is why ActiveRecord provides parameter binding. Rewriting our code to support parameters would look something like this:

def list_products
	conditions_sql = []
	conditions_parameters = []

	if params[:filter_category] != ""
		conditions << "category_id = ?"
		conditions_parameters << params&#91;:filter_category&#93;
	end

	if params&#91;:only_in_stock&#93; == "1"
		conditions << "stock > 0"
	end

	conditions = conditions.join(' AND ') + conditions_parameters

	@products = Product.find(:conditions => conditions_parameters)
end

This looks a bit worse, especially when we have a whole bunch of parameters. Introducing the RecordFinder class, a nicer way to write this would be:

def list_products
	finder = RecordFinder.new

	if params[:filter_category] != ""
		finder.add "category_id = ?", params[:filter_category]
	end

	if params[:only_in_stock] == "1"
		finder.add "stock > 0"
	end

	@products = Product.find(:conditions => finder.to_conditions)
end

This can also work with multiple parameters:

finder = RecordFinder.new
finder.add "email = ? AND password = ?", params[:email], params[:password]
@user = User.find(:first, finder.to_conditions)

We can add shorthand methods for common conditions:

finder.add_wildcard "product.name", params[:name_wildcard]
finder.add_ref :category_id, params[:category_id]

We can change the combination of conditions from AND to OR:

finder = RecordFinder.new('OR')
finder.add "product.new = 1"
finder.add "product.hot = 1"
@hot_or_new_product = Product.find(:all, finder.to_conditions)

We can use sub-finders, for instance this SQL:

WHERE
	product.stock > 0
	AND (
		product.new = 1
		OR
		product.hot = 1
	)

Could be written as:

finder = RecordFinder.new
finder.add "product.stock > 0"

sub_finder = RecordFinder.new('OR')
sub_finder.add "product.new = 1"
sub_finder.add "product.hot = 1"

finder.add_finder sub_finder

And perhaps most importantly, we can extend the RecordFinder to put shared business logic filters in named methods, in stead of repeating the conditions in different controllers in the application:

class ProductFinder  < RecordFinder
	def in_stock
		add "product.stock > 0"
	end

	def new_or_hot
		sub_finder = RecordFinder.new('OR')
		sub_finder.add "product.new = 1"
		sub_finder.add "product.hot = 1"
	end

	... other business filters ...
end

Our ProductController can use it like this:

def list_products
	finder = ProductFinder.new
	finder.in_stock
	finder.new_or_hot

	@products = Product.find(:all, :conditions => finder.to_conditions)
end

Suppose we must change the business logic not to show products for which all the stock is reserved:

class ProductFinder < RecordFinder
	def in_stock
		add "product.stock > product.reserved_stock"
	end

	....
end

We change it once in the in_stock method, and all controllers using this method get the correct filter.The code to the RecordFinder class is below. Any ideas on how to improve it, make the syntax more concise or add useful features is of course welcome.

class RecordFinder

attr_reader :parameters
attr :order_by, true

def initialize (bool_mode = ‘AND’)
@bool_mode = bool_mode
@sqls = []
@parameters = []
@includes = []
@order_by = “”
end

def add (sql, *params)
@sqls << sql @parameters += params end def add_ref(field, int) add "#{field.to_s} = ?", int end def add_wildcard(field, value) add "#{field.to_s} LIKE ?", "%#{value}%" end def add_finder(finder) @sqls << finder.sql_string @parameters += finder.parameters end def sql_string @sqls.collect{|sql| "(#{sql})"}.join(" #{@bool_mode} ") end def to_conditions if @sqls.length > 0
[ sql_string ] + @parameters
else
nil
end
end

def to_find_options
{
:include => @includes,
:conditions => to_conditions,
:order => @order_by
}
end

def include(path)
unless @includes.include? path
@includes << path end end end[/sourcecode]

Blog at WordPress.com.