PageRenderTime 56ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/SQLAlchemy-0.7.8/lib/sqlalchemy/orm/persistence.py

#
Python | 779 lines | 513 code | 123 blank | 143 comment | 132 complexity | ac1ca039297e3bb4657a030ad1cd31ce MD5 | raw file
  1. # orm/persistence.py
  2. # Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
  3. #
  4. # This module is part of SQLAlchemy and is released under
  5. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  6. """private module containing functions used to emit INSERT, UPDATE
  7. and DELETE statements on behalf of a :class:`.Mapper` and its descending
  8. mappers.
  9. The functions here are called only by the unit of work functions
  10. in unitofwork.py.
  11. """
  12. import operator
  13. from itertools import groupby
  14. from sqlalchemy import sql, util, exc as sa_exc
  15. from sqlalchemy.orm import attributes, sync, \
  16. exc as orm_exc
  17. from sqlalchemy.orm.util import _state_mapper, state_str
  18. def save_obj(base_mapper, states, uowtransaction, single=False):
  19. """Issue ``INSERT`` and/or ``UPDATE`` statements for a list
  20. of objects.
  21. This is called within the context of a UOWTransaction during a
  22. flush operation, given a list of states to be flushed. The
  23. base mapper in an inheritance hierarchy handles the inserts/
  24. updates for all descendant mappers.
  25. """
  26. # if batch=false, call _save_obj separately for each object
  27. if not single and not base_mapper.batch:
  28. for state in _sort_states(states):
  29. save_obj(base_mapper, [state], uowtransaction, single=True)
  30. return
  31. states_to_insert, states_to_update = _organize_states_for_save(
  32. base_mapper,
  33. states,
  34. uowtransaction)
  35. cached_connections = _cached_connection_dict(base_mapper)
  36. for table, mapper in base_mapper._sorted_tables.iteritems():
  37. insert = _collect_insert_commands(base_mapper, uowtransaction,
  38. table, states_to_insert)
  39. update = _collect_update_commands(base_mapper, uowtransaction,
  40. table, states_to_update)
  41. if update:
  42. _emit_update_statements(base_mapper, uowtransaction,
  43. cached_connections,
  44. mapper, table, update)
  45. if insert:
  46. _emit_insert_statements(base_mapper, uowtransaction,
  47. cached_connections,
  48. table, insert)
  49. _finalize_insert_update_commands(base_mapper, uowtransaction,
  50. states_to_insert, states_to_update)
  51. def post_update(base_mapper, states, uowtransaction, post_update_cols):
  52. """Issue UPDATE statements on behalf of a relationship() which
  53. specifies post_update.
  54. """
  55. cached_connections = _cached_connection_dict(base_mapper)
  56. states_to_update = _organize_states_for_post_update(
  57. base_mapper,
  58. states, uowtransaction)
  59. for table, mapper in base_mapper._sorted_tables.iteritems():
  60. update = _collect_post_update_commands(base_mapper, uowtransaction,
  61. table, states_to_update,
  62. post_update_cols)
  63. if update:
  64. _emit_post_update_statements(base_mapper, uowtransaction,
  65. cached_connections,
  66. mapper, table, update)
  67. def delete_obj(base_mapper, states, uowtransaction):
  68. """Issue ``DELETE`` statements for a list of objects.
  69. This is called within the context of a UOWTransaction during a
  70. flush operation.
  71. """
  72. cached_connections = _cached_connection_dict(base_mapper)
  73. states_to_delete = _organize_states_for_delete(
  74. base_mapper,
  75. states,
  76. uowtransaction)
  77. table_to_mapper = base_mapper._sorted_tables
  78. for table in reversed(table_to_mapper.keys()):
  79. delete = _collect_delete_commands(base_mapper, uowtransaction,
  80. table, states_to_delete)
  81. mapper = table_to_mapper[table]
  82. _emit_delete_statements(base_mapper, uowtransaction,
  83. cached_connections, mapper, table, delete)
  84. for state, state_dict, mapper, has_identity, connection \
  85. in states_to_delete:
  86. mapper.dispatch.after_delete(mapper, connection, state)
  87. def _organize_states_for_save(base_mapper, states, uowtransaction):
  88. """Make an initial pass across a set of states for INSERT or
  89. UPDATE.
  90. This includes splitting out into distinct lists for
  91. each, calling before_insert/before_update, obtaining
  92. key information for each state including its dictionary,
  93. mapper, the connection to use for the execution per state,
  94. and the identity flag.
  95. """
  96. states_to_insert = []
  97. states_to_update = []
  98. for state, dict_, mapper, connection in _connections_for_states(
  99. base_mapper, uowtransaction,
  100. states):
  101. has_identity = bool(state.key)
  102. instance_key = state.key or mapper._identity_key_from_state(state)
  103. row_switch = None
  104. # call before_XXX extensions
  105. if not has_identity:
  106. mapper.dispatch.before_insert(mapper, connection, state)
  107. else:
  108. mapper.dispatch.before_update(mapper, connection, state)
  109. # detect if we have a "pending" instance (i.e. has
  110. # no instance_key attached to it), and another instance
  111. # with the same identity key already exists as persistent.
  112. # convert to an UPDATE if so.
  113. if not has_identity and \
  114. instance_key in uowtransaction.session.identity_map:
  115. instance = \
  116. uowtransaction.session.identity_map[instance_key]
  117. existing = attributes.instance_state(instance)
  118. if not uowtransaction.is_deleted(existing):
  119. raise orm_exc.FlushError(
  120. "New instance %s with identity key %s conflicts "
  121. "with persistent instance %s" %
  122. (state_str(state), instance_key,
  123. state_str(existing)))
  124. base_mapper._log_debug(
  125. "detected row switch for identity %s. "
  126. "will update %s, remove %s from "
  127. "transaction", instance_key,
  128. state_str(state), state_str(existing))
  129. # remove the "delete" flag from the existing element
  130. uowtransaction.remove_state_actions(existing)
  131. row_switch = existing
  132. if not has_identity and not row_switch:
  133. states_to_insert.append(
  134. (state, dict_, mapper, connection,
  135. has_identity, instance_key, row_switch)
  136. )
  137. else:
  138. states_to_update.append(
  139. (state, dict_, mapper, connection,
  140. has_identity, instance_key, row_switch)
  141. )
  142. return states_to_insert, states_to_update
  143. def _organize_states_for_post_update(base_mapper, states,
  144. uowtransaction):
  145. """Make an initial pass across a set of states for UPDATE
  146. corresponding to post_update.
  147. This includes obtaining key information for each state
  148. including its dictionary, mapper, the connection to use for
  149. the execution per state.
  150. """
  151. return list(_connections_for_states(base_mapper, uowtransaction,
  152. states))
  153. def _organize_states_for_delete(base_mapper, states, uowtransaction):
  154. """Make an initial pass across a set of states for DELETE.
  155. This includes calling out before_delete and obtaining
  156. key information for each state including its dictionary,
  157. mapper, the connection to use for the execution per state.
  158. """
  159. states_to_delete = []
  160. for state, dict_, mapper, connection in _connections_for_states(
  161. base_mapper, uowtransaction,
  162. states):
  163. mapper.dispatch.before_delete(mapper, connection, state)
  164. states_to_delete.append((state, dict_, mapper,
  165. bool(state.key), connection))
  166. return states_to_delete
  167. def _collect_insert_commands(base_mapper, uowtransaction, table,
  168. states_to_insert):
  169. """Identify sets of values to use in INSERT statements for a
  170. list of states.
  171. """
  172. insert = []
  173. for state, state_dict, mapper, connection, has_identity, \
  174. instance_key, row_switch in states_to_insert:
  175. if table not in mapper._pks_by_table:
  176. continue
  177. pks = mapper._pks_by_table[table]
  178. params = {}
  179. value_params = {}
  180. has_all_pks = True
  181. for col in mapper._cols_by_table[table]:
  182. if col is mapper.version_id_col:
  183. params[col.key] = mapper.version_id_generator(None)
  184. else:
  185. # pull straight from the dict for
  186. # pending objects
  187. prop = mapper._columntoproperty[col]
  188. value = state_dict.get(prop.key, None)
  189. if value is None:
  190. if col in pks:
  191. has_all_pks = False
  192. elif col.default is None and \
  193. col.server_default is None:
  194. params[col.key] = value
  195. elif isinstance(value, sql.ClauseElement):
  196. value_params[col] = value
  197. else:
  198. params[col.key] = value
  199. insert.append((state, state_dict, params, mapper,
  200. connection, value_params, has_all_pks))
  201. return insert
  202. def _collect_update_commands(base_mapper, uowtransaction,
  203. table, states_to_update):
  204. """Identify sets of values to use in UPDATE statements for a
  205. list of states.
  206. This function works intricately with the history system
  207. to determine exactly what values should be updated
  208. as well as how the row should be matched within an UPDATE
  209. statement. Includes some tricky scenarios where the primary
  210. key of an object might have been changed.
  211. """
  212. update = []
  213. for state, state_dict, mapper, connection, has_identity, \
  214. instance_key, row_switch in states_to_update:
  215. if table not in mapper._pks_by_table:
  216. continue
  217. pks = mapper._pks_by_table[table]
  218. params = {}
  219. value_params = {}
  220. hasdata = hasnull = False
  221. for col in mapper._cols_by_table[table]:
  222. if col is mapper.version_id_col:
  223. params[col._label] = \
  224. mapper._get_committed_state_attr_by_column(
  225. row_switch or state,
  226. row_switch and row_switch.dict
  227. or state_dict,
  228. col)
  229. prop = mapper._columntoproperty[col]
  230. history = attributes.get_state_history(
  231. state, prop.key,
  232. attributes.PASSIVE_NO_INITIALIZE
  233. )
  234. if history.added:
  235. params[col.key] = history.added[0]
  236. hasdata = True
  237. else:
  238. params[col.key] = mapper.version_id_generator(
  239. params[col._label])
  240. # HACK: check for history, in case the
  241. # history is only
  242. # in a different table than the one
  243. # where the version_id_col is.
  244. for prop in mapper._columntoproperty.itervalues():
  245. history = attributes.get_state_history(
  246. state, prop.key,
  247. attributes.PASSIVE_NO_INITIALIZE)
  248. if history.added:
  249. hasdata = True
  250. else:
  251. prop = mapper._columntoproperty[col]
  252. history = attributes.get_state_history(
  253. state, prop.key,
  254. attributes.PASSIVE_NO_INITIALIZE)
  255. if history.added:
  256. if isinstance(history.added[0],
  257. sql.ClauseElement):
  258. value_params[col] = history.added[0]
  259. else:
  260. value = history.added[0]
  261. params[col.key] = value
  262. if col in pks:
  263. if history.deleted and \
  264. not row_switch:
  265. # if passive_updates and sync detected
  266. # this was a pk->pk sync, use the new
  267. # value to locate the row, since the
  268. # DB would already have set this
  269. if ("pk_cascaded", state, col) in \
  270. uowtransaction.attributes:
  271. value = history.added[0]
  272. params[col._label] = value
  273. else:
  274. # use the old value to
  275. # locate the row
  276. value = history.deleted[0]
  277. params[col._label] = value
  278. hasdata = True
  279. else:
  280. # row switch logic can reach us here
  281. # remove the pk from the update params
  282. # so the update doesn't
  283. # attempt to include the pk in the
  284. # update statement
  285. del params[col.key]
  286. value = history.added[0]
  287. params[col._label] = value
  288. if value is None:
  289. hasnull = True
  290. else:
  291. hasdata = True
  292. elif col in pks:
  293. value = state.manager[prop.key].impl.get(
  294. state, state_dict)
  295. if value is None:
  296. hasnull = True
  297. params[col._label] = value
  298. if hasdata:
  299. if hasnull:
  300. raise sa_exc.FlushError(
  301. "Can't update table "
  302. "using NULL for primary "
  303. "key value")
  304. update.append((state, state_dict, params, mapper,
  305. connection, value_params))
  306. return update
  307. def _collect_post_update_commands(base_mapper, uowtransaction, table,
  308. states_to_update, post_update_cols):
  309. """Identify sets of values to use in UPDATE statements for a
  310. list of states within a post_update operation.
  311. """
  312. update = []
  313. for state, state_dict, mapper, connection in states_to_update:
  314. if table not in mapper._pks_by_table:
  315. continue
  316. pks = mapper._pks_by_table[table]
  317. params = {}
  318. hasdata = False
  319. for col in mapper._cols_by_table[table]:
  320. if col in pks:
  321. params[col._label] = \
  322. mapper._get_state_attr_by_column(
  323. state,
  324. state_dict, col)
  325. elif col in post_update_cols:
  326. prop = mapper._columntoproperty[col]
  327. history = attributes.get_state_history(
  328. state, prop.key,
  329. attributes.PASSIVE_NO_INITIALIZE)
  330. if history.added:
  331. value = history.added[0]
  332. params[col.key] = value
  333. hasdata = True
  334. if hasdata:
  335. update.append((state, state_dict, params, mapper,
  336. connection))
  337. return update
  338. def _collect_delete_commands(base_mapper, uowtransaction, table,
  339. states_to_delete):
  340. """Identify values to use in DELETE statements for a list of
  341. states to be deleted."""
  342. delete = util.defaultdict(list)
  343. for state, state_dict, mapper, has_identity, connection \
  344. in states_to_delete:
  345. if not has_identity or table not in mapper._pks_by_table:
  346. continue
  347. params = {}
  348. delete[connection].append(params)
  349. for col in mapper._pks_by_table[table]:
  350. params[col.key] = \
  351. value = \
  352. mapper._get_state_attr_by_column(
  353. state, state_dict, col)
  354. if value is None:
  355. raise sa_exc.FlushError(
  356. "Can't delete from table "
  357. "using NULL for primary "
  358. "key value")
  359. if mapper.version_id_col is not None and \
  360. table.c.contains_column(mapper.version_id_col):
  361. params[mapper.version_id_col.key] = \
  362. mapper._get_committed_state_attr_by_column(
  363. state, state_dict,
  364. mapper.version_id_col)
  365. return delete
  366. def _emit_update_statements(base_mapper, uowtransaction,
  367. cached_connections, mapper, table, update):
  368. """Emit UPDATE statements corresponding to value lists collected
  369. by _collect_update_commands()."""
  370. needs_version_id = mapper.version_id_col is not None and \
  371. table.c.contains_column(mapper.version_id_col)
  372. def update_stmt():
  373. clause = sql.and_()
  374. for col in mapper._pks_by_table[table]:
  375. clause.clauses.append(col == sql.bindparam(col._label,
  376. type_=col.type))
  377. if needs_version_id:
  378. clause.clauses.append(mapper.version_id_col ==\
  379. sql.bindparam(mapper.version_id_col._label,
  380. type_=col.type))
  381. return table.update(clause)
  382. statement = base_mapper._memo(('update', table), update_stmt)
  383. rows = 0
  384. for state, state_dict, params, mapper, \
  385. connection, value_params in update:
  386. if value_params:
  387. c = connection.execute(
  388. statement.values(value_params),
  389. params)
  390. else:
  391. c = cached_connections[connection].\
  392. execute(statement, params)
  393. _postfetch(
  394. mapper,
  395. uowtransaction,
  396. table,
  397. state,
  398. state_dict,
  399. c.context.prefetch_cols,
  400. c.context.postfetch_cols,
  401. c.context.compiled_parameters[0],
  402. value_params)
  403. rows += c.rowcount
  404. if connection.dialect.supports_sane_rowcount:
  405. if rows != len(update):
  406. raise orm_exc.StaleDataError(
  407. "UPDATE statement on table '%s' expected to "
  408. "update %d row(s); %d were matched." %
  409. (table.description, len(update), rows))
  410. elif needs_version_id:
  411. util.warn("Dialect %s does not support updated rowcount "
  412. "- versioning cannot be verified." %
  413. c.dialect.dialect_description,
  414. stacklevel=12)
  415. def _emit_insert_statements(base_mapper, uowtransaction,
  416. cached_connections, table, insert):
  417. """Emit INSERT statements corresponding to value lists collected
  418. by _collect_insert_commands()."""
  419. statement = base_mapper._memo(('insert', table), table.insert)
  420. for (connection, pkeys, hasvalue, has_all_pks), \
  421. records in groupby(insert,
  422. lambda rec: (rec[4],
  423. rec[2].keys(),
  424. bool(rec[5]),
  425. rec[6])
  426. ):
  427. if has_all_pks and not hasvalue:
  428. records = list(records)
  429. multiparams = [rec[2] for rec in records]
  430. c = cached_connections[connection].\
  431. execute(statement, multiparams)
  432. for (state, state_dict, params, mapper,
  433. conn, value_params, has_all_pks), \
  434. last_inserted_params in \
  435. zip(records, c.context.compiled_parameters):
  436. _postfetch(
  437. mapper,
  438. uowtransaction,
  439. table,
  440. state,
  441. state_dict,
  442. c.context.prefetch_cols,
  443. c.context.postfetch_cols,
  444. last_inserted_params,
  445. value_params)
  446. else:
  447. for state, state_dict, params, mapper, \
  448. connection, value_params, \
  449. has_all_pks in records:
  450. if value_params:
  451. result = connection.execute(
  452. statement.values(value_params),
  453. params)
  454. else:
  455. result = cached_connections[connection].\
  456. execute(statement, params)
  457. primary_key = result.context.inserted_primary_key
  458. if primary_key is not None:
  459. # set primary key attributes
  460. for pk, col in zip(primary_key,
  461. mapper._pks_by_table[table]):
  462. prop = mapper._columntoproperty[col]
  463. if state_dict.get(prop.key) is None:
  464. # TODO: would rather say:
  465. #state_dict[prop.key] = pk
  466. mapper._set_state_attr_by_column(
  467. state,
  468. state_dict,
  469. col, pk)
  470. _postfetch(
  471. mapper,
  472. uowtransaction,
  473. table,
  474. state,
  475. state_dict,
  476. result.context.prefetch_cols,
  477. result.context.postfetch_cols,
  478. result.context.compiled_parameters[0],
  479. value_params)
  480. def _emit_post_update_statements(base_mapper, uowtransaction,
  481. cached_connections, mapper, table, update):
  482. """Emit UPDATE statements corresponding to value lists collected
  483. by _collect_post_update_commands()."""
  484. def update_stmt():
  485. clause = sql.and_()
  486. for col in mapper._pks_by_table[table]:
  487. clause.clauses.append(col == sql.bindparam(col._label,
  488. type_=col.type))
  489. return table.update(clause)
  490. statement = base_mapper._memo(('post_update', table), update_stmt)
  491. # execute each UPDATE in the order according to the original
  492. # list of states to guarantee row access order, but
  493. # also group them into common (connection, cols) sets
  494. # to support executemany().
  495. for key, grouper in groupby(
  496. update, lambda rec: (rec[4], rec[2].keys())
  497. ):
  498. connection = key[0]
  499. multiparams = [params for state, state_dict,
  500. params, mapper, conn in grouper]
  501. cached_connections[connection].\
  502. execute(statement, multiparams)
  503. def _emit_delete_statements(base_mapper, uowtransaction, cached_connections,
  504. mapper, table, delete):
  505. """Emit DELETE statements corresponding to value lists collected
  506. by _collect_delete_commands()."""
  507. need_version_id = mapper.version_id_col is not None and \
  508. table.c.contains_column(mapper.version_id_col)
  509. def delete_stmt():
  510. clause = sql.and_()
  511. for col in mapper._pks_by_table[table]:
  512. clause.clauses.append(
  513. col == sql.bindparam(col.key, type_=col.type))
  514. if need_version_id:
  515. clause.clauses.append(
  516. mapper.version_id_col ==
  517. sql.bindparam(
  518. mapper.version_id_col.key,
  519. type_=mapper.version_id_col.type
  520. )
  521. )
  522. return table.delete(clause)
  523. for connection, del_objects in delete.iteritems():
  524. statement = base_mapper._memo(('delete', table), delete_stmt)
  525. connection = cached_connections[connection]
  526. if need_version_id:
  527. # TODO: need test coverage for this [ticket:1761]
  528. if connection.dialect.supports_sane_rowcount:
  529. rows = 0
  530. # execute deletes individually so that versioned
  531. # rows can be verified
  532. for params in del_objects:
  533. c = connection.execute(statement, params)
  534. rows += c.rowcount
  535. if rows != len(del_objects):
  536. raise orm_exc.StaleDataError(
  537. "DELETE statement on table '%s' expected to "
  538. "delete %d row(s); %d were matched." %
  539. (table.description, len(del_objects), c.rowcount)
  540. )
  541. else:
  542. util.warn(
  543. "Dialect %s does not support deleted rowcount "
  544. "- versioning cannot be verified." %
  545. connection.dialect.dialect_description,
  546. stacklevel=12)
  547. connection.execute(statement, del_objects)
  548. else:
  549. connection.execute(statement, del_objects)
  550. def _finalize_insert_update_commands(base_mapper, uowtransaction,
  551. states_to_insert, states_to_update):
  552. """finalize state on states that have been inserted or updated,
  553. including calling after_insert/after_update events.
  554. """
  555. for state, state_dict, mapper, connection, has_identity, \
  556. instance_key, row_switch in states_to_insert + \
  557. states_to_update:
  558. if mapper._readonly_props:
  559. readonly = state.unmodified_intersection(
  560. [p.key for p in mapper._readonly_props
  561. if p.expire_on_flush or p.key not in state.dict]
  562. )
  563. if readonly:
  564. state.expire_attributes(state.dict, readonly)
  565. # if eager_defaults option is enabled,
  566. # refresh whatever has been expired.
  567. if base_mapper.eager_defaults and state.unloaded:
  568. state.key = base_mapper._identity_key_from_state(state)
  569. uowtransaction.session.query(base_mapper)._load_on_ident(
  570. state.key, refresh_state=state,
  571. only_load_props=state.unloaded)
  572. # call after_XXX extensions
  573. if not has_identity:
  574. mapper.dispatch.after_insert(mapper, connection, state)
  575. else:
  576. mapper.dispatch.after_update(mapper, connection, state)
  577. def _postfetch(mapper, uowtransaction, table,
  578. state, dict_, prefetch_cols, postfetch_cols,
  579. params, value_params):
  580. """Expire attributes in need of newly persisted database state,
  581. after an INSERT or UPDATE statement has proceeded for that
  582. state."""
  583. if mapper.version_id_col is not None:
  584. prefetch_cols = list(prefetch_cols) + [mapper.version_id_col]
  585. for c in prefetch_cols:
  586. if c.key in params and c in mapper._columntoproperty:
  587. mapper._set_state_attr_by_column(state, dict_, c, params[c.key])
  588. if postfetch_cols:
  589. state.expire_attributes(state.dict,
  590. [mapper._columntoproperty[c].key
  591. for c in postfetch_cols if c in
  592. mapper._columntoproperty]
  593. )
  594. # synchronize newly inserted ids from one table to the next
  595. # TODO: this still goes a little too often. would be nice to
  596. # have definitive list of "columns that changed" here
  597. for m, equated_pairs in mapper._table_to_equated[table]:
  598. sync.populate(state, m, state, m,
  599. equated_pairs,
  600. uowtransaction,
  601. mapper.passive_updates)
  602. def _connections_for_states(base_mapper, uowtransaction, states):
  603. """Return an iterator of (state, state.dict, mapper, connection).
  604. The states are sorted according to _sort_states, then paired
  605. with the connection they should be using for the given
  606. unit of work transaction.
  607. """
  608. # if session has a connection callable,
  609. # organize individual states with the connection
  610. # to use for update
  611. if uowtransaction.session.connection_callable:
  612. connection_callable = \
  613. uowtransaction.session.connection_callable
  614. else:
  615. connection = None
  616. connection_callable = None
  617. for state in _sort_states(states):
  618. if connection_callable:
  619. connection = connection_callable(base_mapper, state.obj())
  620. elif not connection:
  621. connection = uowtransaction.transaction.connection(
  622. base_mapper)
  623. mapper = _state_mapper(state)
  624. yield state, state.dict, mapper, connection
  625. def _cached_connection_dict(base_mapper):
  626. # dictionary of connection->connection_with_cache_options.
  627. return util.PopulateDict(
  628. lambda conn:conn.execution_options(
  629. compiled_cache=base_mapper._compiled_cache
  630. ))
  631. def _sort_states(states):
  632. pending = set(states)
  633. persistent = set(s for s in pending if s.key is not None)
  634. pending.difference_update(persistent)
  635. return sorted(pending, key=operator.attrgetter("insert_order")) + \
  636. sorted(persistent, key=lambda q:q.key[1])