Database & ORM

๐Ÿ“˜

db is an extension after 0.9 releases

By the help of community feedback, db is now a glim framework extension. Therefore, it is required to install glim-extensions package.

Glim framework has an extension to support rdb abstraction by its SQLAlchemy integration. SQLAlchemy is great a rdb abstraction kit with connection layer & object relational layer abstractions. In glim, the DB API won't require Orm features if it's not enabled. However, ORM is dependent on DB API naturally. It is highly suggested to read an introduction to sqlalchemy before going further.

So, let's make configuration!

Configuration & Connection Aliasing

After the 0.9.x releases, glim framework now supports sqlalchemy as an extension. Therefore, it is required to put db conifguration to the extensions key in config. An rdb configuration is quite simple in glim framework. Simply add these lines to enable db api & orm components;

# app/config/development.py
{
    # ...
  	'extensions': {
  
      'db': {
          'default': {
              'driver': 'mysql',
              'host': 'localhost',
              'schema': 'test',
              'user': 'root',
              'password': '',
        			'orm': True
          }
      },
    }
}

You might notice that we need a "default" key in this configuration. The reason why default exists is that glim can provide multiple database connections. The "default" key is here instructing glim to make database calls without having to give schema name. You can add more database connections by adding more keys inside the "db" dictionary;

# app/config/development.py
{
    # ...
    'extensions': {
        'db': {
            'default': {
                'driver': 'mysql',
              	'host': 'localhost',
              	'schema': 'test',
              	'user': 'root',
              	'password': '',
        				'orm': True
          	},
      			'analytics': {
        				'driver': 'mysql',
							  'host': 'localhost',
					 			'schema': 'analytics',
					 			'user': 'root',
					 			'password': '',
        				'orm': False
        		}
      	},
    }
}

Now, we have an aliased database connection called "analytics". An aliased connection could be reached in DB API like the following;

from glim_extensions.db import Database as DB
# returns the connection
DB.connection('analytics') # returns the "analytics" connection.
DB.connection() # returns the default connection

Defining Models & Mapping

Glim has a Model class inside glim.db module. The Model class is simply an alias of declarative_base() in SQLAlchemy. Defining a simple User model would be the following;

# app/models.py
from glim_extensions.db import Model
from sqlalchemy import Column, Integer, String

class User(Model):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(255))
    fullname = Column(String(255))
    password = Column(String(255))

This model maps to a users database table given the properties. As you noticed, the table name is mapped using __tablename__ variable. In other words, this table is mapped as if there exists a table with the following sql;

CREATE TABLE `users` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `fullname` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

Defining Relations

In glim, you can define model relations with SQLAlchemy's relationship() function. This section provides model relation examples.

One to One

SQLAlchemy defines one to one relations by the following statement;

"One To One is essentially a bidirectional relationship with a scalar attribute on both sides. To achieve this, the uselist flag indicates the placement of a scalar attribute instead of a collection on the โ€œmanyโ€ side of the relationship."

Consider a system where users having a phone number. In glim, you can define 2 tables namely "User" and "Phone" and provide a one to one relationship with the following;

# models.py
from glim_extentions.db import Model
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship, backref

class User(Model):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(255))
    fullname = Column(String(255))
    password = Column(String(255))
    phone = relationship("Phone", uselist=False, backref="phones")

class Phone(Model):
		__tablename__ = 'phones'
		id = Column(Integer, primary_key=True)
		user_id = Column(Integer, ForeignKey('users.id'))
		number = Column(String(12))

One to Many

SQLAlchemy defines one to many relationships by the following statement;

"A one to many relationship places a foreign key on the child table referencing the parent. relationship() is then specified on the parent, as referencing a collection of items represented by the child"

Consider a blog system where blog posts. This relation can be done in glim by the following;

# models.py
from glim_extentions.db import Model
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship, backref

class Post(Model):
		__tablename__ = 'posts'
		id = Column(Integer, primary_key=True)
		comments = relationship("Comment")

class Comment(Model):
		__tablename__ = 'comments'
		id = Column(Integer, primary_key=True)
		post_id = Column(Integer, ForeignKey('posts.id'))
		text = Column(Text(300))

You can achieve bidirectional relationship in Post using backref statement;

class Post(Model):
		__tablename__ = 'posts'
		id = Column(Integer, primary_key=True)
		comments = relationship("Comment", backref='comments')

This change will also provide a "reverse" type of one to many relationship namely "many to one".

Many to One

SQLAlchemy defines many to one relationships by the following statement;

"Many to one places a foreign key in the parent table referencing the child. relationship() is declared on the parent, where a new scalar-holding attribute will be created"

Many to One relationships can be achieved with backref attribute. Consider a system where users can join a group. In this system, users are limited to join to only one group.

# models.py
from glim_extentions.db import Model
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship, backref

class User(Model):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    group_id = Column(Integer, ForeignKey('group.id'))
    group = relationship("Group")

class Group(Model):
		__tablename__ = 'group'
		id = Column(Integer, primary_key=True)

You can achieve bidirectional relationship in User using backref statement;

class User(Model):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    group_id = Column(Integer, ForeignKey('group.id'))
    group = relationship("Group", backref="users")

Many to Many

SQLAlchemy defines many to many relationships by the following statement;

"Many to Many adds an association table between two classes. The association table is indicated by the secondary argument to relationship(). Usually, the Table uses the MetaData object associated with the declarative base class, so that the ForeignKey directives can locate the remote tables with which to link"

Consider a system where blog posts have tags. This can be done by the following;

# models.py
from glim_extentions.db import Model
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship, backref

class Post(Model):
		__tablename__ = 'posts'
		id = Column(Integer, primary_key=True)
  	tags = relationship('Tag', secondary=tags)

tags = Table('tags',
	Column('tag_id', Integer, ForeignKey('tag.id'),
	Column('post_id', Integer, ForeignKey('post_id'))))

class Tag(Model):
		id = Column(Integer, primary_key=True)

A bidirectional relationship could be established using backref in the Post model by the following;

class Post(Model):
		__tablename__ = 'posts'
  	id = Columtn(Integer, primary_key=True)
  	tags = relationship('Tag', secondary=tags, backref='tags')

Querying

After defining releations and model mappings, querying is the most important concept to learn. In glim, there are many ways to achieve querying. You can even write raw sql queries using the DB API. This section explains the orm querying part only.

Switching between connections

In glim, it is said above that you can do connection aliasing in the "default-analytics" connection. You can switch to "analytics" database connection by the following;

# app/config/<env>.py
config = { 
		'extensions': {
    		'db': {
            'default': {
                'driver': 'mysql',
              	'host': 'localhost',
              	'schema': 'test',
              	'user': 'root',
              	'password': '',
        				'orm': True
          	},
      			'analytics': {
        				'driver': 'mysql',
							  'host': 'localhost',
					 			'schema': 'analytics',
					 			'user': 'root',
					 			'password': '',
        				'orm': False
        		}
      	},
    }
}

from glim_extentions.db import Orm

Orm.connection('analytics').blah() # performs operations on the analytics database
Orm.blah() # performs operations on the default conncetion

In this example we can infer that Orm object holds the active session object of SQLAlchemy.

The query() function

The query() function takes model classes or model column names and configures itself. You can use method chaining here by the following;

from glim_extensions.db import Orm

# query from a class
Orm.query(User).filter_by(name='Aras').all()

# querying from a class with only one row
Orm.query(User).filter_by(name='Aras').first()

# query with multiple classes
Orm.query(User, Phone).join('phone').filter_by(name='Aras').all()

# query specific columns
Orm.query(User.name, User.id).all()

You can get much more information in the Querying section of SQLAlchemy.

Adding new items with orm session

You can create new models and persist in the database using add() & commit() function.

# services.py
from glim_extensions.db import Orm
from app.models import User

def add_users(user1_name, user2_name):
		user1 = User(name=user1_name)
  	user2 = User(name=user2_name)
  	Orm.add(user1)
  	Orm.add(user2)
  	Orm.commit() # write changes to the database

You can also add the users as a list by the following

Orm.add_all([user1, user2])

๐Ÿšง

Disabled autocommit

Currently, it is required to write commit() statements to persist models to database. Although it is highly suggested to make these database changes transactional, the autocommit mode will be enabled. This issue will be addressed on the future releases.

Deleting Items using orm session

You can filter and delete tuples by the following;

from glim_extensions.db import Orm

Orm.query(User).filter(id=7).delete()

Using DB API

The DB API includes the engine instance of active connection. You can use SQL Expression Language that SQLAlchemy provides. It is highly recommended to read this documentation first.

Running SQL Expression on models

The documentation above showed how to define models. SQL Expression language requires Table object to run chained sql statements. Consider an example of a system holding users in a model. The following example shows a simple select statemtent on defined model;

# models.py
from glim_extensions.db import Model
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship, backref

class User(Model):
		__tablename__ = 'users'
  	id = Column(Integer, primary_key=True)
  	name = Column(String(250))

# services.py
from app.models import User
from glim.db import Database as DB
from sqlalchemy.sql import select

def select_all_users():
		s = select([User.__table__])
  	result = DB.execute(s)
  	for row in result:
		    print row

You can notice that sql expression language can be run by using __table__ property of a model. You can fetch one row using fetchone() function;

# services.py
from glim_extensions.db import Database as DB
from app.models import User

# get rows using defined column names
def select_first_user():
		s = select([User.__table__])
  	result = DB.execute(s)
  	row = result.fetchone()
  	print("name:", row['name'], "; fullname:", row['fullname'])
  
# get rows using column name in __table__
def select_first_user2():
  	s = select([User.__table__])
  	result = DB.execute(s)
  	row = result.fetchone()
  	print("name:", row[User.__table__.c.name],
        	"; fullname:", row[User.__table__.c.fullname])

Insert Expressions

Consider the example of the user model above. You can achieve insert expressions by running Model.__table__ statement by the following;

# services.py
from app.models import User
from glim_extensions.db import Database as DB

def insert_user(name):
		ins = User.__table__
  						.insert().values(name='Aras')
 		result = DB.execute(ins)
  	return result

Select Expressions

You can run selects by chaining statements;

from glim_extensions.db import Database as DB
from app.models import User, Address

users = User.__table__
addresses = Address.__table__

# an example of chaining multiple tables
s = select([users, addresses])
			.where(users.c.id == addresses.c.user_id)
  
# an example of chaining more and or_ statements
s = select([users.c.fullname + ", " + addresses.c.email_address])
			.where(users.c.id == addresses.c.user_id)
  		.where(users.c.name.between('m', 'z'))
    	.where(
       	or_(
        	addresses.c.email_address.like('%@gmail.com')
         	addresses.c.email_address.like('%@yandex.com')
        ) 
      )
      
rows = DB.execute(s).fetchall()

Order By, Group By

from sqlalchemy import func, desc
from glim_extensions.db import Database as DB
from app.models import Address

addresses = Address.__table__

stmt = select([
	addresses.c.user_id,
	func.count(addresses.c.id).label('num_addresses')]).\
	order_by("num_addresses")

DB.execute(stmt).fetchall()

stmt = select([
				addresses.c.user_id,
				func.count(addresses.c.id).label('num_addresses')]).\
				order_by(desc("num_addresses"))
  
DB.execute(stmt).fetchall()

Update Expressions

You can run update statements using update() function by the following;

from glim_extensions.db import Database as DB
from app.models import User

users = User.__table__

stmt = users.update().\
				where(users.c.name == 'Aras').\
				values(fullname="Fullname: " + users.c.name)

result = DB.execute(stmt)

Delete Expressions

You can run delete statements using delete() function by the following;

from glim_extensions.db import Database as DB
from app.models import Address

addresses = Address.__table__
DB.execute(addresses.delete())

Raw Queries

There are many ways of running raw sql queries

from sqlalchemy.sql import text
from glim_extensions.db import Database as DB

s = text(
		"SELECT users.fullname || ', ' || addresses.email_address AS title "
		"FROM users, addresses "
		"WHERE users.id = addresses.user_id "
		"AND users.name BETWEEN :x AND :y "
		"AND (addresses.email_address LIKE :e1 "
		"OR addresses.email_address LIKE :e2)")

DB.execute(s, x='m', y='z',
           e1='%@gmail.com',
           e2='%@yandex.com')
	.fetchall()
  
sql = "INSERT INTO users (full_name, title)" \
      "VALUES ('%s','%s')" % (full_name, title))"

result = DB.execute(sql)
print result

Migrations

Migrations are currenlty available by RDB Migrations extension. You can check the docs about database migrations there.