1============================= 2What's new in SQLAlchemy 0.4? 3============================= 4 5.. admonition:: About this Document 6 7 This document describes changes between SQLAlchemy version 0.3, 8 last released October 14, 2007, and SQLAlchemy version 0.4, 9 last released October 12, 2008. 10 11 Document date: March 21, 2008 12 13First Things First 14================== 15 16If you're using any ORM features, make sure you import from 17``sqlalchemy.orm``: 18 19:: 20 21 from sqlalchemy import * 22 from sqlalchemy.orm import * 23 24Secondly, anywhere you used to say ``engine=``, 25``connectable=``, ``bind_to=``, ``something.engine``, 26``metadata.connect()``, use ``bind``: 27 28:: 29 30 myengine = create_engine('sqlite://') 31 32 meta = MetaData(myengine) 33 34 meta2 = MetaData() 35 meta2.bind = myengine 36 37 session = create_session(bind=myengine) 38 39 statement = select([table], bind=myengine) 40 41Got those ? Good! You're now (95%) 0.4 compatible. If 42you're using 0.3.10, you can make these changes immediately; 43they'll work there too. 44 45Module Imports 46============== 47 48In 0.3, "``from sqlachemy import *``" would import all of 49sqlachemy's sub-modules into your namespace. Version 0.4 no 50longer imports sub-modules into the namespace. This may mean 51you need to add extra imports into your code. 52 53In 0.3, this code worked: 54 55:: 56 57 from sqlalchemy import * 58 59 class UTCDateTime(types.TypeDecorator): 60 pass 61 62In 0.4, one must do: 63 64:: 65 66 from sqlalchemy import * 67 from sqlalchemy import types 68 69 class UTCDateTime(types.TypeDecorator): 70 pass 71 72Object Relational Mapping 73========================= 74 75Querying 76-------- 77 78New Query API 79^^^^^^^^^^^^^ 80 81Query is standardized on the generative interface (old 82interface is still there, just deprecated). While most of 83the generative interface is available in 0.3, the 0.4 Query 84has the inner guts to match the generative outside, and has 85a lot more tricks. All result narrowing is via ``filter()`` 86and ``filter_by()``, limiting/offset is either through array 87slices or ``limit()``/``offset()``, joining is via 88``join()`` and ``outerjoin()`` (or more manually, through 89``select_from()`` as well as manually-formed criteria). 90 91To avoid deprecation warnings, you must make some changes to 92your 03 code 93 94User.query.get_by( \**kwargs ) 95 96:: 97 98 User.query.filter_by(**kwargs).first() 99 100User.query.select_by( \**kwargs ) 101 102:: 103 104 User.query.filter_by(**kwargs).all() 105 106User.query.select() 107 108:: 109 110 User.query.filter(xxx).all() 111 112New Property-Based Expression Constructs 113^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 114 115By far the most palpable difference within the ORM is that 116you can now construct your query criterion using class-based 117attributes directly. The ".c." prefix is no longer needed 118when working with mapped classes: 119 120:: 121 122 session.query(User).filter(and_(User.name == 'fred', User.id > 17)) 123 124While simple column-based comparisons are no big deal, the 125class attributes have some new "higher level" constructs 126available, including what was previously only available in 127``filter_by()``: 128 129:: 130 131 # comparison of scalar relations to an instance 132 filter(Address.user == user) 133 134 # return all users who contain a particular address 135 filter(User.addresses.contains(address)) 136 137 # return all users who *dont* contain the address 138 filter(~User.address.contains(address)) 139 140 # return all users who contain a particular address with 141 # the email_address like '%foo%' 142 filter(User.addresses.any(Address.email_address.like('%foo%'))) 143 144 # same, email address equals 'foo@bar.com'. can fall back to keyword 145 # args for simple comparisons 146 filter(User.addresses.any(email_address = 'foo@bar.com')) 147 148 # return all Addresses whose user attribute has the username 'ed' 149 filter(Address.user.has(name='ed')) 150 151 # return all Addresses whose user attribute has the username 'ed' 152 # and an id > 5 (mixing clauses with kwargs) 153 filter(Address.user.has(User.id > 5, name='ed')) 154 155The ``Column`` collection remains available on mapped 156classes in the ``.c`` attribute. Note that property-based 157expressions are only available with mapped properties of 158mapped classes. ``.c`` is still used to access columns in 159regular tables and selectable objects produced from SQL 160Expressions. 161 162Automatic Join Aliasing 163^^^^^^^^^^^^^^^^^^^^^^^ 164 165We've had join() and outerjoin() for a while now: 166 167:: 168 169 session.query(Order).join('items')... 170 171Now you can alias them: 172 173:: 174 175 session.query(Order).join('items', aliased=True). 176 filter(Item.name='item 1').join('items', aliased=True).filter(Item.name=='item 3') 177 178The above will create two joins from orders->items using 179aliases. the ``filter()`` call subsequent to each will 180adjust its table criterion to that of the alias. To get at 181the ``Item`` objects, use ``add_entity()`` and target each 182join with an ``id``: 183 184:: 185 186 session.query(Order).join('items', id='j1', aliased=True). 187 filter(Item.name == 'item 1').join('items', aliased=True, id='j2'). 188 filter(Item.name == 'item 3').add_entity(Item, id='j1').add_entity(Item, id='j2') 189 190Returns tuples in the form: ``(Order, Item, Item)``. 191 192Self-referential Queries 193^^^^^^^^^^^^^^^^^^^^^^^^ 194 195So query.join() can make aliases now. What does that give 196us ? Self-referential queries ! Joins can be done without 197any ``Alias`` objects: 198 199:: 200 201 # standard self-referential TreeNode mapper with backref 202 mapper(TreeNode, tree_nodes, properties={ 203 'children':relation(TreeNode, backref=backref('parent', remote_side=tree_nodes.id)) 204 }) 205 206 # query for node with child containing "bar" two levels deep 207 session.query(TreeNode).join(["children", "children"], aliased=True).filter_by(name='bar') 208 209To add criterion for each table along the way in an aliased 210join, you can use ``from_joinpoint`` to keep joining against 211the same line of aliases: 212 213:: 214 215 # search for the treenode along the path "n1/n12/n122" 216 217 # first find a Node with name="n122" 218 q = sess.query(Node).filter_by(name='n122') 219 220 # then join to parent with "n12" 221 q = q.join('parent', aliased=True).filter_by(name='n12') 222 223 # join again to the next parent with 'n1'. use 'from_joinpoint' 224 # so we join from the previous point, instead of joining off the 225 # root table 226 q = q.join('parent', aliased=True, from_joinpoint=True).filter_by(name='n1') 227 228 node = q.first() 229 230``query.populate_existing()`` 231^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 232 233The eager version of ``query.load()`` (or 234``session.refresh()``). Every instance loaded from the 235query, including all eagerly loaded items, get refreshed 236immediately if already present in the session: 237 238:: 239 240 session.query(Blah).populate_existing().all() 241 242Relations 243--------- 244 245SQL Clauses Embedded in Updates/Inserts 246^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 247 248For inline execution of SQL clauses, embedded right in the 249UPDATE or INSERT, during a ``flush()``: 250 251:: 252 253 254 myobject.foo = mytable.c.value + 1 255 256 user.pwhash = func.md5(password) 257 258 order.hash = text("select hash from hashing_table") 259 260The column-attribute is set up with a deferred loader after 261the operation, so that it issues the SQL to load the new 262value when you next access. 263 264Self-referential and Cyclical Eager Loading 265^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 266 267Since our alias-fu has improved, ``relation()`` can join 268along the same table \*any number of times*; you tell it how 269deep you want to go. Lets show the self-referential 270``TreeNode`` more clearly: 271 272:: 273 274 nodes = Table('nodes', metadata, 275 Column('id', Integer, primary_key=True), 276 Column('parent_id', Integer, ForeignKey('nodes.id')), 277 Column('name', String(30))) 278 279 class TreeNode(object): 280 pass 281 282 mapper(TreeNode, nodes, properties={ 283 'children':relation(TreeNode, lazy=False, join_depth=3) 284 }) 285 286So what happens when we say: 287 288:: 289 290 create_session().query(TreeNode).all() 291 292? A join along aliases, three levels deep off the parent: 293 294:: 295 296 SELECT 297 nodes_3.id AS nodes_3_id, nodes_3.parent_id AS nodes_3_parent_id, nodes_3.name AS nodes_3_name, 298 nodes_2.id AS nodes_2_id, nodes_2.parent_id AS nodes_2_parent_id, nodes_2.name AS nodes_2_name, 299 nodes_1.id AS nodes_1_id, nodes_1.parent_id AS nodes_1_parent_id, nodes_1.name AS nodes_1_name, 300 nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.name AS nodes_name 301 FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id 302 LEFT OUTER JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id 303 LEFT OUTER JOIN nodes AS nodes_3 ON nodes_2.id = nodes_3.parent_id 304 ORDER BY nodes.oid, nodes_1.oid, nodes_2.oid, nodes_3.oid 305 306Notice the nice clean alias names too. The joining doesn't 307care if it's against the same immediate table or some other 308object which then cycles back to the beginning. Any kind 309of chain of eager loads can cycle back onto itself when 310``join_depth`` is specified. When not present, eager 311loading automatically stops when it hits a cycle. 312 313Composite Types 314^^^^^^^^^^^^^^^ 315 316This is one from the Hibernate camp. Composite Types let 317you define a custom datatype that is composed of more than 318one column (or one column, if you wanted). Lets define a 319new type, ``Point``. Stores an x/y coordinate: 320 321:: 322 323 class Point(object): 324 def __init__(self, x, y): 325 self.x = x 326 self.y = y 327 def __composite_values__(self): 328 return self.x, self.y 329 def __eq__(self, other): 330 return other.x == self.x and other.y == self.y 331 def __ne__(self, other): 332 return not self.__eq__(other) 333 334The way the ``Point`` object is defined is specific to a 335custom type; constructor takes a list of arguments, and the 336``__composite_values__()`` method produces a sequence of 337those arguments. The order will match up to our mapper, as 338we'll see in a moment. 339 340Let's create a table of vertices storing two points per row: 341 342:: 343 344 vertices = Table('vertices', metadata, 345 Column('id', Integer, primary_key=True), 346 Column('x1', Integer), 347 Column('y1', Integer), 348 Column('x2', Integer), 349 Column('y2', Integer), 350 ) 351 352Then, map it ! We'll create a ``Vertex`` object which 353stores two ``Point`` objects: 354 355:: 356 357 class Vertex(object): 358 def __init__(self, start, end): 359 self.start = start 360 self.end = end 361 362 mapper(Vertex, vertices, properties={ 363 'start':composite(Point, vertices.c.x1, vertices.c.y1), 364 'end':composite(Point, vertices.c.x2, vertices.c.y2) 365 }) 366 367Once you've set up your composite type, it's usable just 368like any other type: 369 370:: 371 372 373 v = Vertex(Point(3, 4), Point(26,15)) 374 session.save(v) 375 session.flush() 376 377 # works in queries too 378 q = session.query(Vertex).filter(Vertex.start == Point(3, 4)) 379 380If you'd like to define the way the mapped attributes 381generate SQL clauses when used in expressions, create your 382own ``sqlalchemy.orm.PropComparator`` subclass, defining any 383of the common operators (like ``__eq__()``, ``__le__()``, 384etc.), and send it in to ``composite()``. Composite types 385work as primary keys too, and are usable in ``query.get()``: 386 387:: 388 389 # a Document class which uses a composite Version 390 # object as primary key 391 document = query.get(Version(1, 'a')) 392 393``dynamic_loader()`` relations 394^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 395 396A ``relation()`` that returns a live ``Query`` object for 397all read operations. Write operations are limited to just 398``append()`` and ``remove()``, changes to the collection are 399not visible until the session is flushed. This feature is 400particularly handy with an "autoflushing" session which will 401flush before each query. 402 403:: 404 405 mapper(Foo, foo_table, properties={ 406 'bars':dynamic_loader(Bar, backref='foo', <other relation() opts>) 407 }) 408 409 session = create_session(autoflush=True) 410 foo = session.query(Foo).first() 411 412 foo.bars.append(Bar(name='lala')) 413 414 for bar in foo.bars.filter(Bar.name=='lala'): 415 print(bar) 416 417 session.commit() 418 419New Options: ``undefer_group()``, ``eagerload_all()`` 420^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 421 422A couple of query options which are handy. 423``undefer_group()`` marks a whole group of "deferred" 424columns as undeferred: 425 426:: 427 428 mapper(Class, table, properties={ 429 'foo' : deferred(table.c.foo, group='group1'), 430 'bar' : deferred(table.c.bar, group='group1'), 431 'bat' : deferred(table.c.bat, group='group1'), 432 ) 433 434 session.query(Class).options(undefer_group('group1')).filter(...).all() 435 436and ``eagerload_all()`` sets a chain of attributes to be 437eager in one pass: 438 439:: 440 441 mapper(Foo, foo_table, properties={ 442 'bar':relation(Bar) 443 }) 444 mapper(Bar, bar_table, properties={ 445 'bat':relation(Bat) 446 }) 447 mapper(Bat, bat_table) 448 449 # eager load bar and bat 450 session.query(Foo).options(eagerload_all('bar.bat')).filter(...).all() 451 452New Collection API 453^^^^^^^^^^^^^^^^^^ 454 455Collections are no longer proxied by an 456{{{InstrumentedList}}} proxy, and access to members, methods 457and attributes is direct. Decorators now intercept objects 458entering and leaving the collection, and it is now possible 459to easily write a custom collection class that manages its 460own membership. Flexible decorators also replace the named 461method interface of custom collections in 0.3, allowing any 462class to be easily adapted to use as a collection container. 463 464Dictionary-based collections are now much easier to use and 465fully ``dict``-like. Changing ``__iter__`` is no longer 466needed for ``dict``s, and new built-in ``dict`` types cover 467many needs: 468 469:: 470 471 # use a dictionary relation keyed by a column 472 relation(Item, collection_class=column_mapped_collection(items.c.keyword)) 473 # or named attribute 474 relation(Item, collection_class=attribute_mapped_collection('keyword')) 475 # or any function you like 476 relation(Item, collection_class=mapped_collection(lambda entity: entity.a + entity.b)) 477 478Existing 0.3 ``dict``-like and freeform object derived 479collection classes will need to be updated for the new API. 480In most cases this is simply a matter of adding a couple 481decorators to the class definition. 482 483Mapped Relations from External Tables/Subqueries 484^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 485 486This feature quietly appeared in 0.3 but has been improved 487in 0.4 thanks to better ability to convert subqueries 488against a table into subqueries against an alias of that 489table; this is key for eager loading, aliased joins in 490queries, etc. It reduces the need to create mappers against 491select statements when you just need to add some extra 492columns or subqueries: 493 494:: 495 496 mapper(User, users, properties={ 497 'fullname': column_property((users.c.firstname + users.c.lastname).label('fullname')), 498 'numposts': column_property( 499 select([func.count(1)], users.c.id==posts.c.user_id).correlate(users).label('posts') 500 ) 501 }) 502 503a typical query looks like: 504 505:: 506 507 SELECT (SELECT count(1) FROM posts WHERE users.id = posts.user_id) AS count, 508 users.firstname || users.lastname AS fullname, 509 users.id AS users_id, users.firstname AS users_firstname, users.lastname AS users_lastname 510 FROM users ORDER BY users.oid 511 512Horizontal Scaling (Sharding) API 513--------------------------------- 514 515[browser:/sqlalchemy/trunk/examples/sharding/attribute_shard 516.py] 517 518Sessions 519-------- 520 521New Session Create Paradigm; SessionContext, assignmapper Deprecated 522^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 523 524That's right, the whole shebang is being replaced with two 525configurational functions. Using both will produce the most 5260.1-ish feel we've had since 0.1 (i.e., the least amount of 527typing). 528 529Configure your own ``Session`` class right where you define 530your ``engine`` (or anywhere): 531 532:: 533 534 from sqlalchemy import create_engine 535 from sqlalchemy.orm import sessionmaker 536 537 engine = create_engine('myengine://') 538 Session = sessionmaker(bind=engine, autoflush=True, transactional=True) 539 540 # use the new Session() freely 541 sess = Session() 542 sess.save(someobject) 543 sess.flush() 544 545 546If you need to post-configure your Session, say with an 547engine, add it later with ``configure()``: 548 549:: 550 551 Session.configure(bind=create_engine(...)) 552 553All the behaviors of ``SessionContext`` and the ``query`` 554and ``__init__`` methods of ``assignmapper`` are moved into 555the new ``scoped_session()`` function, which is compatible 556with both ``sessionmaker`` as well as ``create_session()``: 557 558:: 559 560 from sqlalchemy.orm import scoped_session, sessionmaker 561 562 Session = scoped_session(sessionmaker(autoflush=True, transactional=True)) 563 Session.configure(bind=engine) 564 565 u = User(name='wendy') 566 567 sess = Session() 568 sess.save(u) 569 sess.commit() 570 571 # Session constructor is thread-locally scoped. Everyone gets the same 572 # Session in the thread when scope="thread". 573 sess2 = Session() 574 assert sess is sess2 575 576 577When using a thread-local ``Session``, the returned class 578has all of ``Session's`` interface implemented as 579classmethods, and "assignmapper"'s functionality is 580available using the ``mapper`` classmethod. Just like the 581old ``objectstore`` days.... 582 583:: 584 585 586 # "assignmapper"-like functionality available via ScopedSession.mapper 587 Session.mapper(User, users_table) 588 589 u = User(name='wendy') 590 591 Session.commit() 592 593 594Sessions are again Weak Referencing By Default 595^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 596 597The weak_identity_map flag is now set to ``True`` by default 598on Session. Instances which are externally deferenced and 599fall out of scope are removed from the session 600automatically. However, items which have "dirty" changes 601present will remain strongly referenced until those changes 602are flushed at which case the object reverts to being weakly 603referenced (this works for 'mutable' types, like picklable 604attributes, as well). Setting weak_identity_map to 605``False`` restores the old strong-referencing behavior for 606those of you using the session like a cache. 607 608Auto-Transactional Sessions 609^^^^^^^^^^^^^^^^^^^^^^^^^^^ 610 611As you might have noticed above, we are calling ``commit()`` 612on ``Session``. The flag ``transactional=True`` means the 613``Session`` is always in a transaction, ``commit()`` 614persists permanently. 615 616Auto-Flushing Sessions 617^^^^^^^^^^^^^^^^^^^^^^ 618 619Also, ``autoflush=True`` means the ``Session`` will 620``flush()`` before each ``query`` as well as when you call 621``flush()`` or ``commit()``. So now this will work: 622 623:: 624 625 Session = sessionmaker(bind=engine, autoflush=True, transactional=True) 626 627 u = User(name='wendy') 628 629 sess = Session() 630 sess.save(u) 631 632 # wendy is flushed, comes right back from a query 633 wendy = sess.query(User).filter_by(name='wendy').one() 634 635Transactional methods moved onto sessions 636^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 637 638``commit()`` and ``rollback()``, as well as ``begin()`` are 639now directly on ``Session``. No more need to use 640``SessionTransaction`` for anything (it remains in the 641background). 642 643:: 644 645 Session = sessionmaker(autoflush=True, transactional=False) 646 647 sess = Session() 648 sess.begin() 649 650 # use the session 651 652 sess.commit() # commit transaction 653 654Sharing a ``Session`` with an enclosing engine-level (i.e. 655non-ORM) transaction is easy: 656 657:: 658 659 Session = sessionmaker(autoflush=True, transactional=False) 660 661 conn = engine.connect() 662 trans = conn.begin() 663 sess = Session(bind=conn) 664 665 # ... session is transactional 666 667 # commit the outermost transaction 668 trans.commit() 669 670Nested Session Transactions with SAVEPOINT 671^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 672 673Available at the Engine and ORM level. ORM docs so far: 674 675http://www.sqlalchemy.org/docs/04/session.html#unitofwork_ma 676naging 677 678Two-Phase Commit Sessions 679^^^^^^^^^^^^^^^^^^^^^^^^^ 680 681Available at the Engine and ORM level. ORM docs so far: 682 683http://www.sqlalchemy.org/docs/04/session.html#unitofwork_ma 684naging 685 686Inheritance 687----------- 688 689Polymorphic Inheritance with No Joins or Unions 690^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 691 692New docs for inheritance: http://www.sqlalchemy.org/docs/04 693/mappers.html#advdatamapping_mapper_inheritance_joined 694 695Better Polymorphic Behavior with ``get()`` 696^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 697 698All classes within a joined-table inheritance hierarchy get 699an ``_instance_key`` using the base class, i.e. 700``(BaseClass, (1, ), None)``. That way when you call 701``get()`` a ``Query`` against the base class, it can locate 702subclass instances in the current identity map without 703querying the database. 704 705Types 706----- 707 708Custom Subclasses of ``sqlalchemy.types.TypeDecorator`` 709^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 710 711There is a `New API <http://www.sqlalchemy.org/docs/04/types 712.html#types_custom>`_ for subclassing a TypeDecorator. 713Using the 0.3 API causes compilation errors in some cases. 714 715SQL Expressions 716=============== 717 718All New, Deterministic Label/Alias Generation 719--------------------------------------------- 720 721All the "anonymous" labels and aliases use a simple 722<name>_<number> format now. SQL is much easier to read and 723is compatible with plan optimizer caches. Just check out 724some of the examples in the tutorials: 725http://www.sqlalchemy.org/docs/04/ormtutorial.html 726http://www.sqlalchemy.org/docs/04/sqlexpression.html 727 728Generative select() Constructs 729------------------------------ 730 731This is definitely the way to go with ``select()``. See htt 732p://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_transf 733orm . 734 735New Operator System 736------------------- 737 738SQL operators and more or less every SQL keyword there is 739are now abstracted into the compiler layer. They now act 740intelligently and are type/backend aware, see: http://www.sq 741lalchemy.org/docs/04/sqlexpression.html#sql_operators 742 743All ``type`` Keyword Arguments Renamed to ``type_`` 744--------------------------------------------------- 745 746Just like it says: 747 748:: 749 750 b = bindparam('foo', type_=String) 751 752in\_ Function Changed to Accept Sequence or Selectable 753------------------------------------------------------ 754 755The in\_ function now takes a sequence of values or a 756selectable as its sole argument. The previous API of passing 757in values as positional arguments still works, but is now 758deprecated. This means that 759 760:: 761 762 my_table.select(my_table.c.id.in_(1,2,3) 763 my_table.select(my_table.c.id.in_(*listOfIds) 764 765should be changed to 766 767:: 768 769 my_table.select(my_table.c.id.in_([1,2,3]) 770 my_table.select(my_table.c.id.in_(listOfIds) 771 772Schema and Reflection 773===================== 774 775``MetaData``, ``BoundMetaData``, ``DynamicMetaData``... 776------------------------------------------------------- 777 778In the 0.3.x series, ``BoundMetaData`` and 779``DynamicMetaData`` were deprecated in favor of ``MetaData`` 780and ``ThreadLocalMetaData``. The older names have been 781removed in 0.4. Updating is simple: 782 783:: 784 785 +-------------------------------------+-------------------------+ 786 |If You Had | Now Use | 787 +=====================================+=========================+ 788 | ``MetaData`` | ``MetaData`` | 789 +-------------------------------------+-------------------------+ 790 | ``BoundMetaData`` | ``MetaData`` | 791 +-------------------------------------+-------------------------+ 792 | ``DynamicMetaData`` (with one | ``MetaData`` | 793 | engine or threadlocal=False) | | 794 +-------------------------------------+-------------------------+ 795 | ``DynamicMetaData`` | ``ThreadLocalMetaData`` | 796 | (with different engines per thread) | | 797 +-------------------------------------+-------------------------+ 798 799The seldom-used ``name`` parameter to ``MetaData`` types has 800been removed. The ``ThreadLocalMetaData`` constructor now 801takes no arguments. Both types can now be bound to an 802``Engine`` or a single ``Connection``. 803 804One Step Multi-Table Reflection 805------------------------------- 806 807You can now load table definitions and automatically create 808``Table`` objects from an entire database or schema in one 809pass: 810 811:: 812 813 >>> metadata = MetaData(myengine, reflect=True) 814 >>> metadata.tables.keys() 815 ['table_a', 'table_b', 'table_c', '...'] 816 817``MetaData`` also gains a ``.reflect()`` method enabling 818finer control over the loading process, including 819specification of a subset of available tables to load. 820 821SQL Execution 822============= 823 824``engine``, ``connectable``, and ``bind_to`` are all now ``bind`` 825----------------------------------------------------------------- 826 827``Transactions``, ``NestedTransactions`` and ``TwoPhaseTransactions`` 828--------------------------------------------------------------------- 829 830Connection Pool Events 831---------------------- 832 833The connection pool now fires events when new DB-API 834connections are created, checked out and checked back into 835the pool. You can use these to execute session-scoped SQL 836setup statements on fresh connections, for example. 837 838Oracle Engine Fixed 839------------------- 840 841In 0.3.11, there were bugs in the Oracle Engine on how 842Primary Keys are handled. These bugs could cause programs 843that worked fine with other engines, such as sqlite, to fail 844when using the Oracle Engine. In 0.4, the Oracle Engine has 845been reworked, fixing these Primary Key problems. 846 847Out Parameters for Oracle 848------------------------- 849 850:: 851 852 result = engine.execute(text("begin foo(:x, :y, :z); end;", bindparams=[bindparam('x', Numeric), outparam('y', Numeric), outparam('z', Numeric)]), x=5) 853 assert result.out_parameters == {'y':10, 'z':75} 854 855Connection-bound ``MetaData``, ``Sessions`` 856------------------------------------------- 857 858``MetaData`` and ``Session`` can be explicitly bound to a 859connection: 860 861:: 862 863 conn = engine.connect() 864 sess = create_session(bind=conn) 865 866Faster, More Foolproof ``ResultProxy`` Objects 867---------------------------------------------- 868 869