EJB Entity Beans: Persistence Before Hibernate Made It Bearable
EJB Entity Beans: Persistence Before Hibernate Made It Bearable
EJB entity beans were Sun's answer to object-relational mapping in J2EE. The idea was sound: define a Java class that represents a database row, and let the container handle loading, saving, and caching.
The implementation was one of the most painful APIs in enterprise Java history.
What Entity Beans Were Supposed to Do
An entity bean represented a persistent business object. The container managed its lifecycle: loading from the database when accessed, flushing changes at transaction commit, pooling instances for performance.
Two types: BMP (bean-managed persistence) where you wrote the SQL yourself, and CMP (container-managed persistence) where the container generated SQL from a descriptor.
A BMP Entity Bean
public class DeviceBean implements EntityBean {
public String ip;
public String name;
public String status;
private EntityContext ctx;
private transient Connection conn;
// Called when loading from database
public void ejbLoad() {
try {
PreparedStatement ps = conn.prepareStatement(
"SELECT name, status FROM devices WHERE ip = ?");
ps.setString(1, (String) ctx.getPrimaryKey());
ResultSet rs = ps.executeQuery();
if (rs.next()) {
this.name = rs.getString("name");
this.status = rs.getString("status");
}
} catch (SQLException e) { throw new EJBException(e); }
}
// Called at transaction commit
public void ejbStore() {
try {
PreparedStatement ps = conn.prepareStatement(
"UPDATE devices SET name=?, status=? WHERE ip=?");
ps.setString(1, name);
ps.setString(2, status);
ps.setString(3, (String) ctx.getPrimaryKey());
ps.executeUpdate();
} catch (SQLException e) { throw new EJBException(e); }
}
// Create a new row
public String ejbCreate(String ip, String name) throws CreateException {
try {
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO devices (ip, name, status) VALUES (?,?,?)");
ps.setString(1, ip); ps.setString(2, name); ps.setString(3, "UNKNOWN");
ps.executeUpdate();
return ip;
} catch (SQLException e) { throw new CreateException(e.getMessage()); }
}
public void ejbPostCreate(String ip, String name) {}
public void ejbRemove() { /* DELETE */ }
public void ejbActivate() { /* get connection from pool */ }
public void ejbPassivate() { /* release connection */ }
public void setEntityContext(EntityContext ctx) { this.ctx = ctx; }
public void unsetEntityContext() { this.ctx = null; }
}
That is 60 lines of boilerplate before you have written a single line of business logic. Every entity bean needed a home interface, a remote interface, a local interface, a local home interface, and two XML descriptors. A table with ten columns was two hundred lines of Java and XML.
Why CMP Was Worse
Container-managed persistence was supposed to be the easier option. You declared fields in the descriptor and the container generated the SQL.
In practice:
- The generated SQL was often inefficient and you had no way to optimise it.
- Relationships between entities (one-to-many, many-to-many) required abstract accessor methods and descriptor entries that had to match precisely.
- Finder methods (queries) were defined in EJBQL — a simplified query language — but complex queries required vendor-specific extensions.
- Debugging container-generated SQL required reading JBoss or WebLogic internals.
Every team that built something real with CMP entity beans eventually reached the same conclusion: you spent more time fighting the framework than building the application.
What We Did Instead
By 2001 the pattern in serious teams was:
- Use stateless session beans for business logic (they were reasonable).
- Skip entity beans entirely.
- Implement persistence with direct JDBC, a DAO pattern, and careful transaction management.
public class DeviceDao {
private final DataSource ds;
public DeviceDao(DataSource ds) { this.ds = ds; }
public Device findByIp(String ip) throws SQLException {
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(
"SELECT ip, name, status FROM devices WHERE ip = ?")) {
ps.setString(1, ip);
ResultSet rs = ps.executeQuery();
return rs.next() ? mapRow(rs) : null;
}
}
private Device mapRow(ResultSet rs) throws SQLException {
return new Device(rs.getString("ip"), rs.getString("name"), rs.getString("status"));
}
}
This was more code than an ORM would require, but it was code you understood and could debug. The SQL was under your control.
The Path Forward
Hibernate 1.0 was released in 2001. It did what CMP entity beans promised — transparent persistence, relationship management, lazy loading — but without the container dependency and with SQL you could inspect and tune. It was adopted rapidly precisely because it solved the entity bean problem without the entity bean pain.
EJB 3.0 (2006) effectively copied the Hibernate model: POJO entities, annotations instead of XML, JPA as the standard API. The XML descriptor nightmare was gone. It arrived five years after the community had already moved on.