Upload
it-people
View
1.025
Download
3
Embed Size (px)
Citation preview
Разработка одностраничных веб-‐приложений
с использованием PonyORM и ReactJS
Александр Козловский, Алексей МалашкевичPyCon Russia 2015
ЗадачаРазработать Single Page ApplicationПриложение -‐ библиотека современного искусства
ЗадачаРазработать Single Page ApplicationПриложение -‐ библиотека современного искусства
Особенности приложения: 1. Множество связанных объектов:
художники, картины, история продаж, выставки, галлереи, каталоги и т.д.
ЗадачаРазработать Single Page ApplicationПриложение -‐ библиотека современного искусства
Особенности приложения: 1. Множество связанных объектов:
художники, картины, история продаж, выставки, галлереи, каталоги и т.д.
2. Нужна мобильная версия приложения (native app)
ЗадачаРазработать Single Page ApplicationПриложение -‐ библиотека современного искусства
Особенности приложения: 1. Множество связанных объектов:
художники, картины, история продаж, выставки, галлереи, каталоги и т.д.
2. Нужна мобильная версия приложения (native app) 3. Разные уровни доступа: обычные пользователи, платные пользователи, художники, редакторы, работники галлерей и т.д.
1. Фронтенд. Современные фреймворки: • Backbone • AngularJS • KnockoutJS • EmberJS • ReactJS
Выбор технологий
1. Фронтенд. Современные фреймворки: • Backbone • AngularJS • KnockoutJS • EmberJS • ReactJS
!!2. Способ передачи данных между бэкендом и фронтендом • REST
Выбор технологий
1. Фронтенд. Современные фреймворки: • Backbone • AngularJS • KnockoutJS • EmberJS • ReactJS
!!2. Способ передачи данных между бэкендом и фронтендом • REST
!3. Бэкенд ✓ Язык -‐ Python ✓ База данных -‐ PostgreSQL
Выбор технологий
Проблемы
1. Дублирование моделей на фронтенде (уже есть на бэкенде)
2. REST может генерировать слишком много запросов к серверу
3. Нет двунаправленных связей между объектами на фронтенде (а на бэкенде есть)
Решения 1. Генерировать фронтенд
модели автоматически на основе моделей на бэкенде
!
1. Дублирование моделей на фронтенде (уже есть на бэкенде)
2. REST может генерировать слишком много запросов к серверу
3. Нет двунаправленных связей между объектами на фронтенде (а на бэкенде есть)
Решения 1. Генерировать фронтенд
модели автоматически на основе моделей на бэкенде
!2 и 3. Передавать связанные объекты в одном пакете
PonyJS
1. Дублирование моделей на фронтенде (уже есть на бэкенде)
2. REST может генерировать слишком много запросов к серверу
3. Нет двунаправленных связей между объектами на фронтенде (а на бэкенде есть)
PonyJS
PonyJS автоматически генерирует фронтенд модели на основе моделей PonyORM на бэкенде !
Почему PonyORM?
o1 = Order.objects.get(pk=1) print(o1.total, o1.customer.id) o2 = Order.objects.get(pk=2) print(o2.total, o2.customer.id)
Django ORM:
1. Iden~tyMap Customer 1
Customer 1
Order 1
Order 2
Order 1
Order 2
Customer 1
o1 = Order[1] print(o1.total, o1.customer.id) o2 = Order[2] print(o2.total, o2.customer.id)
PonyORM:
Ac~ve Record
Iden~ty Map
session.query(Product).filter( (Product.name.startswith('A') & (Product.image == None)) | (extract('year', Product.created_at) < 2015))
Почему PonyORM?
select(p for p in Product if p.name.startswith('A') and p.image is None or p.created_at.year < 2015) Pony
Product.objects.filter( Q(name__startswith='A', image__isnull=True) | Q(created_at__year__lt=2015)) Django
SQLAlchemy
2. Удобный язык запросов
База данных
Место Pony в архитектуре веб-‐приложения
PonyJS
Data Access
Business Logic
Service API
Приложение на сервере - сервис
Data Structures (ViewModels)
Business Logic
Presentation Layer
Браузер клиента
PonyORM
JSON JSONСтатический HTML
Общая схема взаимодействия Браузер Сервер
/artworks
HTML
/api/artworks
JSON
Первый запрос
AJAX запросы
class Author(db.En~ty): id = PrimaryKey(int, auto=True) name = Required(str) artworks = Set("Artwork") !
class Artwork(db.En~ty): id = PrimaryKey(int, auto=True) ~tle = Required(str) image = Required(str) author = Required(Author)
Описание сущностей
AJAX запрос с фронтендаБраузер Сервер
AJAX запрос /api/artworks
JSONpony.load({ url: '/api/artworks', success: funcDon(data) { !! } })
Обработчик на бэкендеБраузер Сервер
[email protected]('/api/artworks') @db_session def get_artworks(): artworks = Artwork.select().order_by(Artwork.id)[:3] print(artworks) # [Artwork[1], Artwork[2], Artwork[3]] return artworks.to_json()
AJAX запрос /api/artworks
Получаем список artworks на фронтенде
Браузер Сервер
AJAX запрос /api/artworks
JSON
pony.load({ url: '/api/artworks', success: funcDon(artworks) { console.log(artworks); } })
Получили на фронтенде тот же список
Список полноценных объектов сущностей
Атрибуты -‐ это getter&setters
Переход по связям объекта
class Author(db.En~ty): id = PrimaryKey(int, auto=True) name = Required(str) artworks = Set("Artwork") !
class Artwork(db.En~ty): id = PrimaryKey(int, auto=True) ~tle = Required(str) image = Required(str) author = Required(Author)
У объекта Artwork есть связь с Author
Добавляем объекты Author Браузер Сервер
[email protected]('/api/artworks') @db_session def get_artworks(): artworks = Artwork.select().order_by(Artwork.id)[:3] return artworks.to_json(include=[Artwork.author])
AJAX запрос /api/artworks
Получаем список artworks на фронтенде
Браузер Сервер
AJAX запрос /api/artworks
JSON
pony.load({ url: '/api/artworks', success: funcDon(artworks) { console.log(artworks); } })
Теперь мы видим автора
Как же он был передан?
user data
identity map
schema
Artwork[1] Artwork[2] Artwork[3]
Author[1] Author[2]
Формат JSON пакета
Оба artwork’а ссылаются на один и тот же объект Author
user data
identity map
schema
Artwork[1] Artwork[2] Artwork[3]
Author[1] Author[2]
Формат JSON пакета{ "data": […], !
"objects": {…}, !
!
"schema": [...] !
}
Пользовательские данные"data": [ { "pk": 1, "class": "Artwork" }, { "pk": 2, "class": "Artwork" }, { "pk": 3, "class": "Artwork" } ]
Iden~ty Map объектов"objects": { "Ar~st": { "1": {...}, "2": {...} }, "Artwork": { "1": {...}, "2": {...}, "3": {...} } }
Iden~ty Map объектов! "Author": { "1": { "id": 1, "name": "Author1" }, "2": { "id": 2, "name": "Author2" } },
! "Artwork": { "1": { "id": 1, "~tle": "Artwork1" "image": "/images/1.jpg", "author": 1, }, "2": { "id": 2, ... "author": 1, }, "3": { "id": 3 ... "author": 2, }
"objects": {
}
Схема объектов"schema": [ { "name": "Author", "newA�rs": [ … ], "pkA�rs": ["id"] }, { "name": "Artwork", "newA�rs": [ … ], "pkA�rs": ["id"] } ]
Схема объектов! { "name": "Author", "newA�rs": [ { "auto": true, "kind": "PrimaryKey", "name": "id", "type": "int" }, { "kind": "Required", "name": "name", "type": "unicode" }, { "kind": "Set", "name": "artworks", "reverse": "author", "type": "Artwork" } ], "pkA�rs": ["id"] },
{ "name": "Artwork", "newA�rs": [ { "auto": true, "kind": "PrimaryKey", "name": "id", "type": "int" }, { "kind": "Required", "name": "Dtle", "type": "unicode" }, { "kind": "Required", "name": "image", "type": "unicode" }, { "kind": "Required", "name": "author", "reverse": "artworks", "type": "Author" } ], "pkA�rs": ["id"] }
"schema": [
]
Подгрузка дополнительных объектовБраузер Сервер
JSON
AJAX запрос /api/artworks?page=2
полученные объекты будут смержены с Identity Map на фронтенде
Изменим атрибут title
Можно сохранять измененияБраузер Сервер
JSON
AJAX запрос /api/artworks
изменить создать удалить объекты
JSON
AJAX запрос /api/update
Теперь сохраним измененияБраузер Сервер
JSON
AJAX запрос /api/artworks
изменить создать удалить объекты
JSON
AJAX запрос /api/update
pony.save({ url: '/api/update', success: funcDon(data) { … } })
Передаваемый JSON{ "data": null, "objects": [ { "class": "Artwork", "_cid_": 3, "_status_": "u", "_pk_": "1", "~tle": { "old": "Artwork1", "new": "New Dtle" } } ]}
Обработчик на бэкендеБраузер Сервер
JSON
AJAX запрос /api/artworks
изменить создать удалить объекты
JSON
AJAX запрос /api/update
@app.route('/update', methods=['POST']) @db_session def update(): diff = request.form['diff'] db.from_json(diff) return db.to_json({'status': 'ok'})
А как же права доступа?
Права доступа
• Проверяются на сервере при выполненииto_json() и from_json()
• Позволяют спрятать из метаданных классы и атрибуты моделей, которыене имеет права видеть текущий пользователь
Права доступа
• Гарантируют, что на фронтенд передаютсятолько объекты, которые можно видеть(row-‐level permissions) и сериализуютсятолько атрибуты, которые можно видеть
• При создании и изменении объектовпроверяется что пользователь имеет правана создание/изменение конкретногоэкземпляра объекта и конкретных атрибутов
Способы задания прав доступа
• На уровне классов
• На уровне объектов (row-‐level permissions)
Pony предлагает очень удобный декларативный способ задания row-‐level permissions
На основе чего задаются права?
• Группы -‐ характеризуют текущего пользователя. Примеры: admin, editor, visitor
• Метки -‐ описывают свойства отдельных объектов. Примеры: public, deleted
• Роли -‐ описывают взаимоотношения пользователя и конкретного объекта. Примеры: owner, creator, moderator
Пример задания прав доступа
with db.permissions_for(Artwork): allow('view’, group='anybody') allow('create, edit', role='owner') allow('edit, delete', group='admin')
Порядок вычисления прав доступа• Пони запрашивает группы текущего пользователя и кеширует их
• Код приложения выбирает объекты запросом и вызывает to_json()
• Пони запрашивает роли текущего пользователя относительно сериализуемых объектов
• Код приложения вычисляет роли и метки и сообщает их Пони
• Пони проверяет роли на соответствие декларативно заданным правам доступа
Получение групп текущего пользователя
@groups_getter(Author) def get_author_groups(author): return ['author'] !
@groups_getter(User) def get_user_groups(user): return [user.type]
Использование групп для задания прав
with db.permissions_for(Artwork): allow('view', group='anybody') allow('create, edit', role='owner') allow('edit, delete', group='admin')
Получение ролей текущего пользователя
@roles_getter(Author, Artwork) def get_author_roles(author, artwork): if author is artwork.author: return ['owner']
Использование ролей для задания прав
with db.permissions_for(Artwork): allow('view', group='anybody') allow('create, edit', role='owner') allow('edit, delete', group='admin')
Проверка роли при создании объекта
with db.permissions_for(Artwork): allow('view', group='anybody') allow('create, edit', role='owner') allow('edit, delete', group=‘admin') !
Юзер при создании объекта не сможет указать в поле artwork.author другого юзера!
Пример использования меток
Допустим, что некоторые картины должны быть скрыты от обычных пользователей при показе на сайте
class Author(db.En~ty): id = PrimaryKey(int, auto=True) name = Required(str) artworks = Set("Artwork") !class Artwork(db.En~ty): id = PrimaryKey(int, auto=True) ~tle = Required(str) image = Required(str) author = Required(Author) hidden = Required(bool, default=False)
Пример использования меток
Получение меток объекта
@labels_getter(Artwork) def get_artwork_labels(artwork): if not artwork.hidden: return ['public']
Пример использования меток
with db.permissions_for(Artwork): allow('view', group='anybody', label='public') allow('create, edit', role='owner') allow('edit, delete', group='admin')
Автоматические фильтры запросов
Удобны, если мы хотим автоматически добавлять условие ко всем запросам для
определенного класса
Автоматические фильтры запросов
@default_filter(Artwork) def public(artwork): return not artwork.hidden
Автоматические фильтры запросов
artworks = Artwork.select( lambda a: a.author.name == 'Gerhard Richter' ).order_by(lambda a: a.title)[:3]
Автоматические фильтры запросовartworks = Artwork.select( lambda a: a.author.name == 'Gerhard Richter' ).order_by(lambda a: a.title)[:3] !SELECT "a"."id", "a"."title", "a"."image", "a"."hidden", "a"."author" FROM "Artwork" "a" INNER JOIN "Author" "author-1" ON "a"."author" = "author-1"."id" WHERE "author-1"."name" = 'Gerhard Richter' AND "a"."hidden" = 0 ORDER BY "a"."title" LIMIT 3
Отключение автоматических фильтров
with default_filters_disabled: artworks = Artwork.select( lambda a: a.author.name == 'Gerhard Richter' ).order_by(lambda a: a.title)[:3]
Отключение автоматических фильтровwith default_filters_disabled: artworks = Artwork.select( lambda a: a.author.name == 'Gerhard Richter' ).order_by(lambda a: a.title)[:3] !SELECT "a"."id", "a"."title", "a"."image", "a"."hidden", "a"."author" FROM "Artwork" "a" INNER JOIN "Author" "author-1" ON "a"."author" = "author-1"."id" WHERE "author-1"."name" = 'Gerhard Richter' ORDER BY "a"."title" LIMIT 3
Разные права для разных сайтов
with db.permissions_for(Artwork): allow('view', group='anybody', label='public', site='public') allow('create, edit', role='owner', site='admin') allow('edit, delete', group='admin', site='admin')
ReactJS• ReactJS позволяет строить страницу из компонентов
• Компоненты представляют собой слой View
• Получают свойства (props) и используют их при рендеринге
• Объекты PonyJS можно использоватьв качестве значений свойств компонентов
Пример React-‐компонентаvar ArtworkDescription = React.createClass({ render: function () { var artwork = this.props.artwork; return <div> <h2>{ artwork.title() }</h2> <img src={ artwork.image() } /> <p>{ artwork.author().name() }</p> </div> } });
Пример React-‐компонентаvar ArtworksPage = React.createClass({ render: function () { var artworkList = … return <div> <h1>Artwork list</h1> <ul>{ artworkList }</ul> </div> } });
Фрагменты кода с ReactJS
var artworkList = _.map( this.props.artworks, function (item) { return <li key={ item.id() }> <ArtworkDescription artwork={ item } /> </li> });
Фрагменты кода с ReactJSvar ArtworksPage = React.createClass({ render: function () { var artworkList = _.map( this.props.artworks, function (item) { return <li key={ item.id() }> <ArtworkDescription artwork={ item } /> </li> }); return <div> <h1>Artwork list</h1> <ul>{ artworkList }</ul> </div> } });
Фрагменты кода с ReactJSvar ArtworksPage = React.createClass({ render: function () { return <div> <h1>Artwork list</h1> <ul>{ _.map(this.props.artworks, function (item) { return <li key={ item.id() }> <ArtworkDescription artwork={ item } /> </li> }); }</ul> </div> } });
Структура SPA на React
• Автогенерация моделей на фронтенде • Сериализация произвольных данных, включая двусторонние связи many-‐to-‐many и инстансы сущностей
• Объекты с двунаправленными связями на клиенте
• Двунаправленный биндинг • Передача изменений обратно на сервер • Права доступа
Заключение. Что предлагает PonyJS?
Спасибо!Q&A
Site: ponyorm.com Twi�er: @ponyorm Github: github.com/ponyorm/pony
Александр Козловский, Алексей МалашкевичPyCon Russia 2015
Чем PonyORM отличается от Django ORM
• Pony использует паттерн Iden~ty Map, Django -‐ Ac~veRecord • Позволяет избегать lost updates • Не растрачивает память • В связанных объектах на других концах -‐ не копии, а единственный экземляр объекта
• Оптимизация запросов (подзапрос в LEFT JOIN) • Можно работать с Пони в интерактивном режиме • Решение проблемы “N+1 запроса” • Оптимистические проверки • Умный реконнект • Информативные сообщения об ошибках
Чем PonyORM отличается от Django ORM
• Идентация SQL запросов в логе • Атрибут лифтинг (multisets) • Лаконичный декларативный язык запросов
• Генераторы и лямбды • Агрегирующие функции самого языка (sum, min, max) • in используется для подзапросов и LIKE, транслируется в разный SQL в зависимости от аргументов
• Raw SQL • Подстановка параметров в запрос • Множественное наследование сущностей • Отсутствие проблемы “срез базового класса”
Чем PonyORM отличается от Django ORM
• Поддержка пула соединений • Кеширование результата транслирования запроса, результата запроса (список объектов), и сами объекты. Второй селект с такими же параметрами в базу не пойдет (если мы не посылали в базу апдейты, если посылали -‐ этот кеш сбросится)
• PonyJS
<body> <table> <caption>Products</caption> <tbody data-‐bind="foreach: products"> <tr> <td><input data-‐bind="value: name"></td> <ul data-‐bind="foreach: categories"> <li data-‐bind="text: name"></li> </ul> </td> </tr> </tbody> </table> </body>
Двунаправленный биндинг
Двунаправленный биндинг
Other Pony ORM features
• Identity Map • Automatic query optimization • N+1 Query Problem solution • Optimistic transactions • ER Diagram Editor
Django ORM
s1 = Student.objects.get(pk=123) print s1.name, s1.group.id s2 = Student.objects.get(pk=456) print s2.name, s2.group.id !
• How many SQL queries will be executed? • How many objects will be created?
Django ORM
s1 = Student.objects.get(pk=123) print s1.name, s1.group.id s2 = Student.objects.get(pk=456) print s2.name, s2.group.id
Student 123
Django ORM
s1 = Student.objects.get(pk=123) print s1.name, s1.group.id s2 = Student.objects.get(pk=456) print s2.name, s2.group.id
Student 123 Group 1
Django ORM
s1 = Student.objects.get(pk=123) print s1.name, s1.group.id s2 = Student.objects.get(pk=456) print s2.name, s2.group.id
Student 123
Student 456
Group 1
Django ORM
s1 = Student.objects.get(pk=123) print s1.name, s1.group.id s2 = Student.objects.get(pk=456) print s2.name, s2.group.id
Student 123
Student 456
Group 1
Group 1
Pony ORM
s1 = Student[123] print s1.name, s1.group.id s2 = Student[456] print s2.name, s2.group.id
Pony ORM – seeds, IdentityMap
s1 = Student[123] print s1.name, s1.group.id s2 = Student[456] print s2.name, s2.group.id
Student 123
Group 1
Pony ORM – seeds, IdentityMap
s1 = Student[123] print s1.name, s1.group.id s2 = Student[456] print s2.name, s2.group.id
Student 123
Group 1
seed
Pony ORM – seeds, IdentityMap
s1 = Student[123] print s1.name, s1.group.id s2 = Student[456] print s2.name, s2.group.id
Student 123
Group 1
seed
Pony ORM – seeds, IdentityMap
s1 = Student[123] print s1.name, s1.group.id s2 = Student[456] print s2.name, s2.group.id
Student 123
Student 456
Group 1
seed
Pony ORM – seeds, IdentityMap
s1 = Student[123] print s1.name, s1.group.id s2 = Student[456] print s2.name, s2.group.id
Student 123
Student 456
Group 1
seed
Solution for the N+1 Query Problem
orders = select(o for o in Order if o.total_price > 1000)\ .order_by(Order.id)[0:5]
for o in orders: print o.total_price, o.customer.name
SELECT o.id, o.total_price, o.customer_id,... FROM "Order" o WHERE o.total_price > 1000 ORDER BY o.id LIMIT 5
Order 1
Order 3
Order 4
Order 7
Order 9
Customer 1
Customer 4
Customer 7
Solution for the N+1 Query Problem
orders = select(o for o in Order if o.price > 1000) for o in orders: print o.total_price, o.customer.name !
SELECT c.id, c.name, … FROM “Customer” c WHERE c.id IN (?, ?, ?)
Solution for the N+1 Query Problem
Order 1
Order 3
Order 4
Order 7
Order 9
Customer 1
Customer 4
Customer 7
Solution for the N+1 Query Problem
Transactions
!
def transfer_money(id1, id2, amount): account1 = Account.objects.get(pk=id1) if account1.amount < amount: raise ValueError('Not enough funds!') account2 = Account.object.get(pk=id2) account1.amount -‐= amount account1.save() account2.amount += amount account2.save()
Django ORM
@transaction.atomic def transfer_money(id1, id2, amount): account1 = Account.objects.get(pk=id1) if account1.amount < amount: raise ValueError('Not enough funds!') account2 = Account.object.get(pk=id2) account1.amount -‐= amount account1.save() account2.amount += amount account2.save()
TransactionsDjango ORM
@transaction.atomic def transfer_money(id1, id2, amount): account1 = Account.objects. \ select_for_update.get(pk=id1) if account1.amount < amount: raise ValueError('Not enough funds!') account2 = Account.objects. \ select_for_update.get(pk=id2) account1.amount -‐= amount account1.save() account2.amount += amount account2.save()
TransactionsDjango ORM
@db_session def transfer_money(id1, id2, amount): account1 = Account.get_for_update(id=id1) if account1.amount < amount: raise ValueError('Not enough funds!') account1.amount -‐= amount account2 = Account.get_for_update(id=id2) account2.amount += amount
TransactionsPony ORM
@db_session def transfer_money(id1, id2, amount): account1 = Account[id1] if account1.amount < amount: raise ValueError('Not enough funds!') account1.amount -‐= amount Account[id2].amount += amount
TransactionsPony ORM
db_session
• Pony tracks which objects where changed • No need to call save() • Pony saves all updated objects in a single
transaction automatically on leaving the db_session scope
Transactions
@db_session def transfer_money(id1, id2, amount): account1 = Account[id1] if account1.amount < amount: raise ValueError('Not enough funds!') account1.amount -‐= amount Account[id2].amount += amount
TransactionsPony ORM
UPDATE Account SET amount = :new_value WHERE id = :id AND amount = :old_value
Optimistic Locking
Optimistic Locking
• Pony tracks which attributes were read and updated
• If object wasn’t locked using the for_update method, Pony uses the optimistic locking automatically