SQLAlchemy & Odin Data Access

Fifty Data Science


Simeon Simeonov

Agenda


  • SQLAlchemy - Design & overview
  • SQLAlchemy - A small practical example
  • Odin Data Access - Overview and examples
  • Q & A

What is SQLAlchemy?


SQLAlchemy is a Python library created by Mike Bayer to provide a high-level Pythonic interface to RDBMS such as PostgreSQL, SQLite, MySQL, Oracle, DB2.

SQLAlchemy includes RDBMS-independent SQL expression language and an object-relational mapper (ORM).

Why use SQLAlchemy?


  • portability - the programming interface is independent of the type of RDBMS and connector used

  • security - no more SQL injections

  • abstraction - no need to bother with complex JOINs

  • object-orientation - you work with objects and NOT tables and rows

  • performance - exploits the likehood of reusing a particular query

  • flexibility - you can override almost anything

Basic architecture

SQLAlchemy consists of several components, including the the ORM.

  • Engine- manages the connection pool and the RDBMS-independent SQL dialect layer
  • MetaData - used to collect and organize information about your table layout (schema)
  • Session - establishes all conversations with the RDBMS and represents a "holding zone" for all the objects which you've loaded or associated with it during its lifespan
  • SQL expression language - provides an API to execute your queries and updates against your tables, all from Python, and all in a database-independent way (low-level interface)
  • ORM - provides a convenient way to add database persistence to your Python objects withoutrequiring you to design your objects around the database, or the database around the objects (high-level interface)

Example

SQLAlchemy gives us the choice between classical mapping and the newer declarative mapping

            
              import sqlalchemy
              sqlalchemy.__version__

              from sqlalchemy import create_engine

              # engine = create_engine("postgresql+psycopg2://user:zipassword@localhost/mydb" , echo=True)
              # The string form of the URL is dialect+driver://user:password@host/dbname[?key=value..],
              # where dialect is a database name such as mysql, oracle, postgresql, etc.,
              # and driver the name of a DBAPI, such as psycopg2, pyodbc, cx_oracle
              # The echo flag is a shortcut to setting up SQLAlchemy logging,
              # which is accomplished via Python’s standard logging module.
              # With it enabled, we’ll see all the generated SQL produced.
              # engine = create_engine("sqlite:///library.db", echo=True)
              engine = create_engine("sqlite:///:memory:", echo=True)

              metadata = MetaData()

              from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey

              authors_table = Table(
                  "authors",
                  metadata,
                  Column("id", Integer, primary_key=True),
                  Column("name", String),
              )  # Column("name", String(50)) is possible

              books_table = Table(
                  "books",
                  metadata,
                  Column("id", Integer, primary_key=True),
                  Column("title", String),
                  Column("description", String),
                  Column("author_id", ForeignKey('authors.id')),
              )

              metadata.create_all(engine)  # creates the tables
            
          

Example (cont...)

Use of SQL expression language

            
              insert_stmt = authors_table.insert(bind=engine)
              type(insert_stmt)
              # Out: <class 'sqlalchemy.sql.expression.Insert'>
              print(insert_stmt)
              # Out: INSERT INTO authors (id, name) VALUES (:id,:name)

              compiled_stmt = insert_stmt.compile()
              print(compiled_stmt.params)
              # Out: {'id': None, 'name': None}

              insert_stmt.execute(name="Alexandre Dumas")  # insert a single entry
              insert_stmt.execute([{"name": 'Mr X'},{'name': 'Mr Y'}])  # a list of entries

              metadata.bind = engine  # no need to explicitly bind the engine from now on
              select_stmt = authors_table.select(authors_table.c.id==2)
              result = select_stmt.execute()
              result.fetchall()
              # Out: [(1, u'Mr X')]

              del_stmt = authors_table.delete()
              del_stmt.execute(whereclause=text("name='Mr Y'"))
              del_stmt.execute()  # delete all
            
          

Example (cont...)

Use of classical mapping

            
              from sqlalchemy.orm import mapper
              from sqlalchemy.orm import relationship, backref

              class Author:
                  def __init__(self, name):
                      self.name = name

                  def __str__(self):
                      return self.name


              class Book:
                  def __init__(self, title, description, author):
                      self.title = title
                      self.description = description
                      self.author = author

                  def __str__(self):
                      return self.title

              mapper(Book, books_table)
              mapper(Author, authors_table, properties = {"books": relation(Book, backref="author")})
            
          

Example (cont...)

Doing the same thing the easy way with declarative mapping

            
              from sqlalchemy.ext.declarative import declarative_base
              from sqlalchemy.orm import relationship, backref

              Base = declarative_base()

              class Author(Base):
                  __tablename__ = "authors"

                  id = Column(Integer, primary_key=True)
                  name = Column(String)

                  def __init__(self, name):
                      self.name = name

                  def __str__(self):
                      return self.name


              class Book(Base):
                  __tablename__ = "books"  # self.__table__ will be available for our objects

                  id = Column(Integer, primary_key=True)
                  title = Column(String)
                  description = Column(String)
                  author_id = Column(Integer, ForeignKey("authors.id"))
                  author = relationship(Author, backref=backref("books", order_by=title))

                  def __init__(self, title, description, author):
                      self.title = title
                      self.description = description
                      self.author = author

                  def __str__(self):
                      return self.title

              Base.metadata.create_all(engine)# create tables
            
          

Example (cont...)

Creating instances

            
              from sqlalchemy.orm import sessionmaker

              Session = sessionmaker(bind=engine)  # bound session
              session = Session()

              author_1 = Author("Richard Dawkins")
              author_2 = Author("Matt Ridley")

              book_1 = Book("The Red Queen", "A popular science book", author_2)
              book_2 = Book("The Selfish Gene", "A popular science book", author_1)
              book_3 = Book("The Blind Watchmaker", "The theory of evolutio", author_1)  # typo

              session.add(author_1)
              session.add(author_2)
              session.add(book_1)
              session.add(book_2)
              session.add(book_3)  # or simply session.add_all([author_1, author_2, book_1, book_2, book_3])

              # session.flush()
              session.commit()  # flushes (issues the statements and sends them to the RDBMS) and commits

              book_3.description = "The theory of evolution"  # update the object
              book_3 in session   # check whether the object is in the session
              # Out: True

              session.commit()
            
          

Example (cont...)

Queries

            
              session.query(Book).order_by(Book.id)  # returns a Query instance with a .statement attribute
              session.query(Book).order_by(Book.id).all()  # returns an object-list

              # return all book objects where title == "The Selfish Gene"
              session.query(Book).filter(Book.title == "The Selfish Gene").order_by(Book.id).all()

              # using LIKE
              session.query(Book).filter(Book.title.like("The%")).order_by(Book.id).all()

              query = session.query(Book).filter(Book.id == 9).order_by(Book.id)
              query.count()  # returns 0L
              query.all()  # returns an empty list
              query.first()  # returns None
              query.one()  # raises NoResultFound exception

              query = session.query(Book).filter(Book.id == 1).order_by(Book.id)
              book_1 = query.one()
              book_1.description  # returns "A popular science book"
              book_1.author.books  # returns a list of Book-objects representing all the books from the same author.

              # get a list of all Book-instances where the author"s name is "Richard Dawkins"
              session.query(Book).filter(Book.author_id == Author.id).filter(Author.name == "Richard Dawkins").all()
              session.query(Book).join(Author).filter(Author.name == "Richard Dawkins").all()
              session.query(Book).\
                  from_statement("SELECT b.* FROM books b, authors a WHERE b.author_id = a.id AND a.name=:name").\
                  params(name="Richard Dawkins").all()
            
          

... simply amazing!

Odin Data Access

https://gitlab.fifty.eu/odin/data-science/odin-data-access

Provides the odin_data_access module with some of the following entities:

  • session module for creating Session objects in a Fifty environment via get_session and get_session_from_env
  • DBBase - a tiny base-class that can be used as a mixin for mapping
  • DBTableAccessor - tiny wrapper for Session and Table as well a "facade" for Query and pandas

The DBTableAccessor:

  • represents a single RDBMS-table that is introspected during instance initialization (__init__)
  • uses a mix of ORM and SQL expression language functionality
  • contins a mapper for mapping custom classes to its corresponding Table
  • used by Batman and Chuck Norris (source: incomplete Google search)

Demo

Now watch this well-rehearsed demo...

Q & A