Vita Rara: A Life Uncommon

Updating a List of Domain Entities in Struts 2


Categories: | | |

I've been working on Quadran a lot, and I'm creating pages for editing lists of entities. The CRUD example from the Struts 2 Showcase application does this in a very simple manner. It recreates all of the entities every time you update the list. That's fine for an example but unusable in real life.

In real life we have to present entities from our database and then put the values back into the same entities, and update our database. We can't just re-create them.

The Entity

So, we'll start with a simple domain object and assume that we're using something like JPA or Hibernate to persist it via a Data Access Object (DAO), and that it contains a property called "id".

public class MyEntity {
	private String id, value;

	public String getId () { return id; }
	public void setId (String in) { id = in; }

	public String getValue () { return value; }
	public void setValue (String in) { value = in; }
}

This is a simple JavaBean that contains two properties id and value.

Our Struts 2 Action Class and JSP

In our action we will have a List and a Map. The map is the key to mapping the values back to our beans. Our action looks like this:


public class EditMyEntitiesAction extends ActionSupport implements Preparable {
	
	// Service Beans
	MyEntityDao myEntityDao = new MyEntityDao (); // A data access object for managing MyEntity objects.

	// Model Beans
	List<MyEntity> myEntities;
	Map<String,MyEntity> myEntitiesMap;

	public void prepare () {
		// Get the List of MyEntity objects from the datastore.
		this.setMyEntities (myEntityDao.findAll () );

		// Create a Map using this.myEntities as the basis for it keyed on myEntity.id.
		Map<String,MyEntity> this.myEntitiesMap = new HashMap ();
		for (MyEntity myEntity : this.getMyEntities () ) {
			this.myEntitiesMap.put (myEntity.getId (), myEntity);
		}		
	}

	public String execute () throws Exception {
		// Iterate over the List of MyEntity objects and persist them using our DAO
		for (MyEntity myEntity : this.getMyEntities () ) {
			this.myEntityDao.updateDatabase (myEntity);
		}		
	}


	/* Accessors */

	public void setMyEntities (List<MyEntity> in) {
		this.myEntities = in;
	}

	public List<MyEntity> getMyEntities () {
		return this.myEntities;
	}

	public Map<String,MyEntity> getMyEntitiesMap () {
		return this.myEntitiesMap;
	}
}

That's all there is to the action. In the prepare we get the List of MyEntity objects and then put them into a map keyed on MyEntity.id. Pretty straight forward. The Map is the key to the whole process. We will use the OGNL EL notation to access that Map and fill the values back into it when a user submits the form.

Here's the pertinent section of the JSP:


<s:iterator value="myEntities">
    <s:textfield name="myEntitiesMap['%{id}'].value" value="%{value}" />
</s:iterator>

How OGNL Fills in our Domain Objects

In this we iterate over the myEntities List. On the textfield tag we set the name to "myEntitiesMap['%{id}'].value", this would result in something that looks like "myEntitiesMap['1'].value". When the user submits the form Struts2 will evaluate this name as so:

Expression Result
myEntitiesMap This will call getMyEntitiesMap() on our EditMyEntitiesAction.
['1'] This will call get("1") on the Map that was returned by getMyEntitiesMap(), which will return a MyEntity object.
.value Will call setValue() on the MyEntity object returned previously. Setting the value of the property to the value that was submitted by our user.

Basically myEntitiesMap['1'].value results in: EditMyEntitiesAction.getMyEntitiesMap().get ("1").setValue (<our user submitted value>).

It took me a while to figure out how to do this. I was struggling over how to get the values back into the "List" of entities in my action, then I hit on the idea of putting them into a Map keyed on the id of the entities, and voila, it all fits together.

What's Not Here

This is by no means an enterprise ready update mechanism. The primary things I've left out is validation. This can be added by implementing a validate() method in our Action. Best practice would be to delegate that validation to some other bean responsible for the management of MyEntity objects.

execute ()

Do I understand well?
1. prepare() // the data reading from db
2. execute() // update the database with original data (?) and display with the iterator
3. execute() // again with new data.

Step two seems bit unlucky. By the way, how does the struts.xml looks like? I does not work for me.

Thanks a lot!

Same for me

I looked also for the persons-example in the showcase of struts but it seems to be an experimental "zero-configuration" example.

Where can I find an example of how to save modified data of an iterator (List/HashMap) in a form?

http://struts.apache.org/2.0.11.2/docs/type-conversion.html is not complete and my arrays are empty :-(

Thanks a lot!!!

Looking for indexed alternative

As a new migration from Struts 1.x, this was a huge help for setting up a variable amount of mapped indexed properties. Thanks :)

Maybe another way?

I have not tried this, but reading this http://struts.apache.org/2.x/docs/type-conversion.html, I think the implication is that you should be able live without myEntitiesMap

You need a file called: EditMyEntitiesAction-conversion.properties with content

KeyProperty_myEntities=id
Element_myEntities=MyEntity
CreateIfNull_myEntities=false

Then you can use:

<s:iterator value="myEntities">
<s:textfield name="myEntities(%{id}).value" value="%{value}" />
</s:iterator>

Although whether it *actually* works is TBD

Tim Azzopardi

PS there is a full example of this at http://forums.opensymphony.com/thread.jspa?threadID=50841&tstart=0 towards the end.
(although it looks like the technique may have issues)

I was able to use the

I was able to use the example from:
http://jira.opensymphony.com/browse/WW-1413
(which is linked from the forum you gave: http://forums.opensymphony.com/thread.jspa?threadID=50841&tstart=0)

It works well with no map, as long as my id does not contain a comma, but is very slow. Not sure if other solutions are faster, I like it as it is fairly simple to follow, but it is slow.

I found a method that works, partially though

I am searching for a way to edit a list of entities without using the Map, too.
I tried the method in http://struts.apache.org/2.x/docs/type-conversion.html
Use some format like this in the jsp page:

<s:iterator value="myEntities" id="bean">
<stextfield name="myEntities(%{bean.id}).value" />
</s:iterator>

But it is not working, neither to display the data in jsp page or return value to the list after submit, I played with it and found that if I change it like this:

<s:iterator value="myEntities" id="bean">
<stextfield name="myEntities('%{bean.id}').value" />
</s:iterator>

It is able to display the right value in form, but still can't return value to the list in Action. The List is always reset to null.

Then I figure out a way as follows to make it work in both display and save back to the list in Action.

<s:iterator value="myEntities" status="status">
<s:textfield name="%{myEntities['+#status.index+'].value}" />
</s:iterator>

But I found out that it still create a new instance of myEntities, and save all the values in form to the new myEntities. If you don't display some filed in the form, the field will be null, and the old data is gone. I haven't try your 'Map' solution yet, does your method keep the old data in myEntities even you don't display them in the jsp page?

It might work

Looking at your example and reading the thread on the forum I think this might work. I'll have to give it a try. I'll admit I have done very little with the type conversion support in S2. I really haven't had time to dig into it.

Mark

Re: How to Send form data to DAO Layer.

You can have a separate bean for the form properties. Take a look at the ModelDriven documentation.

Another alternative is to create the bean as a property of your action in your prepare() method. Then in your form you can reference the properties of your bean in your tags with 'name="bean.propety"'.

Mark