1 /*
2 * Hibernate, Relational Persistence for Idiomatic Java
3 *
4 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
5 * indicated by the @author tags or express copyright attribution
6 * statements applied by the authors. All third-party contributions are
7 * distributed under license by Red Hat Middleware LLC.
8 *
9 * This copyrighted material is made available to anyone wishing to use, modify,
10 * copy, or redistribute it subject to the terms and conditions of the GNU
11 * Lesser General Public License, as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16 * for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this distribution; if not, write to:
20 * Free Software Foundation, Inc.
21 * 51 Franklin Street, Fifth Floor
22 * Boston, MA 02110-1301 USA
23 *
24 */
25 package org.hibernate.persister.collection;
26
27 import java.io.Serializable;
28 import java.sql.PreparedStatement;
29 import java.sql.SQLException;
30 import java.util.Iterator;
31
32 import org.hibernate.HibernateException;
33 import org.hibernate.MappingException;
34 import org.hibernate.jdbc.Expectation;
35 import org.hibernate.jdbc.Expectations;
36 import org.hibernate.cache.CacheException;
37 import org.hibernate.cache.access.CollectionRegionAccessStrategy;
38 import org.hibernate.cfg.Configuration;
39 import org.hibernate.collection.PersistentCollection;
40 import org.hibernate.engine.SessionFactoryImplementor;
41 import org.hibernate.engine.SessionImplementor;
42 import org.hibernate.engine.SubselectFetch;
43 import org.hibernate.exception.JDBCExceptionHelper;
44 import org.hibernate.loader.collection.BatchingCollectionInitializer;
45 import org.hibernate.loader.collection.CollectionInitializer;
46 import org.hibernate.loader.collection.SubselectOneToManyLoader;
47 import org.hibernate.loader.entity.CollectionElementLoader;
48 import org.hibernate.mapping.Collection;
49 import org.hibernate.persister.entity.Joinable;
50 import org.hibernate.persister.entity.OuterJoinLoadable;
51 import org.hibernate.pretty.MessageHelper;
52 import org.hibernate.sql.Update;
53 import org.hibernate.util.ArrayHelper;
54
55 /**
56 * Collection persister for one-to-many associations.
57 *
58 * @author Gavin King
59 */
60 public class OneToManyPersister extends AbstractCollectionPersister {
61
62 private final boolean cascadeDeleteEnabled;
63 private final boolean keyIsNullable;
64 private final boolean keyIsUpdateable;
65
66 protected boolean isRowDeleteEnabled() {
67 return keyIsUpdateable && keyIsNullable;
68 }
69
70 protected boolean isRowInsertEnabled() {
71 return keyIsUpdateable;
72 }
73
74 public boolean isCascadeDeleteEnabled() {
75 return cascadeDeleteEnabled;
76 }
77
78 public OneToManyPersister(
79 Collection collection,
80 CollectionRegionAccessStrategy cacheAccessStrategy,
81 Configuration cfg,
82 SessionFactoryImplementor factory) throws MappingException, CacheException {
83 super( collection, cacheAccessStrategy, cfg, factory );
84 cascadeDeleteEnabled = collection.getKey().isCascadeDeleteEnabled() &&
85 factory.getDialect().supportsCascadeDelete();
86 keyIsNullable = collection.getKey().isNullable();
87 keyIsUpdateable = collection.getKey().isUpdateable();
88 }
89
90 /**
91 * Generate the SQL UPDATE that updates all the foreign keys to null
92 */
93 protected String generateDeleteString() {
94
95 Update update = new Update( getDialect() )
96 .setTableName( qualifiedTableName )
97 .addColumns( keyColumnNames, "null" )
98 .setPrimaryKeyColumnNames( keyColumnNames );
99
100 if ( hasIndex && !indexContainsFormula ) update.addColumns( indexColumnNames, "null" );
101
102 if ( hasWhere ) update.setWhere( sqlWhereString );
103
104 if ( getFactory().getSettings().isCommentsEnabled() ) {
105 update.setComment( "delete one-to-many " + getRole() );
106 }
107
108 return update.toStatementString();
109 }
110
111 /**
112 * Generate the SQL UPDATE that updates a foreign key to a value
113 */
114 protected String generateInsertRowString() {
115
116 Update update = new Update( getDialect() )
117 .setTableName( qualifiedTableName )
118 .addColumns( keyColumnNames );
119
120 if ( hasIndex && !indexContainsFormula ) update.addColumns( indexColumnNames );
121
122 //identifier collections not supported for 1-to-many
123 if ( getFactory().getSettings().isCommentsEnabled() ) {
124 update.setComment( "create one-to-many row " + getRole() );
125 }
126
127 return update.setPrimaryKeyColumnNames( elementColumnNames )
128 .toStatementString();
129 }
130
131 /**
132 * Not needed for one-to-many association
133 */
134 protected String generateUpdateRowString() {
135 return null;
136 }
137
138 /**
139 * Generate the SQL UPDATE that updates a particular row's foreign
140 * key to null
141 */
142 protected String generateDeleteRowString() {
143
144 Update update = new Update( getDialect() )
145 .setTableName( qualifiedTableName )
146 .addColumns( keyColumnNames, "null" );
147
148 if ( hasIndex && !indexContainsFormula ) update.addColumns( indexColumnNames, "null" );
149
150 if ( getFactory().getSettings().isCommentsEnabled() ) {
151 update.setComment( "delete one-to-many row " + getRole() );
152 }
153
154 //use a combination of foreign key columns and pk columns, since
155 //the ordering of removal and addition is not guaranteed when
156 //a child moves from one parent to another
157 String[] rowSelectColumnNames = ArrayHelper.join(keyColumnNames, elementColumnNames);
158 return update.setPrimaryKeyColumnNames( rowSelectColumnNames )
159 .toStatementString();
160 }
161
162 public boolean consumesEntityAlias() {
163 return true;
164 }
165 public boolean consumesCollectionAlias() {
166 return true;
167 }
168
169 public boolean isOneToMany() {
170 return true;
171 }
172
173 public boolean isManyToMany() {
174 return false;
175 }
176
177 protected int doUpdateRows(Serializable id, PersistentCollection collection, SessionImplementor session)
178 throws HibernateException {
179
180 // we finish all the "removes" first to take care of possible unique
181 // constraints and so that we can take better advantage of batching
182
183 try {
184 int count = 0;
185 if ( isRowDeleteEnabled() ) {
186 boolean useBatch = true;
187 PreparedStatement st = null;
188 // update removed rows fks to null
189 try {
190 int i = 0;
191
192 Iterator entries = collection.entries( this );
193 int offset = 1;
194 Expectation expectation = Expectations.NONE;
195 while ( entries.hasNext() ) {
196
197 Object entry = entries.next();
198 if ( collection.needsUpdating( entry, i, elementType ) ) { // will still be issued when it used to be null
199 if ( st == null ) {
200 String sql = getSQLDeleteRowString();
201 if ( isDeleteCallable() ) {
202 expectation = Expectations.appropriateExpectation( getDeleteCheckStyle() );
203 useBatch = expectation.canBeBatched();
204 st = useBatch
205 ? session.getBatcher().prepareBatchCallableStatement( sql )
206 : session.getBatcher().prepareCallableStatement( sql );
207 offset += expectation.prepare( st );
208 }
209 else {
210 st = session.getBatcher().prepareBatchStatement( getSQLDeleteRowString() );
211 }
212 }
213 int loc = writeKey( st, id, offset, session );
214 writeElementToWhere( st, collection.getSnapshotElement(entry, i), loc, session );
215 if ( useBatch ) {
216 session.getBatcher().addToBatch( expectation );
217 }
218 else {
219 expectation.verifyOutcome( st.executeUpdate(), st, -1 );
220 }
221 count++;
222 }
223 i++;
224 }
225 }
226 catch ( SQLException sqle ) {
227 if ( useBatch ) {
228 session.getBatcher().abortBatch( sqle );
229 }
230 throw sqle;
231 }
232 finally {
233 if ( !useBatch ) {
234 session.getBatcher().closeStatement( st );
235 }
236 }
237 }
238
239 if ( isRowInsertEnabled() ) {
240 Expectation expectation = Expectations.appropriateExpectation( getInsertCheckStyle() );
241 boolean callable = isInsertCallable();
242 boolean useBatch = expectation.canBeBatched();
243 String sql = getSQLInsertRowString();
244 PreparedStatement st = null;
245 // now update all changed or added rows fks
246 try {
247 int i = 0;
248 Iterator entries = collection.entries( this );
249 while ( entries.hasNext() ) {
250 Object entry = entries.next();
251 int offset = 1;
252 if ( collection.needsUpdating( entry, i, elementType ) ) {
253 if ( useBatch ) {
254 if ( st == null ) {
255 if ( callable ) {
256 st = session.getBatcher().prepareBatchCallableStatement( sql );
257 }
258 else {
259 st = session.getBatcher().prepareBatchStatement( sql );
260 }
261 }
262 }
263 else {
264 if ( callable ) {
265 st = session.getBatcher().prepareCallableStatement( sql );
266 }
267 else {
268 st = session.getBatcher().prepareStatement( sql );
269 }
270 }
271
272 offset += expectation.prepare( st );
273
274 int loc = writeKey( st, id, offset, session );
275 if ( hasIndex && !indexContainsFormula ) {
276 loc = writeIndexToWhere( st, collection.getIndex( entry, i, this ), loc, session );
277 }
278
279 writeElementToWhere( st, collection.getElement( entry ), loc, session );
280
281 if ( useBatch ) {
282 session.getBatcher().addToBatch( expectation );
283 }
284 else {
285 expectation.verifyOutcome( st.executeUpdate(), st, -1 );
286 }
287 count++;
288 }
289 i++;
290 }
291 }
292 catch ( SQLException sqle ) {
293 if ( useBatch ) {
294 session.getBatcher().abortBatch( sqle );
295 }
296 throw sqle;
297 }
298 finally {
299 if ( !useBatch ) {
300 session.getBatcher().closeStatement( st );
301 }
302 }
303 }
304
305 return count;
306 }
307 catch ( SQLException sqle ) {
308 throw JDBCExceptionHelper.convert(
309 getSQLExceptionConverter(),
310 sqle,
311 "could not update collection rows: " +
312 MessageHelper.collectionInfoString( this, id, getFactory() ),
313 getSQLInsertRowString()
314 );
315 }
316 }
317
318 public String selectFragment(
319 Joinable rhs,
320 String rhsAlias,
321 String lhsAlias,
322 String entitySuffix,
323 String collectionSuffix,
324 boolean includeCollectionColumns) {
325 StringBuffer buf = new StringBuffer();
326 if ( includeCollectionColumns ) {
327 // buf.append( selectFragment( lhsAlias, "" ) )//ignore suffix for collection columns!
328 buf.append( selectFragment( lhsAlias, collectionSuffix ) )
329 .append( ", " );
330 }
331 OuterJoinLoadable ojl = ( OuterJoinLoadable ) getElementPersister();
332 return buf.append( ojl.selectFragment( lhsAlias, entitySuffix ) )//use suffix for the entity columns
333 .toString();
334 }
335
336 /**
337 * Create the <tt>OneToManyLoader</tt>
338 *
339 * @see org.hibernate.loader.collection.OneToManyLoader
340 */
341 protected CollectionInitializer createCollectionInitializer(java.util.Map enabledFilters) throws MappingException {
342 return BatchingCollectionInitializer.createBatchingOneToManyInitializer( this, batchSize, getFactory(), enabledFilters );
343 }
344
345 public String fromJoinFragment(String alias,
346 boolean innerJoin,
347 boolean includeSubclasses) {
348 return ( ( Joinable ) getElementPersister() ).fromJoinFragment( alias, innerJoin, includeSubclasses );
349 }
350
351 public String whereJoinFragment(String alias,
352 boolean innerJoin,
353 boolean includeSubclasses) {
354 return ( ( Joinable ) getElementPersister() ).whereJoinFragment( alias, innerJoin, includeSubclasses );
355 }
356
357 public String getTableName() {
358 return ( ( Joinable ) getElementPersister() ).getTableName();
359 }
360
361 public String filterFragment(String alias) throws MappingException {
362 String result = super.filterFragment( alias );
363 if ( getElementPersister() instanceof Joinable ) {
364 result += ( ( Joinable ) getElementPersister() ).oneToManyFilterFragment( alias );
365 }
366 return result;
367
368 }
369
370 protected CollectionInitializer createSubselectInitializer(SubselectFetch subselect, SessionImplementor session) {
371 return new SubselectOneToManyLoader(
372 this,
373 subselect.toSubselectString( getCollectionType().getLHSPropertyName() ),
374 subselect.getResult(),
375 subselect.getQueryParameters(),
376 subselect.getNamedParameterLocMap(),
377 session.getFactory(),
378 session.getEnabledFilters()
379 );
380 }
381
382 public Object getElementByIndex(Serializable key, Object index, SessionImplementor session, Object owner) {
383 return new CollectionElementLoader( this, getFactory(), session.getEnabledFilters() )
384 .loadElement( session, key, incrementIndexByBase(index) );
385 }
386
387 }