Создаем сокращенные ссылки с помощью SQLAlchemy и PosgreSQL
Я хотел сохранить внешний URL проекта в виде короткой ссылки (немного похожей на битовую), чтобы вредоносный URL мог глобально блокироваться в системе. Чтобы избежать угадывания URL-адреса, я использовал hashids, который генерируют короткие уникальные непоследовательные идентификаторы из чисел. С помощью хешей я могу легко преобразовать идентификатор первичного ключа в альтернативный идентификатор, который может быть предоставлен.
http://mydomain.com/AW3d -> http://google.com (301 Moved Permanently to http://google.com)
Чтобы проиллюстрировать нашу проблему, у нас есть модель под названием Link
. Эта модель используется для хранения URL-адреса и соответствующего хеш-кода для короткого URL-адреса.
from sqlalchemy.ext.declarative import declared_attr, as_declarative from sqlalchemy import Unicode, String, Column, Integer@as_declarative class Base(object): @declared_attr def __tablename__(cls): return cls.__name__.lower() id = Column(Integer, primary_key=True) engine = create_engine('postgresql://test@/test') DBSession = scoped_session(sessionmaker(bind=engine)) Base.metadata.bind = engine class Link(Base): url = Column('url', Unicode, index=True, unique=True, nullable=False) hashid = Column('url', String, index=True, unique=True, nullable=False)
Даже если блок транзакции защитит нас от случайной вставки из-за ошибки приложения на полпути. Я хочу, чтобы избежать обнуляемого столбца, если я могу сохранить целостность нашей базы данных. В некоторых случаях наличие значения NULL может иметь смысл, но нам не нужен объект ссылки без хеша.
Чтобы иметь возможность создать хеш-код перед вставкой, нам нужно знать, какой будет основной идентификатор. К счастью, в PostgreSQL есть последовательности, и когда вы определяете столбец Integer первичного ключа, SQLAlchemy автоматически использует тип SERIAL. SERIAL - это не настоящие типы, а удобство записи для создания столбцов с уникальным идентификатором. За сценой Postgres создает непустой столбец Integer со значением по умолчанию, равным следующему значению последовательности.
CREATE TABLE tablename ( colname SERIAL );
эквивалентно записи:
CREATE SEQUENCE tablename_colname_seq; CREATE TABLE tablename ( colname integer NOT NULL DEFAULT nextval('tablename_colname_seq') ); ALTER SEQUENCE tablename_colname_seq OWNED BY tablename.colname;
Давайте сначала посмотрим, решение, если бы мы позволили NULL для столбца хэш Функция для создания объекта Link может выглядеть следующим образом:
from hashids import Hashids def create_link(url): hashids = Hashids() link = Link( id=nextid, url=url, ) DBSession.add(link) DBSession.flush(link) link.hashid = hashids.encode(link.id) DBSession.commit(link) return link
В нашем случае мы решили не допускать NULL в столбце hashid, поэтому приведенный выше код даст нам ошибку нарушения ограничения not-null.
Тип SERIAL создал последовательность формата tablename_columm_seq
и, используя последовательность, мы можем предварительно выбрать id. Переписанная функция для создания объекта Link с использованием sequence может выглядеть следующим образом:
from hashids import Hashids from sqlalchemy.schema import Sequence def create_link(url): hashids = Hashids() nextid = DBSession.execute(Sequence("link_id_seq")) hashid = hashids.encode(nextid) link = Link( id=nextid, url=url, hashid=hashid ) DBSession.add(link) DBSession.commit(link) return link
Поскольку последовательности не являются транзакционными, проблем с предварительной загрузкой идентификатора из него не возникает. Я не знаю ни о каких проблемах, которые могли бы случиться.
Я надеюсь, что этот пост может помочь, если вы столкнетесь с подобным сценарием использования, я могу представить себе ситуацию, когда я хотел узнать идентификатор, прежде чем вставить свою запись.