В этой небольшой статье я хочу дать ответ на вопрос, который возник у меня, когда я познакомился с сессиями в SQLAlchemy. Если сформулировать его кратко, то звучит он примерно так: “А зачем оно надо вообще”? Меня, как человека пришедшего из мира джанги, сессии приводили в уныние и я считал их откровенной фигней, которая усложняет жизнь. Но я ошибался. Как оказалось, сессии в алхимии - штука очень даже полезная. И вот почему.
Cессии являются неотъемлемой частью SQLAlchemy ORM и реализуют шаблоны Unit Of Work и Identity Map. Что это за шаблоны и зачем они нужны мы сейчас и разберем.
Unit Of Work (UoW, Единица работы)
Сессии в рамках этого паттерна отслеживают изменения, сделанные в рамках одной бизнес-транзакции, а затем “сбрасывают” их пачкой в базу, предварительно выполнив топологическую сортировку по зависимостям и сгруппировав повторяющиеся операции.
Чтобы понять зачем это надо, возьмем пару сниппетов с ActiveRecord ORM-ом и посмотрим какие проблемы там возникают и как сессии их решают.
class User(models.Model):
   username = models.CharField(max_length=255)
   name = models.CharField(max_length=255)
   last_name = models.CharField(max_length=255)
class PhoneNumber(models.Model):
   number = models.CharField(max_length=255)
   user = models.ForeignKey(User, on_delete=models.CASCADE)def process_users(users_records):
   for user_record in users_records:
       u = User(**user_record['user'])
       # Мы не сможем сохранить пользователя, если отсутствуют обязательные поля
       u.save()
       for entry in user_record['entries']:
           if entry['type'] == 'phone':
               p = PhoneNumber(user=u, number=entry['phone'])
               # Если мы не сохранили пользователя выше, то мы не сможем добавить телефон
               p.save()
           elif entry['type'] == 'fields':
               u.name = entry['name']
               u.last_name = entry['last_name']
               u.save()Какие проблемы мы здесь можем увидеть?:
- Мы отправляем множество мелких запросов, чем увеличиваем нагрузку на базу, т.к. при каждом вызове метода .save() ORM отправит в базе запрос INSERT/UPDATE. При вызове .delete() происходит то же самое, к слову. 
- Нам необходимо самим поддерживать правильный порядок запросов, что увеличивает сложность и может приводить к ошибкам. Мы не сможем, к примеру, создать пользователя, если у него не заполнены все обязательные поля, как не сможем записать телефон, если не смогли сохранить пользователя. Для решения этой проблемы мы можем держать объекты в памяти и отправлять запросы уже в самом конце, но в таком случае нам нужно отправлять запросы в правильном порядке и порядок этот поддерживать вручную. 
Теперь запишем все то же самое, но только с применением сессий:
class User(Base):
   __tablename__ = "users"
   id = Column(Integer, primary_key=True)
   username = Column(String(255))
   name = Column(String(255))
   last_name = Column(String(255))
   phones = relationship(
       "PhoneNumber", back_populates="user"
   )
class PhoneNumber(Base):
   __tablename__ = 'phone_numbers'
   id = Column(Integer, primary_key=True)
   number = Column(String(255))
   user_id = Column(Integer, ForeignKey(User.id))
   user = relationship(User, back_populates="phones")def process_users(users_records):
   with Session() as sess:
       for user_record in users_records:
           user = User(**user_record['user'])
           # Никаких запросов в базу не пойдет
           sess.add(user)
           for entry in user_record['entries']:
               if entry['type'] == 'phone':
                   phone = PhoneNumber(user=user, number=entry['phone'])
                   # Здесь также никаких запросов в базу не отправляется
                   sess.add(phone)
               elif entry['type'] == 'fields':
                   user.name = entry['name']
                   user.last_name = entry['last_name']
       # Здесь сессия откроет транзакцию, отправит запросы и выполнит commit
       sess.commit()Чем же этот вариант лучше?
- Во-первых сессия откроет транзакцию прозрачным для программиста образом перед самой отправкой запросов в базу, т.е. мы не держим транзакцию долго открытой. 
- Во-вторых, сессия сгруппирует операции обновления данных и мы избавимся от множества мелких запросов и снизим нагрузку на базу. 
- В-третьих, сессия за наc аггрегирует в памяти изменения и отправит запросы в правильном порядке, выполнив сортировку по зависимостям. 
Чтобы было нагляднее, приведу пример входных данных и sql, который прийдет для них в базу.
USERS_RECORDS = [
   {
       'user': {
           'username': 'donald',
           'name': 'Donald',
           'last_name': 'Duck'
       },
       'entries': [
           {
               'type': 'phone',
               'phone': '+7 941 234 43 45'
           }
       ]
   },
   {
       'user': {
           'username': 'bullwi',
           'name': 'Bullwinkle',
           'last_name': 'Moose'
       },
       'entries': [
           {
               'type': 'fields',
               'name': 'Rocky',
               'last_name': 'Squirrel'
           }
       ]
   }
]
process_users(USERS_RECORDS)BEGIN
INSERT INTO users (username, name, last_name) VALUES ('donald', 'Donald', 'Duck'),('bullwi', 'Rocky', 'Squirrel') RETURNING users.id
INSERT INTO phone_numbers (number, user_id) VALUES ('+7 941 234 43 45', 3) RETURNING phone_numbers.id
COMMITОбратите внимание на порядок запросов и их число. В частности сначала создаются пользователи вне зависимости от того, в каком порядке объекты создавались в приложении, а для создания пользователей нам потребовался всего один запрос.
Identity Map (IM, Карта идентичности)
Этот паттерн гарантирует, что объекты, загруженные из базы присутствуют в приложении только в одном экземпляре. Помимо гарантии уникальности сессия в рамках этого шаблона может сокращать число запросов к базе. Но не во всех случаях.
Рассмотрим пример.
u1 = User.objects.filter(username='donald').first()
u2 = User.objects.filter(username='donald').first()
u3 = User.objects.get(3)  # Donald
u4 = User.objects.filter(id=3).first()  # Donald
assert u1 is u2 is u3 is u4  # FailsВ случае ActiveRecord-а в базу уйдет 4 запроса на выборку и мы получим 4 разных объекта на уровне приложения. Это приводит опять же к тому, что:
- Нам нужно следить за порядком операций 
- Объекты могут содержать устаревшие данные 
def process_user_one():
   u = User.objects.get(3)  # Donald
   u.name = 'Don'
   return u
def process_user_two():
   u = User.objects.get(3)  # Donald
   if u.name == 'Don':
       p = PhoneNumber(user=u, number='+1 234 443 23 42')
       p.save()
   u.name = 'Donald'
   return u
user1 = process_user_one()
user1.save()
user2 = process_user_two()
user2.save()
assert user1.name == 'Donald'  # Fails, user1.name == ‘Don’
assert user2.name == 'Donald'Как видим, нам нужно позаботиться, чтобы user1 был сохранен в базу раньше вызова process_user_two(), в противном случае результат будет другим. Вторая же проблема - это устаревшие данные: user1 все еще зовут Don. В большом приложении это может стать источником неприятных ошибок.
В случае использования сессий обе эти проблемы будут решены, плюс число запросов к базе может сократиться.
u1 = session.query(User).filter_by(username='donald').one()
u2 = session.query(User).filter_by(username='donald').one()
u3 = session.query(User).get(3)  # Donald
u4 = session.query(User).filter_by(id=3).one()  # Donald
assert u1 is u2 is u3 is u4  # SuccessВ данном примере в базу уйдет только 3 запроса. Когда мы вызовем метод .get(), сессия возьмет нашего Дональда из своей карты объектов без доп. запроса.
def process_user_one(session):
   u = session.query(User).get(3)
   u.name = 'Don'
   return u
def process_user_two(session):
   u = session.query(User).get(3)
   if u.name == 'Don':
       p = PhoneNumber(user=u, number='+1 234 443 23 42')
       session.add(p)
   u.name = 'Donald'
   return u
with Session() as sess:
   user1 = process_user_one(sess)
   user2 = process_user_two(sess)
   assert user1.name == 'Donald'  # Success
   assert user2.name == 'Donald'  # SuccessЗдесь же нам не обязательно сохранять user1 в базу раньше времени и оба объекта содержат свежие значение.
 
           
 
danilovmy
Спасибо @romblin, а что насчет builk_create и atomic transaction, разве это не аналогичный функционал в Джанго?
Соответственно если во втором примере - создание телефона для Don в Джанго - сделать через один запрос Phone.filter(user_Name=Don).create(**kwargs) - то в чем преимущество сессии?
Спрашиваю,т.к. смотрел на алхимию из Джанго-орм но больших преимуществ не нашел.