Русский
Русский
English
Статистика
Реклама

Orm

Обзор Prisma ORM

29.04.2021 10:23:13 | Автор: admin


Это статья-обзор о Prisma ORM.

ORM (англ. Object-Relational Mapping объектно-реляционное отображение или преобразование) технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая виртуальную объектную базу данных.

Работа с базами данных (моделирование данных, изменение схем, формирование запросов и т.п.) одна из наиболее сложных задач, возникающих при разработке приложений. Prisma предлагает решение, позволяющее сосредоточиться на данных вместо SQL.

Что такое Prisma?


Как утверждают разработчики, Prisma представляет собой открытую ORM нового поколения для Node.js и TypeScript, реализующую новую парадигму объектно-реляционного отображения.

Поддерживаемые языки программирования:

  • JavaScript
  • TypeScript
  • Go (в разработке)

Поддерживаемые базы данных:

  • MySQL
  • PostgreSQL
  • SQLite
  • MSSQL (в разработке)
  • MongoDB Connector (в разработке)

Предоставляемые инструменты:

  • Prisma Client: автоматически генерируемый и типобезопасный клиент для БД
  • Prisma Migrate: декларативное моделирование данных и настраиваемые миграции
  • Prisma Studio: современный пользовательский интерфейс для просмотра и редактирования данных
  • Prisma VSCode Extension: расширение для VSCode, обеспечивающее подсветку синтаксиса, автозавершение, быстрые исправления и др.

Prisma Client может быть использован в любом Node.js или TypeScript серверном приложении. Это может быть REST API, GraphQL API, gRPC API и т.д.

Как Prisma работает?


Все начинается с определения схемы. Схема позволяет разработчикам определять модели с помощью интуитивно понятного языка. Она также содержит соединение с БД и определяет генератор:

datasource db {

provider = postgresql

url = env(DATABASE_URL)

}

generator client {

provider = prisma-client-js

}

model Post {

id Int @id @default(autoincrement())

title String

content String?

published Boolean @default(false)

author User? @relation(fields: [authorId], references: [id])

authorId Int?

}

model User {

id Int @id @default(autoincrement())

email String @unique

name String?

posts Post[]

}


Каждая модель привязана к таблице в БД и является основой для генерируемого Prisma Client интерфейса доступа к данным.

В приведенной схеме мы настраиваем следующее:


Модель данных это коллекция моделей. Главными задачами моделей является следующее:

  • Представление таблицы в БД
  • Предоставление основы для запросов Prisma Client

Для использования Prisma Client прежде всего необходимо установить соответствующий пакет из npm:

yarn add @prisma/client

# или

npm i @prisma/client


Установка данного пакета вызывает команду prisma generate, которая читает схему и генерирует код клиента. После генерации клиента, мы можем импортировать его в наш код и использовать для отправки запросов:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()


Миграции


Prisma Migrate преобразует схему в SQL для создания/изменения/удаления таблиц в БД. Миграция выполняется с помощью команды prisma migrate, предоставляемой Prisma CLI. Вот как выглядит SQL для приведенных выше моделей (SQLite):

CREATE TABLE Post (

id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,

title TEXT NOT NULL,

content TEXT,

published BOOLEAN NOT NULL DEFAULT false,

authorId INTEGER,

FOREIGN KEY (authorId) REFERENCES User (id) ON DELETE SET NULL ON UPDATE CASCADE

);

CREATE TABLE User (

id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,

email TEXT NOT NULL,

name TEXT

);

CREATE UNIQUE INDEX User.email_unique ON User(email);


Доступ к данным


Prisma Client позволяет разработчикам мыслить в категориях объектов. Другими словами, вместо концепции экземпляров модели, в ответ на запрос к БД возвращаются обычные JavaScript-объекты. Кроме того, запросы являются полностью типизированными. Рассмотрим несколько примеров:

// Получение всех постов

const posts = await prisma.post.findMany()

// Получение всех постов и их авторов

const postsWithAuthors = await prisma.post.findMany({

include: { author: true }

})

// Создание нового пользователя с новым постом

const userWithPosts = await prisma.user.create({

data: {

email: 'john@mail.com',

name: 'John Smith',

posts: {

create: [{ title: 'Hello World' }]

}

}

})

// Получение всех пользователей с именем John

const users = await prisma.user.findMany({

where: {

name: { contains: 'John' }

}

})

// Получение всех постов определенного пользователя

const postsByUser = await prisma.user.findUnique({

where: { email: 'john@mail.com' }

}).posts()

// Пагинация

const posts = await prisma.post.findMany({

take: 5,

cursor: { id: '3' }

})


С полным описанием API можно ознакомиться здесь.

Быстрый старт


В данном разделе мы научимся отправлять запросы к базе данных SQLite на TypeScript с помощью Prisma Client.

Загрузка начального проекта и установка зависимостей


Копируем репозиторий с начальным проектом (на самом деле в данном репозитории находится 2 проекта, один на JavaScript, другой на TypeScript; разница между ними невелика):

git clone https://github.com/prisma/quickstart.git

Переходим в директорию, с которой мы будем работать, устанавливаем зависимости и открываем директорию в редакторе кода:

# Переключаем рабочую директорию

cd quickstart/typescript/starter

# Устанавливаем зависимости

yarn

# или

npm i

# Открываем директорию в редакторе кода

code .


Проект состоит из 6 файлов:

  • package.json: определяет настройки проекта (название, описание, зависимости, команды и т.д.)
  • prisma/schema.prisma: схема, в которой, в том числе, определяются наши модели
  • prisma/.env: определяет URL для соединения с базой данных в качестве переменной среды окружения
  • prisma/dev.db: файл БД SQLite
  • script.ts: исполняемый скрипт TypeScript
  • tsconfig.json: настройки компилятора TypeScript

Зависимости проекта:

  • prisma: Prisma CLI, который можно вызывать с помощью npx prisma
  • @prisma/client: Prisma Client для доступа к БД
  • typescript: набор инструментов TypeScript
  • ts-node: используется для запуска скрипта TypeScript

Файл prisma/dev.db содержит две таблицы с фиктивными данными:

User
id email name
1 sarah@prisma.io Sarah
2 maria@prisma.io Maria

Post
id title content published authorId
1 Hello World null false 2

Обратите внимание: колонка authorId содержит ссылку на таблицу User, т.е. 2 в колонке authorId таблицы Post это ссылка на 2 в колонке id таблицы User.

Формирование запроса


Перед тем, как писать запрос к БД с помощью Prisma Client, взглянем на нашу схему:

datasource db {

provider = sqlite

url = env(DATABASE_URL)

}

generator client {

provider = prisma-client-js

}

model Post {

id Int @id @default(autoincrement())

title String

content String?

published Boolean @default(false)

author User? @relation(fields: [authorId], references: [id])

authorId Int?

}

model User {

id Int @id @default(autoincrement())

email String @unique

name String?

posts Post[]

}


Файл script.ts на данном этапе выглядит следующим образом:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {

// здесь будут находиться наши запросы

}

main()

.catch(e => {

throw e

})

.finally(async () => {

await prisma.$disconnect()

})


Начнем с запроса на получение всех пользователей:
async function main() {

const allUsers = await prisma.user.findMany()

console.log(allUsers)

}


Выполним этот код с помощью следующей команды:

yarn dev

# или

npm run dev


Вот что должно появиться в терминале:

[

{ id: 1, email: 'sarah@prisma.io', name: 'Sarah' },

{ id: 2, email: 'maria@prisma.io', name: 'Maria' },

]


Одной из наиболее важных возможностей, предоставляемых Prisma Client, является обеспечение легкой работы с отношениями между данными. Для получения постов определенного пользователя достаточно применить настройку include. Изменим код функции main():

async function main() {

const allUsers = await prisma.user.findMany({

include: { posts: true }

})

// Используем `console.dir` для правильного отображения вложенных объектов

console.dir(allUsers, { depth: null })

}


Выполняем код:

yarn dev

# или

npm run dev


Каждый объект теперь содержит свойство posts, представляющее данные, полученные благодаря наличию связи между таблицами User и Post:

[

{ id: 1, email: 'sarah@prisma.io', name: 'Sarah', posts: [] },

{

id: 2,

email: 'maria@prisma.io',

name: 'Maria',

posts: [

{

id: 1,

title: 'Hello World',

content: null,

published: false,

authorId: 2,

}

]

}

]


Обратите внимание, что переменная allUsers является строго типизированной благодаря типам, автоматически генерируемым Prisma Client. В этом можно убедиться, если навести курсор на allUsers в редакторе кода (VSCode или любом другом со встроенной поддержкой TypeScript):

const allUsers: (User & {

posts: Post[]

})[]

export type Post = {

id: number

title: string

content: string | null

published: boolean

authorId: number | null

}


Запись данных


Запрос findMany используется для чтения данных из БД. Для записи данных используется запрос create:

async function main() {

// Создаем пост

const post = await prisma.post.create({

data: {

title: 'Prisma облегчает работу с БД',

author: {

// Подключаем пост к записи в таблице `User`

connect: { email: 'sarah@prisma.io' }

}

}

})

console.log(post)

const allUsers = await prisma.user.findMany({

include: { posts: true }

})

console.dir(allUsers, { depth: null })

}


Запускаем код:

yarn dev

# или

npm run dev



Вывод:

{

id: 2,

title: 'Prisma облегчает работу с БД',

content: null,

published: false,

authorId: 1

}

[

{

id: 1,

email: 'sarah@prisma.io',

name: 'Sarah',

posts: [

{

id: 2,

title: 'Prisma облегчает работу с БД',

content: null,

published: false,

authorId: 1

}

]

},

{

id: 2,

email: 'maria@prisma.io',

name: 'Maria',

posts: [

{

id: 1,

title: 'Hello World',

content: null,

published: false,

authorId: 2

}

]

}

]


Наш запрос добавил новую запись в таблицу Post:

Post
id title content published authorId
1 Hello World null false 2
2 Prisma облегчает работу с БД null false 1

Давайте опубликуем созданный пост с помощью запроса update:

async function main() {

const post = await prisma.post.update({

where: { id: 2 },

data: { published: true }

})

console.log(post)

}


Запускаем код:

yarn dev

# или

npm run dev


Вывод:

{

id: 2,

title: 'Prisma облегчает работу с БД',

content: null,

published: true,

authorId: 1

}


Наш запрос обновил соответствующую запись в таблице Post:

Post
id title content published authorId
1 Hello World null false 2
2 Prisma облегчает работу с БД null true 1

Изменение схемы


Сначала добавим новую модель (Profile) в нашу схему:

model Post {

id Int @id @default(autoincrement())

title String

content String?

published Boolean @default(false)

author User? @relation(fields: [authorId], references: [id])

authorId Int?

}

model User {

id Int @id @default(autoincrement())

email String @unique

name String?

posts Post[]

profile Profile?

}

model Profile {

id Int @id @default(autoincrement())

bio String

user User @relation(fields: [userId], references: [id])

userId Int @unique

}


Обратите внимание, что мы также добавили новое поле в модель User.

Затем выполняем следующую команду:

npx prisma migrate dev --name add-profile

После этого у нас появляется возможность запрашивать данные из таблицы Profile, например, с помощью prisma.profile.findMany().

Express REST API


В данном разделе мы с нуля реализуем REST API с помощью Prisma Client, Express и TypeScript.

REST (от англ. Representational State Transfer передача состояния представления) архитектурный стиль взаимодействия компонентов распределенного приложения в сети.

Инициализация проекта и установка зависимостей


Создаем директорию проекта, инициализируем проект и устанавливаем зависимости:

# создание директории

mkdir prisma-express

cd !$

# инициализация проекта

yarn init -yp

# или

npm init -y

# установка основных зависимостей

yarn add @prisma/client express

# или

npm i @prisma/client express

# установка зависимостей для разработки

yarn add -D prisma typescript ts-node @types/express @types/node

# или

npm i -D prisma typescript ts-node @types/express @types/node


Структура нашего проекта будет следующей:

- prisma

- schema.prisma схема и модели Prisma

- seed.ts скрипт для наполнения БД фиктивными данными

- index.ts основной файл приложения

- package.json

- tscongig.json настройки TypeScript


Содержание package.json:

{

name: prisma-express,

version: 1.0.0,

license: MIT,

scripts: {

dev: ts-node src/index.ts

},

dependencies: {

@prisma/client: 2.21.2,

express: 4.17.1

},

devDependencies: {

prisma: 2.21.2,

@types/express: 4.17.11,

@types/node: 12.20.10,

ts-node: 9.1.1,

typescript: 4.2.4

}

}


Содержание tsconfig.json:

{

compilerOptions: {

sourceMap: true,

outDir: dist,

strict: true,

lib: [esnext],

esModuleInterop: true

}

}


Схема и модели


Наша схема будет состоять из 2 моделей User и Post:

// prisma/schema.prisma

generator client {

provider = prisma-client-js

}

datasource db {

provider = sqlite

url = file:./dev.db

}

model User {

id Int @id @default(autoincrement())

email String @unique

name String?

posts Post[]

}

model Post {

id Int @id @default(autoincrement())

createdAt DateTime @default(now())

updatedAt DateTime @updatedAt

title String

content String?

published Boolean @default(false)

viewCount Int @default(0)

author User? @relation(fields: [authorId], references: [id])

authorId Int?

}


Как видите, мы снова испольуем SQLite в качестве БД, модель User не изменилась, а у модели Post появилось несколько дополнительных полей.

Файл prisma/seed.ts будет использоваться для наполнения БД фиктивными данными. Вставьте в него следующий код:

import { PrismaClient, Prisma } from '@prisma/client'

const prisma = new PrismaClient()

const userData: Prisma.UserCreateInput[] = [

{

name: 'John',

email: 'john@mail.com',

posts: {

create: [

{

title: 'Title1',

content: 'Some text',

published: true,

},

],

},

},

{

name: 'Jane',

email: 'jane@mail.com',

posts: {

create: [

{

title: 'Title2',

content: 'Another text',

published: true,

},

],

},

},

{

name: 'Alice',

email: 'alice@mail.com',

posts: {

create: [

{

title: 'Title3',

content: 'And another',

published: true,

},

{

title: 'Title4',

content: 'And another once again',

},

],

},

},

]

async function main() {

console.log(`Наполнение БД фиктивными данными...`)

for (const u of userData) {

const user = await prisma.user.create({

data: u,

})

console.log(`Пользователь с id ${user.id} успешно создан`)

}

console.log(`Наполнение БД данными закончено.`)

}

main()

.catch((e) => {

console.error(e)

process.exit(1)

})

.finally(async () => {

await prisma.$disconnect()

})


В данном скрипте мы просто перебираем массив userData и создаем пользователей с помощью запроса create.

Прежде чем мы перейдем к непосредственной реализации REST API, создадим БД и наполним ее данными.

Для создания БД выполняем следующую команду:

npx prisma migrate dev --name init


Запускам выполнение скрипта из prisma/seed.ts:

npx prisma db seed --preview-feature


Теперь наша БД готова к использованию.

REST API


Реализуем минимальный сервер с помощью Express:

// index.ts

// Импортируем Express

import express from 'express'

// Создаем экземпляр приложения

const app = express()

// Подключаем посредника (middleware) для разбора JSON и помещение данных в req.body

app.use(express.json())

// Запускаем сервер на порту 3000

app.listen(3000, () => console.log(` Сервер запущен по адресу: http://localhost:3000`))


Далее импортируем Prisma Client и создаем экземпляр клиента:

import express from 'express'

// Импортируем Prisma Client и типы для TypeScript

import { Prisma, PrismaClient } from '@prisma/client'

const app = express()

// Создаем экземпляр клиента

const prisma = new PrismaClient()


Теперь определимся с конечными точками (endpoints), которые будут нужны нашему приложению:

`GET`

- `/users`: получение всех пользователей

- `/post/:id`: получение поста по его `id`

- `/user/:id/drafts`: получение черновиков (неопубликованных постов, постов, значением свойства `published` которых является `false`) определенного пользователя

- `/feed?searchString={searchString}&take={take}&skip={skip}&orderBy={orderBy}`: получение всех опубликованных постов

- параметры строки запроса (все параметры являются опциональными):

- `searchString`: фильтрация постов по заголовку (`title`) или содержанию (`content`)

- `take`: количество возвращаемых объектов

- `skip`: количество объектов, которые должны быть пропущены

- `orderBy`: порядок сортировки по возврастанию или по убыванию. Возможные значения: `asc` или `desc`

`POST`

- `/post`: создание нового поста

- тело запроса:

- `title: String` заголовок поста (обязательно)

- `content: String` содержание поста (опционально)

- `authorEmail: String`: адрес электронной почты автора поста (обязательно)

- `/signup`: создание нового пользователя

- тело запроса:

- `email: String`: email пользователя (обязательно)

- `name: String`: имя пользователя (опционально)

- `postData: PostCreateInput[]`: посты, принадлежащие пользователю (опционально)

`PUT`

- `publish/:id`: публикация определенного поста (установка его свойства `published` в значение `true`)

- `/post/:id/views`: увеличение количества просмотров определенного поста

`DELETE`

- `/post/:id`: удаление определенного поста


Начнем с GET-запросов.

Получение всех пользователей:

app.get('/users', async (req, res) => {

// Для получение всех объектов определенной модели используется запрос `findMany()`

const users = await prisma.user.findMany()

// Возвращаем ответ в формате JSON

res.json(users)

})


Получение определенного поста:

app.get(`/post/:id`, async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

// Для получения определенного (уникального) объекта модели используется запрос `findUnique()`

// с настройкой `where` (где)

// Обратите внимание, что `id` имеет тип `Int` (число),

// поэтому мы выполняем преобразование типа с помощью фукнции `Number()`

const post = await prisma.post.findUnique({

where: { id: Number(id) },

})

res.json(post)

})



Получение черновиков пользователя:

app.get('/user/:id/drafts', async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

// В данном случае логика немного сложнее:

// сначала необходимо получить все посты определенного пользователя,

// затем их отфильтровать

const drafts = await prisma.user.findUnique({

where: {

id: Number(id),

}

}).posts({

where: { published: false }

})

res.json(drafts)

})



Получение всех опубликованных постов с условиями:

app.get('/feed', async (req, res) => {

// Извлекаем параметры из строки запроса

const { searchString, skip, take, orderBy } = req.query

// Формируем строку поиска

const or: Prisma.PostWhereInput = searchString ? {

OR: [

{ title: { contains: searchString as string } },

{ content: { contains: searchString as string } },

],

} : {}

// Получаем все опубликованные посты с применением ограничений, если таковые имеются

// Сортировка выполняется по дате и времени последнего обновления

const posts = await prisma.post.findMany({

where: {

published: true,

...or

},

include: { author: true },

take: Number(take) || undefined,

skip: Number(skip) || undefined,

orderBy: {

updatedAt: orderBy as Prisma.SortOrder

},

})

res.json(posts)

})



POST-запросы.

Создание нового поста:

app.post(`/post`, async (req, res) => {

// Извлекаем данные из тела запроса

const { title, content, authorEmail } = req.body

// Создаем новый пост с помощью запроса `create`

// Обратите внимание, что мы подключаем пост к таблице `User`

// с привязкой к `email` автора поста с помощью настройки `connect`

const result = await prisma.post.create({

data: {

title,

content,

author: { connect: { email: authorEmail } },

},

})

res.json(result)

})



Создание нового пользователя:

app.post(`/signup`, async (req, res) => {

// Извлекаем данные из тела запроса

const { name, email, posts } = req.body

// У пользователя может быть несколько постов

const postData = posts?.map((post: Prisma.PostCreateInput) => {

return { title: post?.title, content: post?.content }

})

// Обратите внимание, что мы используем запрос `create` дважды:

// один раз для создания пользователя и еще один для создания постов

const result = await prisma.user.create({

data: {

name,

email,

posts: {

create: postData

}

},

})

res.json(result)

})



PUT-запросы.

Публикация поста:

app.put('/publish/:id', async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

try {

// Находим пост с указанным `id`

// Нас интересует только поле `published` -

// делаем выборку с помощью настройки `select`

const postData = await prisma.post.findUnique({

where: { id: Number(id) },

select: {

published: true

}

})

// Меняем значение свойства `published` на противоположное

// с помощью запроса `update`

const updatedPost = await prisma.post.update({

where: { id: Number(id) },

data: { published: !postData?.published },

})

res.json(updatedPost)

} catch (error) {

// Если в процессе обработки запроса возникла ошибка, то, скорее всего, поста с указанным `id` не существует

res.json({ error: `Пост с указанным id ${id} не найден` })

}

})



Увеличение количества просмотров поста:

app.put('/post/:id/views', async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

try {

// Увеличиваем значение свойства `viewCount` с помощью операции `increment: 1`, где

// `increment` означает увеличение, а `1` количество, на которое происходит увеличение

const post = await prisma.post.update({

where: { id: Number(id) },

data: {

viewCount: {

increment: 1

}

}

})

res.json(post)

} catch (error) {

// Если в процессе обработки запроса возникла ошибка, то, скорее всего, поста с указанным `id` не существует

res.json({ error: `Пост с указанным id ${id} не найден` })

}

})



DELETE-запрос.

Удаление поста:

app.delete(`/post/:id`, async (req, res) => {

// Извлекаем `id` из параметров запроса

const { id } = req.params

// Удаляем пост с помощью запроса `delete`

const post = await prisma.post.delete({

where: {

id: Number(id),

},

})

res.json(post)

})



На этом разработка нашего REST API завершена.

С другими примерами использования Prisma можно ознакомиться здесь.

Проверка работоспособности API


Пришло время убедиться в работоспособности нашего REST API. Для этого воспользуемся Postman. Обратите внимание, что для работы с localhost необходимо установить настольного агента (desktop agent).

Запускаем сервер с помощью команды:

yarn dev

# или

npm run dev




Получаем всех пользователей:


Получаем пост с id === 2:


Создаем нового пользователя:


Создаем новый черновик от лица Боба:


Публикуем данный черновик:


Увеличиваем количество его просмотров:


Полагаю, мы убедились в том, что наш сервис прекрасно функционирует.

Заключение


Итак, какие выводы можно сделать из проведенного нами обзора Prisma ORM? Безусловно, по сравнению с другими популярными решениями для работы с БД семейства SQL, такими как Sequelize или TypeORM, Prisma выглядит более привлекательно как с точки зрения удобства создания и изменения БД, так и с точки зрения простоты формирования запросов и получения данных.

Если же говорить о более специализированных инструментах, таких как Mongoose, то сложно вынести окончательный вердикт, учитывая, что разработчики Prisma обещают в ближайшее время представить MongoDB connector. Однако, если на данный момент Prisma и уступает Mongoose в некоторых аспектах, тот факт, что Prisma умеет работать с несколькими реляционными БД, а также предоставляет возможность выполнять комплексные (include) и точечные (select) запросы (по аналогии с GraphQL), заставляет внимательно следить за ее дальнейшим развитием.

Вместе с тем, нельзя сказать, что Prisma реализует совершенно новый подход к работе с БД. Речь идет, скорее, о доработке существующих технологий, об их совершенствовании в русле доминирующих концепций.



Наши серверы можно использовать для разработки на WebAssembly.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Recovery mode Как работает управление репутацией в интернете

23.10.2020 18:08:20 | Автор: admin
Прошлый мой материал про такое явление, как заказные отзывы, повлёк за собой довольно много негатива, который, зачастую, вполне обоснован в рамках статьи. В рамках некой индульгенции стоит рассказать уже о более честных и безболезненных методах формирования мнения о компании или продукте, которые используются повсеместно.
Таким образом, цели данной статьи:
А) Показать, что такое направление в принципе бывает и вполне успешно существует.
Б) Рассказать, как оно работает.


Введение


Если говорить простым языком, то главная цель в рамках управления репутацией это спрятать всё негативное подальше, а позитивное продвинуть так, чтобы это было видно пользователю, который интересуется той или иной компанией, товаром или персоной.
Дабы сразу отсечь возможный негатив и хейт по отношению к этой статье, речи о заказных отзывах идти не будет. Здесь я в общих чертах опишу принцип действия каждого из инструментов. Некоторые из них задействуются в рамках других направлений, какие-то только в управлении репутацией в интернете.
Перед прочтением просьба учесть, что этот материал является лишь рассказом о том, каким образом действует управление репутацией в общих чертах. Если углубляться в конкретику, то материал грозит стать слишком внушительным.

Из чего состоит управление репутацией?


Итак, если говорить о самых основных инструментах в рамках управления репутацией, то лучше их будет структурировать по двум направлениям, таким как служба клиентской поддержки и формирование видимой зоны.
За клиентскую поддержку отвечают два основных направления:
Мониторинг упоминаний в интернете.
Реагирование на упоминания.
Здесь из названия инструментов можно легко определить, за что они отвечают. Мониторинг проводят, как правило, через YouScan и Brand Analytics. Основная цель находить упоминания о компании, которые требуют ответа или внимания. Это может быть негатив, на который стоит ответить от официального представителя, проблема с товаром или услугой/заказом, информацию о которой необходимо передать в службу клиентской поддержки. Помимо этого, может быть обычное замечание или совет по работе компании и улучшению сервиса, которое так же стоит взять во внимание.
С реагированием ситуация примерно такая же простая. Вы берете выгрузку из системы мониторинга и смотрите, на что же нужно ответить. Отвечать можно как от лица официального представителя, так и от лица клиента, но это уже несколько грязная работа, которую так же могут выполнять в рамках управления репутацией.

Формирование инфополя


В этом случае объем работ и количество инструментов больше, нежели у службы клиентской поддержки. Если в первом случае работа направлена, в основном, на работу с действующими, то при формировании инфополя цель несколько иная. Объектом воздействия станет потенциальный клиент, который с вашей компанией/продуктом уже знаком, как и с конкурентами, и теперь он хочет выбрать, к кому именно обратиться.
Стоит взять во внимание, что помимо обычной рекламы и пиара есть много нестандартных методов, об одном из которых мы и говорим.

SERM (Search Engine Reputation Management)


Многие, кто посвящен в эту тему не очень подробно, называют Сермом полный комплекс управления репутацией, хотя это не так. SERM отвечает только за формирование выдачи, а точнее за продвижение позитивных ресурсов в видимую зону, тем самым смещая негативные. Работа происходит по брендовому запросу и репутационному (бренд/товар + отзывы). Делается это, чаще всего, благодаря повышению поведенческого и социального факторов на эти самые ресурсы. Помимо этого, есть еще ряд деталей для продвижения, но в них углубляться не буду.
Вообще, в рамках SERMа нужно понимать, что речь идет именно о репутационных запросах, которыми могут являться компания + отзывы, компания, товар + обзор, имя персоны и т.д Звучит довольно просто, но довольно часто, когда хотят заказать управление репутацией, указывают желаемый запрос вроде монтажная пена отзывы.

SMRM (Social Media Reputation Management)


Как и логично будет предположить из названия, этот инструмент отвечает за управление репутацией в социальных медиа.
К этому можно отнести и классический SMM, который работает в связке с ORM и отрабатываются возражения клиента. Однако это еще не всё. В управлении репутацией есть очень частая практика создания подконтрольных ресурсов, которую можно отнести как к SERMу, так и к SMRM. Среди этих ресурсов есть следующие:
Сайты-отзовики (сайты сателлиты), которые используются для размещения отзывов под какой-либо конкретный бренд или категорию товаров. Сейчас они уже далеко не так актуальны, как пару лет назад, поскольку доверие потребителя вряд ли может подкрепиться за счёт ресурса, посвященного одной лишь компании. Можно делать и более масштабные отзовики, однако для согласования всего контента на него и самого ТЗ и состава сайта потребуется довольно много времени, сил, и средств.
Статьи. Они используются для более развернутых обзоров продукции или работы компании. Как правило, это такие же полу-рекламные статьи, какие пишут сами компании о себе, только опубликована она будет от лица клиента, у которого уже есть опыт работы с компанией или пользования продуктом. Как правило, размещаются 2-3 статьи в начале работ по репутации, после его они уже продвигаются в видимую зону.
Блоги. По сути, то же самое, что и статьи, только блог берется один и на него с определенной периодичностью размещаются статьи. Само собой, его так же продвигают в видимую зону.
Группы отзывов и предложений в социальных сетях. Здесь чаще всего берется VK, где регистрируется подобная группа, в которой компания может получить обратную связь касательно своей работы, и, конечно же, беспрепятственно модерировать её.
Отдельно отмечу, что группы и сайты-сателлиты теряют свою актуальность, но все еще многие занимаются их созданием и ведением.

Мониторинг


По сути, основной и самый важный инструмент, если компания серьезно работает над своей репутацией. Не важно, осуществляет ли его подрядчик или сама компания, которая следит за репутацией, ведь выгрузка, как правило, собирается раз в день, после чего уже можно увидеть и решить, что же делать со всеми упоминаниями.
Самое главное хорошо настроить систему мониторинга, чтобы ни одно упоминание, тем более негативное, не могло ускользнуть без внимания. Нужно проработать максимально детально всю семантику, особенно это важно для компаний и товаров с популярными названиями.
Также очень часто бывает, когда в отзыве или комментарии нет названия компании. В таком случае можно просматривать вручную небольшой пул ресурсов. Лучше всего будет взять ТОП 10, либо только соц сети компании, хотя, в таком случае, это пересекается с работой SMM отдела.

Реагирование на упоминания


Один из самых главных инструментов, который отвечает за еще свежий негатив или другие упоминания, которые требуют внимания.
Важно понимать, что если речь идет о негативе, то следует делить его на конструктивный и надуманный. Одно дело, когда клиент купил твой товар, ему попался брак или у него есть какое-то слабое место и покупатель написал об этом в социальной сети или отзовике и разложил всё по полочкам. В этом случае более чем логично отреагировать на сообщение, если нашли его через систему мониторинга.
Другое дело, если это обычный провокационный хейтерский отзыв, где нет никаких аргументов и указаний на недостатки, а текст переполнен матом и надуманными фактами. В этом случае лучше либо не реагировать, либо делать это от лица евангелиста бренда, который вступится и сможет вести дискуссию (если, конечно, это можно назвать дискуссией).

Как вести диалог с автором конструктивного негатива?


Первым делом важно понять, что если вы решили заказать управление репутацией, то в первое время подрядчик будет бегать к вам как ребенок с уточнениями, как именно стоит отвечать на тот или иной негатив. Как раз на протяжении первого месяца будет составляться карта реакций, где должны быть учтены все нюансы общения с клиентом, варианты ответа, возможный ответный негатив.
Когда упоминание уже найдено, необходимо сразу же выяснить подробности, в чем именно причина такого недовольства, если, конечно, этого не понятно из самого сообщения.
После этого максимум в двух сообщениях перевести диалог в личные сообщения или на почту, чтобы всё выяснение обстоятельств не проходило у всех на глазах. Если вы смогли это сделать, то всё отлично, работа сделана. Дальше уже дело за службой поддержки.
Можно задаться вопросом: А зачем вообще всё это, если можно просто передать обращение в службу поддержки?. Ответ довольно прост в таком случае важно, чтобы пользователи, в том числе те, которые оставили недовольство на видном месте, видели живое общение, которое даст понять, что компании не все равно на своего клиента. Служба поддержки, как правило, отвечает максимально сухо и скриптованно, поэтому не имеет такого же эффекта.

Удаление негативных отзывов


Прочитав этот заголовок, вы, наверное, подумали, что речь идет об удалении всего и вся, но не тут то было. Делать так не стоит как минимум по двум причинам:
1. При неправильном подходе негатив может получить большую огласку.
2. У Вас не получится удалить всё.
Пытаться удалить негатив стоит лишь на трастовых площадках, модерация которых может и будет идти на диалог, которым важна, в первую очередь, своя собственная репутация. Это как раз и есть те самые трастовые ресурсы, которые берутся в рамках размещения, если заказать управление репутацией. Они довольно сложные в рамках размещения даже в том случае, если отзыв оставляет реальный клиент. Что же касается удаления отзывов, то при наличии серьезных и объективных доводов, почему тот или иной отзыв является заказным и не от вашего клиента, ресурс вполне может серьезно рассмотреть жалобу и удалить отзыв или негативный комментарий.
Даже такие, казалось бы, логичные и официальные претензии могут вылиться в новую волну негатива, если речь идет о ресурсах, которые являются мусорными и нацеленными на негатив. Сам рынок компромата в России развит довольно хорошо (Если вообще уместно сказать именно это слово). Это и является ключевым фактором, почему подобные порталы и сайты раскрутят историю о том, что им написал некто с просьбой удалить тот ли иной негатив или компромат.
Такие ресурсы лучше не трогать, а смещать ниже, где его увидит минимальное количество пользователей.

Как строить репутацию компании на старте?


Рассмотрим подход к формированию имиджа для компании, которая только выходит на рынок.
Само собой, наиболее актуально это направление для брендов, которые преимущественно работают в интернете, или же их продажи зависят от обсуждения в социальных сетях и сарафанного радио.
Итак, допустим, что есть компания Икс, которая только отрегулировала и настроила бизнес-процессы и начинает свою деятельность. Для неё важно, чтобы клиент, когда выбирал между ней и более известными конкурентами, выбрал именно её. Не так важно, решили заказать управление репутацией или сделать сами, зависит только от возможностей, как организационных, так и финансовых. Как же рассказать о себе в рамках репутации?
В первую очередь надо знать, что если компания занимается, допустим, производством холодильников, то формировать выдачу по коммерческим запросам, вроде купить холодильник в принципе невозможно. Наша цель это брендовый и репутационные запросы, то есть название бренда и название бренда+отзывы.

Формируем выдачу


Формирование выдачи необходимо для того, чтобы дать больше информации для потенциального и уже теплого клиента, который знает о вас и решает, к какой компании обратиться к конкурентам или вам.
Дать эту информацию можно несколькими способами:
Размещение статей о компании, продукте, всех аспектах и конкурентных преимуществах. Это могут быть как статьи от самой компании, так и от сторонних лиц. Например, если речь идет о новом ресторане, то есть смысл пригласить известного критика или блогера, который, в силу своего опыта и компетенций раскроет все детали, интересные клиенту. Однако важно понимать, что в таком случае компания практически не контролирует, что именно будет написано в материале. Понятно, что можно и просто занести, но, когда читаешь такие статьи или рецензии сразу видно, что к чему.
Сами эти статьи стоит продвигать в видимую зону выдачи.
В первое время это будет легко, однако со временем, когда контента будет все больше, эта задача будет постепенно становится все более сложной.
Ведение блога компании, в котором на регулярной основе будут публиковаться статьи. Казалось бы, а в чем разница? Дело в том, что блог ведет сама компания и размещает там регулярно. Статьи же, о которых говорили в прошлом разделе, публикуются на сторонних ресурсах, вроде того же Хабра, Пикабу, VC, ЖЖ и так далее, зависит от сферы бизнеса. И эти статьи уже занимают свое место в инфополе. Блог же постоянно обновляется.
Отзывы. Нет, нет, и еще раз нет, речь идет не о размещении отзывов через левые аккаунты или прочие средства. Когда репутация компании еще чиста, то лучшим решением станет стимуляция реальных отзывов. Это можно делать через абсолютно разные способы, от почтовых рассылок до флаеров и скидок за оставленные отзывы. Вчера, например, в пакет в аптеке мне как раз положили флаер, на котором был QR код, пройдя по которому просили оставить отзыв.
Как одна из основ, которая уже есть у всех это создание групп, посвященным обратной связи и отзывам от клиентов. Чаще всего речь идет о ВК, поскольку именно там функционал располагает к дискуссии, а сами обращения удобно как писать, так и отвечать на них.

Контролируем инфополе


Здесь же все еще проще, по крайней мере, если ситуация не запущенная. В таком случае самое важное и основное мониторинг. Поможет он не только в рамках управления репутацией, но и для понимания активности пользователей компании в интернете, где о вас говорят больше всего, какие каналы привлечения аудитории являются более успешными, а где есть проседания.
Когда вы знаете, что где-то написали то, что портит общую картину, а уж тем более, если это проблема клиента, который хочет добиться её решения отвечайте ему и решайте её. Важно делать это не как устаревший робот с простыми скриптами, а вести полноценное общение с человеком с целью решения проблемы, постепенно, как и писал выше, уводить диалог на почту, в личные сообщения и так далее.
Помимо этого, крайне не рекомендуется все время переводить к службе поддержки. Недавно даже видел, как на одном из популярном маркетплейсов магазин, продающий чехлы для телефонов и ноутбуков, на все отзывы отвечал сообщением, вроде Спасибо за отзыв, нам очень важно ваше мнение. Всё было бы не так печально, если б таких ответов не было на негатив и отзывы в одну звезду. Смотрелось это, сами понимаете, не очень хорошо.

Вывод


При правильном подходе и своевременном начале заботы о своем имидже в интернете более чем возможно обойтись малой кровью и создать такое инфополе, которому будут нипочем даже атаки конкурентов, не говоря уже о реальном органическом негативе. В такой ситуации не будет желания заказать управление репутацией, по крайней мере в том виде, в котором это направление представляет большинство.
Сейчас это направление эволюционирует, причем это связано не только с появлением новых инструментов в рамках работы, но и с осознанием бизнеса, что куда легче с самого начала подходить к репутации и имиджу как к капиталу, а не как к пустому звуку.

Иван Клейменов, Puppet Agency

Подробнее..

Я 20 лет наслаждаюсь разнообразием архитектур и хочу поделиться мыслями

09.09.2020 10:06:55 | Автор: admin


Сначала хотел написать комментарий к статье "Я десять лет страдал от ужасных архитектур в C#...", но понял две вещи:

1. Слишком много мыслей, которыми хочется поделиться.
2. Для такого объёма формат комментария неудобен ни для написания, ни для прочтения.
3. Давно читаю Хабр, иногда комментирую, но ни разу не писал статей.
4. Я не силён в нумерованных списках.

Disclaimer: я не критикую @pnovikov или его задумку в целом. Текст качественный (чувствуется опытный редактор), часть мыслей разделяю. Архитектур много, но это нормально (да, звучит как название корейского фильма).

Однако давайте по порядку. Сначала моё мнение о том, что влияет на архитектуру, потом про спорные моменты в статье об исправлении архитектур. Ещё расскажу о том, что у нас хорошо работает может, пригодится кому-нибудь.

И, конечно, всё сказанное здесь является личным мнением на основе моего опыта и когнитивных искажений.

О моём мнении


Я часто принимаю архитектурные решения. Когда-то большие, когда-то маленькие. Изредка придумываю архитектуру с нуля. Ну, как с нуля наверняка всё придумано до нас, но про что-то мы не знаем, поэтому приходится придумывать. И не из любви к велосипедостроению (скажем так, не только из любви к нему), а потому что для некоторых задач не нашлось готового решения, которое устроило бы по всем параметрам.

Почему я считаю, что совершенно разные архитектуры имеют право на жизнь? Можно порассуждать о том, что программирование искусство, а не ремесло, но я не буду. Моё мнение: когда-то искусство, когда-то ремесло. Речь не об этом. Главное, что задачи разные. И люди. Уточню под задачами подразумеваются требования бизнеса.

Если когда-то мои задачи станут однотипными, я напишу или попрошу кого-то написать нейросеть (а может, хватит и скрипта), которая меня заменит. А сам займусь чем-то менее безрадостным. Пока же мой и, надеюсь, ваш личный апокалипсис не наступил, давайте подумаем, как влияют задачи и прочие условия на разнообразие архитектур. TL&DR; разнообразно.

Производительность или масштабируемость


Вот это самая правильная, пожалуй, причина поменять архитектуру. Если, конечно, не проще старую архитектуру адаптировать к новым требованиям. Но тут сложно кратко рассказать что-то полезное.

Сроки


Допустим, сроки (дважды убедился, что написал через о) сильно сжаты. Тогда нам не до того чтобы выбирать, а, тем более, придумывать архитектуру бери знакомые инструменты и копай. Но есть нюанс иногда сложные проекты можно сделать вовремя только применив (и, возможно, придумав) что-то принципиально новое. Кто-то может сказать, что пригласить заказчика в баню старый прием, но я сейчас про архитектуру

Когда сроки комфортные часто получается парадоксальная ситуация вроде и можно придумать что-то новое, но зачем? Правда, многие успешно поддаются соблазну заняться другим более горящим проектом и сводят ситуацию к предыдущей.

В моей практике сроки редко приводят к революциям в архитектуре, но бывает. И это прекрасно.

Скорость и качество разработки


Бывает так команда (или кто-то из руководства) замечают, что скорость разработки снизилась или за итерацию багов многовато набежало. Нередко виновной в этом признают неправильную архитектуру. Иногда заслуженно. Чаще просто как самого удобного обвиняемого (особенно, если в коллективе нет её родителя).

В принципе, в одних случаях всё сводится к фактору сроков. А в других к поддерживаемости, о ней далее.

Поддерживаемость


Неоднозначная тема. Потому что всё очень субъективно и зависит много от чего. Например от команды, языка программирования, процессов в компании, количества адаптаций под разных клиентов. Давайте про последний фактор, мне он кажется самым интересным.

Вот вы сделали заказной проект. Успешно, в сроки и бюджет уложились, заказчик всем доволен. Было и у меня такое. Теперь вы смотрите на то, что использовали и думаете так вот она золотая жила! Мы сейчас используем все эти наработки, быстро сделаем один B2B-продукт, и Сначала всё хорошо. Продукт сделали, пару раз продали. Наняли ещё продавцов и разработчиков (нужно больше золота). Заказчики довольны, платят за сопровождение, случаются новые продажи

А потом один из заказчиков говорит человеческим голосом мне бы вот эту штуковину совсем по-другому сделать сколько это может стоить?. Ну, подумаешь несколько ifчиков с другим кодом воткнуть (допустим, некогда было DI прикрутить), что плохого может случиться?

И в первый раз действительно ничего плохого не случится. Я бы даже не советовал в такой ситуации что-то специальное городить. Преждевременное усложнение архитектуры сродни преждевременной оптимизации. Но когда это случается во второй и третий раз это повод вспомнить про такие штуки как DI, паттерн стратегия, Feature Toggle и иже с ними. И, на время, это поможет.

А потом наступает день, когда вы смотрите на настройки проекта (всего-то несколько сотен опций) для конкретного тестового стенда Вспоминаете, как посчитать число сочетаний и думаете как же, вашу мать, это можно протестировать? Понятно, что в идеальном мире это просто ведь каждая фича спроектирована и реализована так, что она никак не влияет на другую, а если влияет, то всё это предусмотрено и вообще наши разработчики никогда не ошибались.

Конечно, я сгустил краски можно выделить какие-то наборы фич, которые используются у реальных заказчиков, написать больше тестов (как и каких тема отдельного разговора) и немного упростить задачу. Но вдумайтесь каждый серьезный релиз нужно протестировать для всех заказчиков. Напоминаю, это не B2C, где можно сказать выкатим фичу для 5% пользователей и соберём фидбек для B2B фидбек можно по судам начать собирать

Решения? Например, разделить продукт на модули с отдельным жизненным циклом (не забывая тестировать их взаимодействие). Это снизит сложность сопровождения, хотя и усложнит разработку. И я сейчас не о благодатной для холиваров теме монолит vs. микросервисы в монолите тоже можно устроить подобное (хотя и сложнее, на мой взгляд).

И, заметьте, с прагматической точки зрения на каждом этапе у нас была неплохая архитектура.

И к чему всё это?


Я не хочу вас (и себя) утомлять перечислением других причин для изменений в архитектуре. Давайте сейчас согласимся, что архитектуры имеют свойство со временем меняться, в зависимости от многих факторов. А значит: идеальная архитектура, решающая ну вот все проблемы не существует.

Если я вас в этом еще не убедил посмотрите на разнообразие языков программирования и фреймворков (только не во фронтенде не надо вскрывать эту тему). Если кто-то скажет, что это плохо, предлагаю провести мысленный эксперимент представьте мир, в котором есть один конкретный язык программирования. С одним важным условием он вам не нравится. Например, потому что вы никогда его не использовали, да и не собирались этого делать.

И, признаюсь, есть еще один веский довод придумывать что-то новое, оптимизируя несколько параметров, играя компромиссами это чертовски увлекательно. А теперь, когда мы все (правда?) согласны, что разнообразие архитектур это нормально

Обсуждение статьи про исправление архитектур



А что IoC?


Про IoC соглашусь, что портянкам место в армии, а модули это вселенское добро. Но вот всё остальное

Если, конечно, послушать некоторых апологетов чистого кода, то можно накодить гору сервисов, в каждом из которых будет в среднем полтора метода, а в методе две с половиной строки. Но зачем? Вот честно, вы точно хотите соблюдать принципы, которые помогут справиться с маловероятными проблемами в далеком будущем, но размазывают даже несложную логику по десяткам файлов? Или вам, всё-таки, достаточно сейчас написать прилично работающий код?

К слову сказать, сейчас работаю над модулем, который точно будет использоваться в разных продуктах и, скорее всего, будет активно тюниться. Так и там стараюсь не мельчить. Не окупается. Хотя вот в нём использую единственные реализации интерфейсов чаще обычного.

Так вот, если у нас есть модули и мы не мелочны, то откуда взяться проблемам производительности IoC или неподдерживаемых портянок IoC-конфигураций? Я не сталкивался.

Правда, уточню наши условия работы:

  • Модули у нас не те, которые предоставляются почти любым IoC-фреймворком а вот прям модули которые общаются между собой удалённо через API (иногда можно, из соображений производительности, поселить их в одном процессе, но схема работы не поменяется).
  • IoC используется максимально простой и максимально просто зависимости втыкаются в параметры конструктора.
  • Да, у нас сейчас микросервисная архитектура, но и здесь стараемся не мельчить.

Совет: интерфейсы можно держать в том же файле, что и класс удобно (если, конечно, пользуетесь нормальной IDE, а не блокнотом). Исключения делаю, когда интерфейсы (или комментарии к ним) разрастаются. Но это всё вкусовщина, конечно.

А что не так с ORM и зачем прямой доступ к БД?


Да я и сам скажу, что не так многие из них слишком далеки от SQL. Но не все. Поэтому, вместо того, чтобы терпеть, пока O/RM удаляет 3000 объектов или придумывать ещё один, найдите тот, который вас устроит.

Совет: попробуйте LINQ to DB. Он хорошо сбалансирован, есть методы Update/Delete для нескольких строк. Только осторожно вызывает привыкание. Да, нет каких-то фич EF и немного другая концепция, но мне понравился намного больше EF.

Кстати, приятно, что это разработка наших соотечественников. Игорю Ткачеву респект (не нашёл его на Хабре).

А что не так с тестами на БД?


Да они будут медленнее, чем на данных в памяти. Фатально ли это? Да нет, конечно же. Как решать эту проблему? Вот два рецепта, которые лучше применять одновременно.

Рецепт 1. Берёшь крутого разработчика, который любит делать всякие прикольные штуки и обсуждаешь с ним, как красиво решить эту проблему. Мне повезло, потому что force решил проблему быстрее, чем она появилась (даже не помню, обсуждали её или нет). Как? Сделал (за день, вроде) тестовую фабрику для ORM, которая подменяет основное подмножество операций на обращения к массивам.

Для простых юнит-тестов идеально. Альтернативный вариант юзать SQLite или что-то подобное вместо больших БД.
Комментарий от force: Тут надо сделать пару уточнений. Во-первых, мы стараемся не использовать сырые запросы к базе данных в коде, а максимально используем ORM, если он хороший, то лезть в базу с SQL наголо не требуется. Во-вторых, разница поведения с базой есть, но ведь мы не проверяем вставку в базу, мы проверяем логику, и небольшое различие в поведении тут несущественно, т.к. особо ни на что не влияет. Поддержка корректной тестовой базы гораздо сложнее.

Рецепт 2. Бизнес-сценарии я предпочитаю тестировать на настоящих БД. А если в проекте заявлена возможность поддержки нескольких СУБД, тесты выполняются для нескольких СУБД. Почему? Да всё просто. В утверждении не хочется тестировать сервер баз данных, увы, происходит подмена понятий. Я, знаете ли, тестирую не то, что join работает или order by.

Я тестирую свой код, работающий с БД. А зная, что даже разные версии одной СУБД могут выдавать разные результаты на разных запросах (пруф), я хочу основные сценарии проверять именно на тех БД, с которыми этот код будет работать.

Обычно подобные тесты у меня выглядят так:

  • Для группы тестов (Fixture) с нуля генерируется по метаданным БД. Если нужно заполняются необходимые справочники.
  • Каждый сценарий сам добавляет нужные данные в процессе прохождения (пользователи ведь тоже это делают). В тестах на производительность не так, но это уже совсем другая история
  • После каждого теста лишние данные (кроме справочников) удаляются.

Совет: если такие тесты у вас выполняются объективно долго (а не потому, что пора оптимизировать запросы к базе) сделайте билд, который будет запускать их реже (категории тестов или отдельный проект в помощь). Иначе разработчики не захотят сами запускать и остальные быстрые тесты.

Транзакция и email


Просто дополню историю транзакция в БД по каким-то причинам упала, а e-mail ушёл. А какое веселье будет, когда транзакция подождёт недоступного почтового сервера, поставив колом всю систему из-за какого-нибудь уведомления, которое пользователь потом отправит в корзину, не читая

Правда, я всегда считал, что письма в транзакции отправляют только джуны в дикой природе в отсутствие ревью. В нашей команде за такое канделябром бьют (пока виртуальным).

Итоги


В целом, если @pnovikov не имеет планов по захвату мира с помощью единственно верной идеологии архитектуры, других, достойных упоминания, расхождений во взглядах не нашёл. Для каких-то задач, безусловно, озвученные им принципы подойдут. С удовольствием прочитаю следующие статьи и комментарии, может, найду какие-то полезные идеи для себя.

Предлагаемый фреймворк вряд ли буду использовать. Причина проста у нас уже есть идеальная архитектура

P.S. Если у вас будет желание обсудить что-то в комментариях, буду рад принять в этом участие.
Подробнее..

Добавляем ORM в проект за четыре шага

17.09.2020 00:18:21 | Автор: admin

Представим, что вашему проекту срочно понадобился ORM, и вы хотите внедрить его как можно быстрее. В этой статье я хочу рассказать, как это можно сделать всего за четыре шага на примере использования open source проекта Apache Cayenne.


Для начала вкратце опишу механизм работы с данной библиотекой. Схема базы данных и модели описывается в xml файле, который может быть сгенерирован через GUI приложение или через консоль. Затем на основе xml файла генерируются java объекты, которые являются соответствующим отображением таблиц в базе. Последним шагом создается ServerRuntime объект, который инкапсулирует в себе весь стек Apache Cayenne.
Итак, перейдем к примеру. Что необходимо сделать:


  • Создать схему базы данных
  • Импортировать схему в проект, то есть получить xml файлы с описанием схемы
  • Создать объектную модель, то есть сгенерировать java классы
  • Проинициализировать ServerRuntime для доступа к базе данных из приложения

Что потребуется для начала? Уже существующий maven или gradle проект, Java 1.8+ и база данных. Мой тестовый проект использует maven, java 14 и самую свежую версию Apache Cayenne 4.2.M1. В качестве базы я использую mysql. Вы для своих проектов можете использовать стабильную версию 4.1 и любую из известных реляционных баз на ваш выбор.
Для наглядности я прикреплю ссылку на пример.


Создание схемы


Для примера создадим простейшую схему, состоящую из трех сущностей: марка авто, модель авто, отзыв на модель авто.


CREATE SCHEMA IF NOT EXISTS cars_demo; USE cars_demo;CREATE TABLE car_brand (ID INT NOT NULL AUTO_INCREMENT, NAME VARCHAR(200) NULL, COUNTRY VARCHAR(200) NULL, PRIMARY KEY (ID)) ENGINE=InnoDB;CREATE TABLE car_model (ID INT NOT NULL AUTO_INCREMENT, NAME VARCHAR(200) NULL, CAR_BRAND_ID INT NULL, PRIMARY KEY (ID)) ENGINE=InnoDB;CREATE TABLE feedback (CAR_MODEL_ID INT NULL, ID INT NOT NULL AUTO_INCREMENT, FEEDBACK VARCHAR(200) NULL, PRIMARY KEY (ID)) ENGINE=InnoDB;ALTER TABLE car_model ADD FOREIGN KEY (CAR_BRAND_ID) REFERENCES car_brand (ID) ON DELETE CASCADE;ALTER TABLE feedback ADD FOREIGN KEY (CAR_MODEL_ID) REFERENCES car_model (ID) ON DELETE CASCADE;

Первый шаг пройден, двигаемся ко второму.


Импорт схемы


Здесь уже начинается непосредственное использование библиотеки. Для начала подключим необходимый плагин к проекту:


            <plugin>                <groupId>org.apache.cayenne.plugins</groupId>                <artifactId>cayenne-maven-plugin</artifactId>                <version>${cayenne.version}</version>                <configuration>                    <dataSource> <!--1-->                        <driver>com.mysql.jdbc.Driver</driver>                         <url>jdbc:mysql://127.0.0.1:3306/cars_demo</url>                         <username>root</username>                         <password>root</password>                    </dataSource>                    <cayenneProject>${project.basedir}/src/main/resources/cayenne/cayenne-project.xml</cayenneProject> <!--2-->                    <map>${project.basedir}/src/main/resources/cayenne/datamap.map.xml</map> <!--3-->                    <dbImport> <!--4-->                        <defaultPackage>cayenne.note.project.model</defaultPackage>                        <catalog>cars_demo</catalog>                    </dbImport>                </configuration>                <dependencies>                    <dependency> <!--5-->                        <groupId>mysql</groupId>                        <artifactId>mysql-connector-java</artifactId>                        <version>${mysql.version}</version>                    </dependency>                </dependencies>            </plugin>

  • (1) DataSource, для подключения к базе
  • (2) Путь, где будет лежать сгенерированный xml, который необходим для запуска Cayenne
  • (3) Путь, где будет лежать xml с описанием модели и базы
  • (4) Базовый пакет, где позже будут находиться сгенерированные классы
  • (5) Зависимость от mysql-connector для работы с mysql

Далее в консоли запускаем импорт модели:


mvn cayenne:cdbimport

После выполнения этой команды должны появится два файла, указанные в (2) и (3). Как я уже говорил, файл cayenne-project.xml является служебным файлом, необходимым для работы библиотеки. Файл datamap.map.xml это описание модели базы данных и ее объектного отображения, а также всех связей.


Пару слов о процессе cdbimport: по умолчанию он импортирует всю схему, включая все связи. Данная команда может быть кастомизирована. Вы можете указать, какие сущности стоит включить в импорт, какие исключить, есть возможность указать паттерн для импорта таблиц. Более подробно с этим можно ознакомиться в документации.


Генерация классов


В предыдущем пункте мы сгенерировали описание модели, теперь же нам необходимо сгенерировать java классы, которые можно будет использовать в проекте. Сделать это очень просто, достаточно просто запустить в консоли команду:


mvn cayenne:cgen

После успешной генерации в пакете, который был указан в настройке плагина, будет находиться набор сгенерированных сущностей. Стоит обратить внимание, что сгенерировалось два набора классов. Первый находится в пакете auto и является служебным. Не стоит модифицировать эти классы, так как все изменения исчезнут при следующей генерации. Для пользовательских изменений существуют классы без нижнего подчеркивания, которые наследуются от классов из пакета auto. Именно они предназначаются для прямого использования и кастомизации.


Пример использования


Мы на финишной прямой, осталось только привести пример использования Apache Cayenne.
Создадим ServerRuntime это основной стэк Cayenne, который создается один раз для всего проекта.
Из рантайма всегда можно получить ObjectContext объект, который используется для работы с базой данных.


ServerRuntime cayenneRuntime = ServerRuntime.builder()                .dataSource(DataSourceBuilder                        .url("jdbc:mysql://127.0.0.1:3306/cars_demo")                        .driver("com.mysql.cj.jdbc.Driver")                        .userName("root") // Need to change to your username                        .password("root") // Need to change to your password                        .build())                .addConfig("cayenne/cayenne-project.xml")                .build();        ObjectContext context = cayenneRuntime.newContext();

Создадим несколько сущностей и отправим их в базу:


CarBrand carBrand = context.newObject(CarBrand.class);carBrand.setName("BMW");carBrand.setCountry("Germany");CarModel carModel = context.newObject(CarModel.class);carModel.setName("i3");carModel.setCarBrand(carBrand);Feedback feedback = context.newObject(Feedback.class);feedback.setFeedback("Like");feedback.setCarModel(carModel);context.commitChanges();

Как видно, мы создаем объекты при помощи ObjectContext, затем модифицируем их и фиксируем изменения при помощи context.commitChanges().


Для выборки сущностей можно использовать API на любой вкус от чистого sql и ejbql до хорошо читаемого API. Полное описание можно найти в документации.
Небольшой пример обычного селекта из базы с использованием Apache Cayenne:


List<CarBrand> carBrans = ObjectSelect.query(CarBrand.class).select(context);

На этом у меня все. Как видно из этого примера, за несколько шагов можно получить боевой ORM, который полностью готов к работе. Надеюсь статья оказалась полезной. С полным примером можно ознакомиться здесь.

Подробнее..

Как дойти до CQRS, если у тебя PHP

17.03.2021 10:08:48 | Автор: admin

Привет, я Сережа из Брянска, и в моем городе и до пандемии почти не было конференций и митапов. Поэтому я привык смотреть записи докладов из разных сообществ. Но как задать их авторам вопрос? Так что я завел подкаст и зову в него интересных докладчиков на поговорить.

Недавно я посмотрел доклад Как перестать бояться CQRS. Вроде бы простая идея, но есть нюансы. Так и появился этот выпуск.

CQRS vs CQS (не перепутай)

Аудио

Вы можете включить аудиоверсию в ней больше технических деталей.

Сергей Жук, Skyeng: А может быть CQRS с одной моделью в коде? Или обязательно две?

Дмитрий Симушев, Райффайзенбанк: CQRS это всё-таки про две модели. У тебя может быть одно хранилище данных, одна физическая база. Но в коде это будут именно два кусочка один на запись, другой на чтение.

Сергей Жук, Skyeng: А какую проблему мы вообще решаем этим? Зачем вообще все усложнять и разделять?

Дмитрий Симушев, Райффайзенбанк: Дело в возрастающей сложности приложений. Когда мы используем одну модель для чтения и записи, вся бизнес-логика кочует в клиентский код. Надо как-то от этого уйти, и мы собираем логику в сущности. А еще мы должны держать много избыточных данных, чтобы строить графические интерфейсы возникает необходимость очень сложного чтения.

Часто, когда ты строишь графический интерфейс, нужно собирать данные из большого количества сущностей. Скажем, тебе нужен заказ, нужен пользователь, количество средств у него на счету, количество новых сообщений у этого пользователя, что-то ещё. Набор данных, который нужно отобразить в интерфейсе, сильно избыточен для команд. Команды меняют состояние системы это формализованный скрипт того, как бизнес видит взаимодействия сущностей. Запросы, наоборот, ничего не меняют, они только возвращают данные и помогают абстрагировать логику отображения и выборки данных.

Так у тебя органически появляются две модели: одна для чтения, другая для записи.

Второй момент, когда CQRS сильно помогает при разноплановой нагрузке. Например, на запись у тебя маленький поток запросов, а на чтение очень большой, и ты хочешь независимо вот эту историю масштабировать. Скажем, берёшь и делаешь проекцию в каком-нибудь документоориентированном хранилище, где модель уже специально оптимизирована для чтения.

Сергей Жук, Skyeng: А какую роль во всём этом играет ORM? Если мы для чтения можем вообще кастомные штуки делать, получается, мы можем ее выкинуть? Или стоит оставить ее для записи?

Дмитрий Симушев, Райффайзенбанк: Если мы говорим о модели записи, ORM все еще нужна, когда у тебя богатая доменная модель. Если всё построено на объектах и эти объекты нужно каким-то образом на реляционную базу отобразить, ORM помогает не думать о твоём коде в терминах БД. Ты работаешь с сущностями, а ORM за тебя всё отображает на базу.

Когда мы переходим к модели чтения, для отображения нужны не сущности, а плоские объекты. PDO может отлично брать SQL-запросы и результаты засовывать в DTO-шки. Это получится сильно быстрее. Получается, мы можем оптимизировать модель чтения, просто выкинув всё ненужное. И тут ORM как раз та штука, от которой можно отказаться. С ней мы упремся в производительность. Но этот путь стоит проходить эволюционно: если тебе хватает на чтение средств ORM, если тебя в принципе все устраивает почему бы и нет?

Сергей Жук, Skyeng: Так может, тогда и одну модель оставить?

Мы возвращаемся к исходному вопросу. СQRS это точно про две модели?

То есть, видишь, можно оставить одну ORM, а снаружи поведение, как всё у нас разделено. Мы разделим записи и чтение. Записи будут быстрые. Можно даже асинхронно сделать. Снаружи-то не видно...

Дмитрий Симушев, Райффайзенбанк: Ты знаешь, это тоже работает, но это не CQRS.

Сергей Жук, Skyeng: А почему?

Дмитрий Симушев, Райффайзенбанк: Принцип Command-query separation (CQS) появился еще раньше: один эндпоинт занимается чтением, другой записью, и они между не взаимодействуют. CQRS следующий шаг на этом пути: за чтение и запись отвечают разные подсистемы в коде, и ты используешь вообще разные модели. Если тебе достаточно CQS, если тебе хватает производительности окей. Если нет, ты уже идёшь в CQRS и в разделение этих моделей.

Сергей Жук, Skyeng: Давай тогда поговорим про архитектуру CQRS. Я верно понимаю, что чтение синхронное, а записи асинхронные? Или необязательно? Например, в рамках http-запроса мы получим данные, поставим команду на выполнение и сразу отдадим ответ: например, там 202? Сама же команда попадает в какую-то очередь, шину, и выполнится за пределами request-response?

Дмитрий Симушев, Райффайзенбанк: Необязательно. У тебя же обычно есть слой контроллеров. Скажем, у тебя может быть контроллер, который отправляет сначала команду, дожидается, пока она будет выполнена, делает запрос и возвращает что-то наружу. Само разделение идёт на более низких слоях.

CQRS не должен просачиваться полностью на все слои твоей архитектуры. Есть слой доменной части, есть слой приложения, есть слой контроллеров. Можно контроллером сначала вызывать команду, потом делать запрос и возвращать что-то по REST.

Сергей Жук, Skyeng: Но в ООП есть принцип, что метод у объекта должен либо менять состояние, но ничего не возвращать, либо должен что-то возвращать но не менять состояние. Я воспринимал CQRS в этом ключе: грубо говоря, эндпоинт либо возвращает какие-то данные, не меняя состояние стораджа, либо он меняет состояние, но при этом всегда void.

Дмитрий Симушев, Райффайзенбанк: Знаешь, тут большой вопрос: а нужно ли CQRS проецировать на уровень эндпоинтов?

Ты говоришь про метод, но это метод кода, более низких слоев. Эндпоинты не должны этой истории подчиняться. Точнее, так: необязательно это делать прямо до конца, до эндпоинта. Ты можешь провести границу иначе: сказать, что в ядре, например, CQRS, а дальше я могут быть варианты работать с REST, GraphQL, навигацию делать, что-то еще.

Сергей Жук, Skyeng: Окей, а как понять, что мой кейс созрел для CQRS?

Дмитрий Симушев, Райффайзенбанк: Первое условие: хорошо выраженная предметная область. У тебя должен быть зрелый бизнес-домен. Второй момент: должны быть либо проблемы с производительностью, либо неравномерная нагрузка.

Как понять, что уперся?

Сергей Жук, Skyeng: Расскажи, как ты пришёл к CQRS? Наверняка же сначала возникли какие-то проблемы?

Дмитрий Симушев, Райффайзенбанк: Вот как получилось. Мы начали делать приложение, продумали сущности, стали их реализовывать. И вроде всё хорошо, но потом приходит бизнес: Ребят, вот здесь нам нужно вывести еще немножко информации. Встает выбор: делать сущности еще одну связь, чтобы при чтении взять её целиком, серилизовать и отдать на клиент. Либо придумывать что-то ещё.

Добавление связей работает только до какого-то определенного количества этих связей. А когда модель распухает, ORM на чтении начинает очень здорово тормозить. И тебе в любом случае приходится как-то это дело оптимизировать. В момент, когда мы поняли, что не можем ничего сделать с производительностью и уперлись на чтение в ресурсах, мы посмотрели по сторонам и подумали: Окей, а почему бы нам не делать обычные скрипт-запросы в базу и отдавать данные на клиента?

Поняли, что эта история очень здорово вписывается в CQRS и разбили приложение на два кусочка: один модели, плюс-минус чистые, без чтения; и второй модель чтения, которая просто подает данные.

Сергей Жук, Skyeng: Как вы поняли, что уперлись?

Дмитрий Симушев, Райфайзенбанк: Стали тормозили эндпоинты. Смотришь, у тебя ручка отрабатывает за секунды. Залезаешь внутрь, смотришь - вроде всё гладко. А потом понимаешь, что большую часть времени работает Doctrine.

Начинаешь оптимизировать Doctrine: делаешь всякие радостные хаки с ленивой загрузкой, включением-выключением, кэшем второго уровня. И в какой-то момент понимаешь, что выжал из нее все. Но тебе не хватает. Тогда ты пробуешь переписать запрос на чистый SQL и получаешь прирост х10 просто за счет того, что ты выкинул ORM и в чем-то упростил свой код.

Сергей Жук, Skyeng: Проблема была даже не в медленным запросе, а в том что PHP-код этот медленно обрабатывал?

Дмитрий Симушев, Райфайзенбанк: И то, и то. В случае с PHP-кодом, самая большая нагрузка в гидрации у Doctrine: все, что ты притащил, она сначала разворачивает в сущности, а ты сущность все равно серилизуешь в JSON, чтобы отдать на фронт. Но есть и SQL-ная часть нам нужно было, скажем, половину полей сущности, а из-за специфики ORM выбирали ее всю.

Пока мы остановились на разделении моделей. Нам хватило. PostgreSQL держит нагрузку на этом уровне. Но в целом, никто не мешает пойти дальше и выделить, скажем, отдельный сторадж.

Сергей Жук, Skyeng: Тяжело было затаскивать в уже написанный проект CQRS, разделять модели?

Дмитрий Симушев, Райффайзенбанк: Ты знаешь, очень гладко все проходит. У тебя есть сущность, есть бизнес-модель. Ты понимаешь, что у тебя на чтении система не справляется, и просто сбоку делаешь сервис, который ходит в базу данных. В контроллере используешь его и всё запрос ты инкапсулировал, постепенно можешь часть нагрузки на чтение снимать с бизнес-модели. И вот так, кусочками, вплоть до единичного запроса и эндпоинтов.

Мы перевели только то, что тормозило: нужно было оптимизировать запросы мы их оптимизировали. Как только стало нормально тут же остановились. Часть моделей у нас работает и на чтение, и на запись.

Сергей Жук, Skyeng: У вас были проблемы? Что было тяжело, может быть, пошло не так?

Дмитрий Симушев, Райффайзенбанк: Наверное, самое сложное это решиться. Потому что есть ORM, есть логика, которая собрана в одном месте, есть маппинги, которые в одном месте описаны. И ты вроде как понимаешь, что если ты сейчас начнешь где-то сбоку делать что-то ещё то рано или поздно это стрельнет в ногу. Самое сложное принять, что ты уперся и делать надо.

Из сложностей в процессе: любое серьезное изменение начинает появляться в двух местах модели чтения и модели записи. Если пишешь плоские запросы, надо и в маппингах сущности менять и не забывать прочесывать запросы, чтобы они не падали, когда ты делаешь изменения.

Сергей Жук, Skyeng: А к тестированию как-то подход меняется при этом?

Дмитрий Симушев, Райффайзенбанк: Ты немножко меняешь вектор. Когда у тебя одна модель, ты можешь взять юнит и протестировать сущность. У тебя есть геттеры, у тебя есть сеттеры, ты что-то сделал, посмотрел.

Когда переезжаешь на историю с двумя моделями и синхронизацию в базе фокус смещается на интеграционные, end-to-end тесты. Тебе приходится чуть больше тестировать на следующих уровнях пирамиды тестирования, менять юниты на что-то еще.

Сергей Жук, Skyeng: Часто CQRS рассматривается как некий шаг на пути к Event Sourcing. Как тебе удалось спокойно остановиться?)

Дмитрий Симушев, Райффайзенбанк: Когда мы переходим к CQRS, то начинаем сначала с одного стораджа. Если не хватает производительности делаем слейвы на чтение. А вот если дальше всё равно не хватает можно задействовать Event Sourcing.

Есть бизнес-домены, где Event Sourcing сам по себе очень полезен. Не как технология, не как следствие CQRS, а сам по себе. В целом, это любая область, где изменение приводит к каким-то последствиям. Классический пример финансовые истории с бизнес-транзакциями, когда мы рассматриваем, например, какие-то клиентские счета. Тут не так интересна цифра на счете, как история того, что происходило с этим счётом. Ты смотришь в первую очередь на сам процесс, нежели на то, где сейчас находишься.

Предметная область моей команды это справочные данные, связанные с городами, отделениями и прочим таким. По производительности мы еще не дошли до этапа, когда нам надо выделять разнородные хранилища. А поэтому и Event Sourcing не принесет особого профита.

Сергей Жук, Skyeng: А если я знаю, что будет нагрузка и что домен подходит под Event Sourcing, но мне надо запилить MVP?

Дмитрий Симушев, Райффайзенбанк: Ты можешь начать с самого простого: ORM, одной базы данных, одной модели на запись и на чтение, сериализатора. Потому что никто не мешает тебе в какой-то момент остановиться и выделить отдельные запросы на чтение. Как только ты понимаешь, что надо ускоряться ты постепенно начинаешь выделять модель чтения. Модель записи можно вообще пока не трогать. Когда выделишь модель чтения, тогда начинай резать и модель записи. В CQRS очень здорово то, что ко всему можно приходить эволюционно.

Полезное по теме:

Доклад Аделя Файзрахманова про плюсы отделения кода на чтение от кода на запись и про случаи, когда CQRS не нужен.

Другие выпуски подкаста Между скобок.

Подробнее..

Немного SQL алхимии

06.12.2020 02:07:34 | Автор: admin
О популярной библиотеке SQLAlchemy для работы с разными СУБД из Python было написано довольно много статей. Предлагаю вашему вниманию обзор и сравнение запросов с использованием ORM и SQL подходов. Данное руководство будет интересно прежде всего начинающим разработчикам, поскольку позволяет быстро окунуться в создание и работу с SQLAlchemy, поскольку документация от разработчика SQLAlchemy на мой скромный взгляд тяжела для чтения.
image

Немного о себе: я также начинающий разработчик, прохожу обучение по курсу Python разработчик. Данный материал был составлен не в результате ДЗ, а в порядке саморазвития. Мой код может быть достаточно наивным, в связи с чем прошу не стесняться и свои замечания оставлять в комментариях. Если я вас еще не напугал, прошу под кат :)

Мы с вами разберем практический пример нормализации плоской таблицы, содержащей дублирующиеся данные, до состояния 3НФ (третьей нормальной формы).
Из вот такой таблицы:
Таблица с данными
image

сделаем вот такую БД:
Схема связей БД
image

Для нетерпеливых: код, готовый к запуску находится в этом репозитории. Интерактивная схема БД здесь. Шпаргалка по составлению ORM запросов находится в конце статьи.

Договоримся, что в тексте статьи мы будем использовать слово Таблица вместо Отношение, и слово Поле вместо Аттрибута. По заданию нам надо таблицу с музыкальными файлами поместить в БД, при этом устранив избыточность данных. В исходной таблице (формат CSV) имеются следующие поля (track, genre, musician, album, length, album_year, collection, collection_year). Связи между ними такие:
каждый музыкант может петь в нескольких жанрах, как и в одном жанре могут выступать несколько музыкантов (отношение многие ко многим).
в создании альбома могут участвовать один или несколько музыкантов (отношение многие ко многим).
трек принадлежит только одному альбому (отношение один ко многим)
треки могут в ходить в состав нескольких сборников (отношение многие ко многим)
трек может не входить ни в одну в коллекцию.

Для упрощения предположим что названия жанров, имена музыкантов, названия альбомов и коллекций не повторяются. Названия треков могут повторяться. В БД мы запроектировали 8 таблиц:
genres (жанры)
genres_musicians (промежуточная таблица)
musicians (музыканты)
albums_musicians (промежуточная таблица)
albums (альбомы)
tracks (треки)
collections_tracks (промежуточная таблица)
collections (коллекции)
* данная схема тестовая, взята из одного из ДЗ, в ней есть некоторые недостатки например нет связи треков с музыкантом, а также трека с жанром. Но для обучения это несущественно, и мы опустим этот недостаток.

Для теста я создал две БД на локальном Postgres: TestSQL и TestORM, доступ к ним: логин и пароль test. Давайте наконец писать код!

Создаем подключения и таблицы


Создаем подключения к БД
* код функций read_data и clear_db есть в репозитории.
DSN_SQL = 'postgresql://test:test@localhost:5432/TestSQL'    DSN_ORM = 'postgresql://test:test@localhost:5432/TestORM'    # Прочитаем данные из CSV в память в виде словаря.    DATA = read_data('data/demo-data.csv')    print('Connecting to DB\'s...')    # Мы будем работать с сессиями, поэтому создадим их раздельными для каждой БД.    engine_orm = sa.create_engine(DSN_ORM)    Session_ORM = sessionmaker(bind=engine_orm)    session_orm = Session_ORM()    engine_sql = sa.create_engine(DSN_SQL)    Session_SQL = sessionmaker(bind=engine_sql)    session_sql = Session_SQL()    print('Clearing the bases...')    # Удаляем все таблицы из БД перед заливкой содержимого. Используем только для учебы.    clear_db(sa, engine_sql)    clear_db(sa, engine_orm)


Создаем таблицы классическим путем через SQL
* код функции read_query есть в репозитории. Тексты запросов также есть в репозитории.
print('\nPreparing data for SQL job...')    print('Creating empty tables...')    session_sql.execute(read_query('queries/create-tables.sql'))    session_sql.commit()    print('\nAdding musicians...')    query = read_query('queries/insert-musicians.sql')    res = session_sql.execute(query.format(','.join({f"('{x['musician']}')" for x in DATA})))    print(f'Inserted {res.rowcount} musicians.')    print('\nAdding genres...')    query = read_query('queries/insert-genres.sql')    res = session_sql.execute(query.format(','.join({f"('{x['genre']}')" for x in DATA})))    print(f'Inserted {res.rowcount} genres.')    print('\nLinking musicians with genres...')    # assume that musician + genre has to be unique    genres_musicians = {x['musician'] + x['genre']: [x['musician'], x['genre']] for x in DATA}    query = read_query('queries/insert-genre-musician.sql')    # this query can't be run in batch, so execute one by one    res = 0    for key, value in genres_musicians.items():        res += session_sql.execute(query.format(value[1], value[0])).rowcount    print(f'Inserted {res} connections.')    print('\nAdding albums...')    # assume that albums has to be unique    albums = {x['album']: x['album_year'] for x in DATA}    query = read_query('queries/insert-albums.sql')    res = session_sql.execute(query.format(','.join({f"('{x}', '{y}')" for x, y in albums.items()})))    print(f'Inserted {res.rowcount} albums.')    print('\nLinking musicians with albums...')    # assume that musicians + album has to be unique    albums_musicians = {x['musician'] + x['album']: [x['musician'], x['album']] for x in DATA}    query = read_query('queries/insert-album-musician.sql')    # this query can't be run in batch, so execute one by one    res = 0    for key, values in albums_musicians.items():        res += session_sql.execute(query.format(values[1], values[0])).rowcount    print(f'Inserted {res} connections.')    print('\nAdding tracks...')    query = read_query('queries/insert-track.sql')    # this query can't be run in batch, so execute one by one    res = 0    for item in DATA:        res += session_sql.execute(query.format(item['track'], item['length'], item['album'])).rowcount    print(f'Inserted {res} tracks.')    print('\nAdding collections...')    query = read_query('queries/insert-collections.sql')    res = session_sql.execute(query.format(','.join({f"('{x['collection']}', {x['collection_year']})" for x in DATA if x['collection'] and x['collection_year']})))    print(f'Inserted {res.rowcount} collections.')    print('\nLinking collections with tracks...')    query = read_query('queries/insert-collection-track.sql')    # this query can't be run in batch, so execute one by one    res = 0    for item in DATA:        res += session_sql.execute(query.format(item['collection'], item['track'])).rowcount    print(f'Inserted {res} connections.')    session_sql.commit()



По сути мы создаем пакетами справочники (жанры, музыкантов, альбомы, коллекции), а затем в цикле связываем остальные данные и строим вручную промежуточные таблицы. Запускаем код и видим что БД создалась. Главное не забыть вызывать commit() у сессии.

Теперь пробуем сделать тоже самое, но с применением ORM подхода. Для того чтобы работать с ORM нам надо описать классы данных. Для этого мы создадим 8 классов (по одному на кажую таблицу).
Заголовок спойлера
Код скрипта объявления классов.
Base = declarative_base()class Genre(Base):    __tablename__ = 'genres'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(20), unique=True)    # Объявляется отношение многие ко многим к Musician через промежуточную таблицу genres_musicians    musicians = relationship("Musician", secondary='genres_musicians')class Musician(Base):    __tablename__ = 'musicians'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(50), unique=True)    # Объявляется отношение многие ко многим к Genre через промежуточную таблицу genres_musicians    genres = relationship("Genre", secondary='genres_musicians')    # Объявляется отношение многие ко многим к Album через промежуточную таблицу albums_musicians    albums = relationship("Album", secondary='albums_musicians')class GenreMusician(Base):    __tablename__ = 'genres_musicians'    # здесь мы объявляем составной ключ, состоящий из двух полей    __table_args__ = (PrimaryKeyConstraint('genre_id', 'musician_id'),)    # В промежуточной таблице явно указываются что следующие поля являются внешними ключами    genre_id = sa.Column(sa.Integer, sa.ForeignKey('genres.id'))    musician_id = sa.Column(sa.Integer, sa.ForeignKey('musicians.id'))class Album(Base):    __tablename__ = 'albums'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(50), unique=True)    year = sa.Column(sa.Integer)    # Объявляется отношение многие ко многим к Musician через промежуточную таблицу albums_musicians    musicians = relationship("Musician", secondary='albums_musicians')class AlbumMusician(Base):    __tablename__ = 'albums_musicians'    # здесь мы объявляем составной ключ, состоящий из двух полей    __table_args__ = (PrimaryKeyConstraint('album_id', 'musician_id'),)    # В промежуточной таблице явно указываются что следующие поля являются внешними ключами    album_id = sa.Column(sa.Integer, sa.ForeignKey('albums.id'))    musician_id = sa.Column(sa.Integer, sa.ForeignKey('musicians.id'))class Track(Base):    __tablename__ = 'tracks'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(100))    length = sa.Column(sa.Integer)    # Поскольку по полю album_id идет связь один ко многим, достаточно указать чей это внешний ключ    album_id = sa.Column(sa.Integer, ForeignKey('albums.id'))    # Объявляется отношение многие ко многим к Collection через промежуточную таблицу collections_tracks    collections = relationship("Collection", secondary='collections_tracks')class Collection(Base):    __tablename__ = 'collections'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(50))    year = sa.Column(sa.Integer)    # Объявляется отношение многие ко многим к Track через промежуточную таблицу collections_tracks    tracks = relationship("Track", secondary='collections_tracks')class CollectionTrack(Base):    __tablename__ = 'collections_tracks'    # здесь мы объявляем составной ключ, состоящий из двух полей    __table_args__ = (PrimaryKeyConstraint('collection_id', 'track_id'),)    # В промежуточной таблице явно указываются что следующие поля являются внешними ключами    collection_id = sa.Column(sa.Integer, sa.ForeignKey('collections.id'))    track_id = sa.Column(sa.Integer, sa.ForeignKey('tracks.id'))


Нам достаточно создать базовый класс Base для декларативного стиля описания таблиц и унаследоваться от него. Вся магия отношений между таблицами заключается в правильном использовании relationship и ForeignKey. В коде указано в каком случае мы создаем какое отношение. Главное не забыть прописать relationship с обеих сторон связи многие ко многим.

Непосредственно создание таблиц с использованием ORM подхода происходит путем вызова:
Base.metadata.create_all(engine_orm)

И вот тут включается магия, буквально все классы, объявленные в коде через наследование от Base становятся таблицами. Сходу я не увидел как указать экземпляры каких классов надо создать сейчас, а какие отложить для создания позже (например в другой БД). Наверняка такой способ есть, но в нашем коде все классы-наследники Base инстанцируются одномоментно, имейте это ввиду.


Наполнение таблиц при использовании ORM подхода выглядит так:
Заполнение таблиц данными через ORM
    print('\nPreparing data for ORM job...')    for item in DATA:        # создаем жанры        genre = session_orm.query(Genre).filter_by(name=item['genre']).scalar()        if not genre:            genre = Genre(name=item['genre'])        session_orm.add(genre)        # создаем музыкантов        musician = session_orm.query(Musician).filter_by(name=item['musician']).scalar()        if not musician:            musician = Musician(name=item['musician'])        musician.genres.append(genre)        session_orm.add(musician)        # создаем альбомы        album = session_orm.query(Album).filter_by(name=item['album']).scalar()        if not album:            album = Album(name=item['album'], year=item['album_year'])        album.musicians.append(musician)        session_orm.add(album)        # создаем треки        # проверяем на существование трек не только по имени но и по альбому, так как имя трека по условию может        # быть не уникально        track = session_orm.query(Track).join(Album).filter(and_(Track.name == item['track'],                                                                 Album.name == item['album'])).scalar()        if not track:            track = Track(name=item['track'], length=item['length'])        track.album_id = album.id        session_orm.add(track)        # создаем коллекции, учитываем что трек может не входить ни в одну в коллекцию        if item['collection']:            collection = session_orm.query(Collection).filter_by(name=item['collection']).scalar()            if not collection:                collection = Collection(name=item['collection'], year=item['collection_year'])            collection.tracks.append(track)            session_orm.add(collection)        session_orm.commit()


Приходится поштучно заполнять каждый справочник (жанры, музыканты, альбомы, коллекции). В случае SQL запросов можно было генерировать пакетное добавление данных. Зато промежуточные таблицы в явном виде не надо создавать, за это отвечают внутренние механизмы SQLAlchemy.

Запросы к базам



По заданию нам надо написать 15 запросов используя обе техники SQL и ORM. Вот список поставленных вопросов в порядке возрастания сложности:
1. название и год выхода альбомов, вышедших в 2018 году;
2. название и продолжительность самого длительного трека;
3. название треков, продолжительность которых не менее 3,5 минуты;
4. названия сборников, вышедших в период с 2018 по 2020 год включительно;
5. исполнители, чье имя состоит из 1 слова;
6. название треков, которые содержат слово мой/my.
7. количество исполнителей в каждом жанре;
8. количество треков, вошедших в альбомы 2019-2020 годов;
9. средняя продолжительность треков по каждому альбому;
10. все исполнители, которые не выпустили альбомы в 2020 году;
11. названия сборников, в которых присутствует конкретный исполнитель;
12. название альбомов, в которых присутствуют исполнители более 1 жанра;
13. наименование треков, которые не входят в сборники;
14. исполнителя(-ей), написавшего самый короткий по продолжительности трек (теоретически таких треков может быть несколько);
15. название альбомов, содержащих наименьшее количество треков.
Как видите, вышеизложенные вопросы подразумевают как простую выборку так и с объединением таблиц, а также использование агрегатных функций.

Ниже предоставлены решения по каждому из 15 запросов в двух вариантах (используя SQL и ORM). В коде запросы идут парами, чтобы показать идентичность результатов на выводе в консоль.
Запросы и их краткое описание
    print('\n1. All albums from 2018:')    query = read_query('queries/select-album-by-year.sql').format(2018)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Album).filter_by(year=2018):        print(item.name)    print('\n2. Longest track:')    query = read_query('queries/select-longest-track.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track).order_by(Track.length.desc()).slice(0, 1):        print(f'{item.name}, {item.length}')    print('\n3. Tracks with length not less 3.5min:')    query = read_query('queries/select-tracks-over-length.sql').format(310)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track).filter(310 <= Track.length).order_by(Track.length.desc()):        print(f'{item.name}, {item.length}')    print('\n4. Collections between 2018 and 2020 years (inclusive):')    query = read_query('queries/select-collections-by-year.sql').format(2018, 2020)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Collection).filter(2018 <= Collection.year,                                                     Collection.year <= 2020):        print(item.name)    print('\n5. Musicians with name that contains not more 1 word:')    query = read_query('queries/select-musicians-by-name.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Musician).filter(Musician.name.notlike('%% %%')):        print(item.name)    print('\n6. Tracks that contains word "me" in name:')    query = read_query('queries/select-tracks-by-name.sql').format('me')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track).filter(Track.name.like('%%me%%')):        print(item.name)    print('Ok, let\'s start serious work')    print('\n7. How many musicians plays in each genres:')    query = read_query('queries/count-musicians-by-genres.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Genre).join(Genre.musicians).order_by(func.count(Musician.id).desc()).group_by(            Genre.id):        print(f'{item.name}, {len(item.musicians)}')    print('\n8. How many tracks in all albums 2019-2020:')    query = read_query('queries/count-tracks-in-albums-by-year.sql').format(2019, 2020)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track, Album).join(Album).filter(2019 <= Album.year, Album.year <= 2020):        print(f'{item[0].name}, {item[1].year}')    print('\n9. Average track length in each album:')    query = read_query('queries/count-average-tracks-by-album.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Album, func.avg(Track.length)).join(Track).order_by(func.avg(Track.length)).group_by(            Album.id):        print(f'{item[0].name}, {item[1]}')    print('\n10. All musicians that have no albums in 2020:')    query = read_query('queries/select-musicians-by-album-year.sql').format(2020)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Musician).join(Musician.albums).filter(Album.year != 2020).order_by(            Musician.name.asc()):        print(f'{item.name}')    print('\n11. All collections with musician Steve:')    query = read_query('queries/select-collection-by-musician.sql').format('Steve')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Collection).join(Collection.tracks).join(Album).join(Album.musicians).filter(            Musician.name == 'Steve').order_by(Collection.name):        print(f'{item.name}')    print('\n12. Albums with musicians that play in more than 1 genre:')    query = read_query('queries/select-albums-by-genres.sql').format(1)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Album).join(Album.musicians).join(Musician.genres).having(func.count(distinct(            Genre.name)) > 1).group_by(Album.id).order_by(Album.name):        print(f'{item.name}')    print('\n13. Tracks that not included in any collections:')    query = read_query('queries/select-absence-tracks-in-collections.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    # Important! Despite the warning, following expression does not work: "Collection.id is None"    for item in session_orm.query(Track).outerjoin(Track.collections).filter(Collection.id == None):        print(f'{item.name}')    print('\n14. Musicians with shortest track length:')    query = read_query('queries/select-musicians-min-track-length.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    subquery = session_orm.query(func.min(Track.length))    for item in session_orm.query(Musician, Track.length).join(Musician.albums).join(Track).group_by(            Musician.id, Track.length).having(Track.length == subquery).order_by(Musician.name):        print(f'{item[0].name}, {item[1]}')    print('\n15. Albums with minimum number of tracks:')    query = read_query('queries/select-albums-with-minimum-tracks.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    subquery1 = session_orm.query(func.count(Track.id)).group_by(Track.album_id).order_by(func.count(Track.id)).limit(1)    subquery2 = session_orm.query(Track.album_id).group_by(Track.album_id).having(func.count(Track.id) == subquery1)    for item in session_orm.query(Album).join(Track).filter(Track.album_id.in_(subquery2)).order_by(Album.name):        print(f'{item.name}')


Для тех, кому не хочется погружаться в чтение кода, я попробую показать как выглядит сырой SQL и его альтернатива в ORM выражении, поехали!

Шпаргалка по сопоставлению SQL запросов и ORM выражений



1. название и год выхода альбомов, вышедших в 2018 году:
SQL
select namefrom albumswhere year=2018

ORM
session_orm.query(Album).filter_by(year=2018)


2. название и продолжительность самого длительного трека:
SQL
select name, lengthfrom tracksorder by length DESClimit 1

ORM
session_orm.query(Track).order_by(Track.length.desc()).slice(0, 1)


3. название треков, продолжительность которых не менее 3,5 минуты:
SQL
select name, lengthfrom trackswhere length >= 310order by length DESC

ORM
session_orm.query(Track).filter(310 <= Track.length).order_by(Track.length.desc())


4. названия сборников, вышедших в период с 2018 по 2020 год включительно:
SQL
select namefrom collectionswhere (year >= 2018) and (year <= 2020)

ORM
session_orm.query(Collection).filter(2018 <= Collection.year, Collection.year <= 2020)

* обратите внимание что здесь и далее фильтрация задается уже с использованием filter, а не с использованием filter_by.

5. исполнители, чье имя состоит из 1 слова:
SQL
select namefrom musicianswhere not name like '%% %%'

ORM
session_orm.query(Musician).filter(Musician.name.notlike('%% %%'))


6. название треков, которые содержат слово мой/my:
SQL
select namefrom trackswhere name like '%%me%%'

ORM
session_orm.query(Track).filter(Track.name.like('%%me%%'))


7. количество исполнителей в каждом жанре:
SQL
select g.name, count(m.name)from genres as gleft join genres_musicians as gm on g.id = gm.genre_idleft join musicians as m on gm.musician_id = m.idgroup by g.nameorder by count(m.name) DESC

ORM
session_orm.query(Genre).join(Genre.musicians).order_by(func.count(Musician.id).desc()).group_by(Genre.id)


8. количество треков, вошедших в альбомы 2019-2020 годов:
SQL
select t.name, a.yearfrom albums as aleft join tracks as t on t.album_id = a.idwhere (a.year >= 2019) and (a.year <= 2020)

ORM
session_orm.query(Track, Album).join(Album).filter(2019 <= Album.year, Album.year <= 2020)


9. средняя продолжительность треков по каждому альбому:
SQL
select a.name, AVG(t.length)from albums as aleft join tracks as t on t.album_id = a.idgroup by a.nameorder by AVG(t.length)

ORM
session_orm.query(Album, func.avg(Track.length)).join(Track).order_by(func.avg(Track.length)).group_by(Album.id)


10. все исполнители, которые не выпустили альбомы в 2020 году:
SQL
select distinct m.namefrom musicians as mleft join albums_musicians as am on m.id = am.musician_idleft join albums as a on a.id = am.album_idwhere not a.year = 2020order by m.name

ORM
session_orm.query(Musician).join(Musician.albums).filter(Album.year != 2020).order_by(Musician.name.asc())


11. названия сборников, в которых присутствует конкретный исполнитель (Steve):
SQL
select distinct c.namefrom collections as cleft join collections_tracks as ct on c.id = ct.collection_idleft join tracks as t on t.id = ct.track_idleft join albums as a on a.id = t.album_idleft join albums_musicians as am on am.album_id = a.idleft join musicians as m on m.id = am.musician_idwhere m.name like '%%Steve%%'order by c.name

ORM
session_orm.query(Collection).join(Collection.tracks).join(Album).join(Album.musicians).filter(Musician.name == 'Steve').order_by(Collection.name)


12. название альбомов, в которых присутствуют исполнители более 1 жанра:
SQL
select a.namefrom albums as aleft join albums_musicians as am on a.id = am.album_idleft join musicians as m on m.id = am.musician_idleft join genres_musicians as gm on m.id = gm.musician_idleft join genres as g on g.id = gm.genre_idgroup by a.namehaving count(distinct g.name) > 1order by a.name

ORM
session_orm.query(Album).join(Album.musicians).join(Musician.genres).having(func.count(distinct(Genre.name)) > 1).group_by(Album.id).order_by(Album.name)


13. наименование треков, которые не входят в сборники:
SQL
select t.namefrom tracks as tleft join collections_tracks as ct on t.id = ct.track_idwhere ct.track_id is null

ORM
session_orm.query(Track).outerjoin(Track.collections).filter(Collection.id == None)

* обратите внимание что несмотря на предупреждение в PyCharm надо именно так составлять условие фильтрации, если написать как предлагает IDE (Collection.id is None) то оно работать не будет.

14. исполнителя(-ей), написавшего самый короткий по продолжительности трек (теоретически таких треков может быть несколько):
SQL
select m.name, t.lengthfrom tracks as tleft join albums as a on a.id = t.album_idleft join albums_musicians as am on am.album_id = a.idleft join musicians as m on m.id = am.musician_idgroup by m.name, t.lengthhaving t.length = (select min(length) from tracks)order by m.name

ORM
subquery = session_orm.query(func.min(Track.length))session_orm.query(Musician, Track.length).join(Musician.albums).join(Track).group_by(Musician.id, Track.length).having(Track.length == subquery).order_by(Musician.name)


15. название альбомов, содержащих наименьшее количество треков:
SQL
select distinct a.namefrom albums as aleft join tracks as t on t.album_id = a.idwhere t.album_id in (    select album_id    from tracks    group by album_id    having count(id) = (        select count(id)        from tracks        group by album_id        order by count        limit 1    ))order by a.name

ORM
subquery1 = session_orm.query(func.count(Track.id)).group_by(Track.album_id).order_by(func.count(Track.id)).limit(1)subquery2 = session_orm.query(Track.album_id).group_by(Track.album_id).having(func.count(Track.id) == subquery1)session_orm.query(Album).join(Track).filter(Track.album_id.in_(subquery2)).order_by(Album.name)


Как видите, вышеизложенные вопросы подразумевают как простую выборку так и с объединением таблиц, а также использование агрегатных функций и подзапросов. Все это реально сделать с SQLAlchemy как в режиме SQL так и в режиме ORM. Разноообразие операторов и методов позволяет выполнить запрос наверное любой сложности.

Надеюсь данный материал поможет избавиться начинающим быстро и эффективно начать составлять запросы.
Подробнее..
Категории: Python , Sql , Python 3 , Sqlalchemy , Orm

SQLAlchemy а ведь раньше я презирал ORM

05.06.2021 22:17:23 | Автор: admin

Так вышло, что на заре моей карьеры в IT меня покусал Oracle -- тогда я ещё не знал ни одной ORM, но уже шпарил SQL и знал, насколько огромны возможности БД.

Знакомство с DjangoORM ввело меня в глубокую фрустрацию. Вместо возможностей -- хрена с два, а не составной первичный ключ или оконные функции. Специфические фичи БД проще забыть. Добивало то, что по цене нулевой гибкости мне продавали падение же производительности -- сборка ORM-запроса не бесплатная. Ну и вишенка на торте -- в дополнение к синтаксису SQL надо знать ещё и синтаксис ORM, который этот SQL сгенерирует. Недостатки, которые я купил за дополнительную когнитивную нагрузку -- вот уж где достижение индустрии. Поэтому я всерьёз считал, что без ORM проще, гибче и в разы производительнее -- ведь у вас в руках все возможности БД.

Так вот, эта история с SQLAlchemy -- счастливая история о том, как я заново открыл для себя ORM. В этой статье я расскажу, как я вообще докатился до такой жизни, о некоторых подводных камнях SQLAlchemy, и под конец перейду к тому, что вызвало у меня бурный восторг, которым попытаюсь с вами поделиться.

Опыт и как результат субъективная система взглядов

Я занимался оптимизацией SQL-запросов. Мне удавалось добиться стократного и более уменьшения cost запросов, в основном для Oracle и Firebird. Я проводил исследования, экспериментировал с индексами. Я видел в жизни много схем БД: среди них были как некоторое дерьмо, так и продуманные гибкие и расширяемые инженерные решения.

Этот опыт сформировал у меня систему взглядов касательно БД:

  • ORM не позволяет забыть о проектировании БД, если вы не хотите завтра похоронить проект

  • Переносимость -- миф, а не аргумент:

    • Если ваш проект работает с postgres через ORM, то вы на локальной машине разворачиваете в докере postgres, а не работаете с sqlite

    • Вы часто сталкивались с переходом на другую БД? Не пишите только "Однажды мы решили переехать..." -- это было однажды. Если же это происходит часто или заявленная фича, оправданная разными условиями эксплуатации у разных ваших клиентов -- милости прошу в обсуждения

    • У разных БД свои преимущества и болячки, всё это обусловлено разными структурами данных и разными инженерными решениями. И если при написании приложения мы используем верхний мозг, мы пытаемся избежать этих болячек. Тема глубокая, и рассмотрена в циклах лекций Базы данных для программиста и Транзакции от Владимира Кузнецова

  • Структура таблиц определяется вашими данными, а не ограничениями вашей ORM

Естественно, я ещё и код вне БД писал, и касательно этого кода у меня тоже сформировалась система взглядов:

  • Контроллер должен быть тонким, а лучший код -- это тот код, которого нет. Код ORM -- это часть контроллера. И если код контроллера спрятан в библиотеку, это не значит, что он стал тонким -- он всё равно исполняется

  • Контроллер, выполняющий за один сеанс много обращений к БД -- это очень тонкий лёд

  • Я избегаю повсеместного использования ActiveRecord -- это верный способ как работать с неконсистентными данными, так и незаметно для себя сгенерировать бесконтрольное множество обращений к БД

  • Оптимизация работы с БД сводится к тому, что мы не читаем лишние данные. Есть смысл запросить только интересующий нас список колонок

  • Часть данных фронт всё равно запрашивает при инициализации. Чаще всего это категории. В таких случаях нам достаточно отдать только id

  • Отладка всех новых запросов ORM обязательна. Всегда надо проверять, что там ORM высрала (тут пара сочных примеров), дабы не было круглых глаз. Даже при написании этой статьи у меня был косяк как раз по этому пункту

Идея сокращения по возможности количества выполняемого кода в контроллере приводит меня к тому, что проще всего возиться не с сущностями, а сразу запросить из БД в нужном виде данные, а выхлоп можно сразу отдать сериализатору JSON.

Все вопросы данной статьи происходят из моего опыта и системы взглядов

Они могут и не найти в вас отголоска, и это нормально

Мы разные, и у нас всех разный фокус внимания. Я общался с разными разработчиками. Я видел разные позиции, от "да не всё ли равно, что там происходит? Работает же" до "я художник, у меня справка есть". При этом у некоторых из них были другие сильные стороны. Различие позиций -- это нормально. Невозможно фокусироваться на всех аспектах одновременно.

Мне, например, с большего без разницы, как по итогу фронт визуализирует данные, хотя я как бы фулстэк. Чем я отличаюсь от "да не всё ли равно, что там происходит"? Протокол? Да! Стратегия и оптимизация рендеринга? Да! Упороться в WebGL? Да! А что по итогу на экране -- пофиг.

Знакомство в SQLAlchemy

Первое, что бросилось в глаза -- возможность писать DML-запросы в стиле SQL, но в синтаксисе python:

order_id = bindparam('order_id', required=True)return \    select(        func.count(Product.id).label("product_count"),        func.sum(Product.price).label("order_price"),        Customer.name,    )\    .select_from(Order)\    .join(        Product,        onclause=(Product.id == Order.product_id),    )\    .join(        Customer,        onclause=(Customer.id == Order.customer_id),    )\    .where(        Order.id == order_id,    )\    .group_by(        Order.id,    )\    .order_by(        Product.id.desc(),    )

Этим примером кода я хочу сказать, что ORM не пытается изобрести свои критерии, вместо этого она пытается дать нечто, максимально похожее на SQL. К сожалению, я заменил реальный фрагмент ORM-запроса текущего проекта, ибо NDA. Пример крайне примитивен -- он даже без подзапросов. Кажется, в моём текущем проекте таких запросов единицы.

Естественно, я сразу стал искать, как тут дела с составными первичными ключами -- и они есть! И оконные функции, и CTE, и явный JOIN, и много чего ещё! Для особо тяжёлых случаев можно даже впердолить SQL хинты! Дальнейшее погружение продолжает радовать: я не сталкивался ни с одним вопросом, который решить было невозможно из-за архитектурных ограничений. Правда, некоторые свои вопросы я решал через monkey-patching.

Производительность

Насколько крутым и гибким бы ни было API, краеугольным камнем является вопрос производительности. Сегодня вам может и хватит 10 rps, а завтра вы пытаетесь масштабироваться, и если затык в БД -- поздравляю, вы мертвы.

Производительность query builder в SQLAlchemy оставляет желать лучшего. Благо, это уровень приложения, и тут масштабирование вас спасёт. Но можно ли это как-то обойти? Можно ли как-то нивелировать низкую производительность query builder? Нет, серьёзно, какой смысл тратить мощности ради увеличения энтропии Вселенной?

В принципе, нам на python не привыкать искать обходные пути: например, python непригоден для реализации числодробилок, поэтому вычисления принято выкидывать в сишные либы.

Для SQLAlchemy тоже есть обходные пути, и их сразу два, и оба сводятся к кэшированию по разным стратегиям. Первый -- применение bindparam и lru_cache. Второй предлагает документация -- future_select. Рассмотрим их преимущества и недостатки.

bindparam + lru_cache

Это самое простое и при этом самое производительное решение. Мы покупаем производительность по цене памяти -- просто кэшируем собранный объект запроса, который в себе кэширует отрендеренный запрос. Это выгодно до тех пор, пока нам не грозит комбинаторный взрыв, то есть пока число вариаций запроса находится в разумных пределах. В своём проекте в большинстве представлений я использую именно этот подход. Для удобства я применяю декоратор cached_classmethod, реализующий композицию декораторов classmethod и lru_cache:

from functools import lru_cachedef cached_classmethod(target):    cache = lru_cache(maxsize=None)    cached = cache(target)    cached = classmethod(cached)    return cached

Для статических представлений тут всё понятно -- функция, создающая ORM-запрос не должна принимать параметров. Для динамических представлений можно добавить аргументы функции. Так как lru_cache под капотом использует dict, аргументы должны быть хешируемыми. Я остановился на варианте, когда функция-обработчик запроса генерирует "сводку" запроса и параметры, передаваемые в сгенерированный запрос во время непосредственно исполнения. "Сводка" запроса реализует что-то типа плана ORM-запроса, на основании которой генерируется сам объект запроса -- это хешируемый инстанс frozenset, который в моём примере называется query_params:

class BaseViewMixin:    def build_query_plan(self):        self.query_kwargs = {}        self.query_params = frozenset()    async def main(self):        self.build_query_plan()        query = self.query(self.query_params)        async with BaseModel.session() as session:            respone = await session.execute(                query,                self.query_kwargs,            )            mappings = respone.mappings()        return self.serialize(mappings)
Некоторое пояснение по query_params и query_kwargs

В простейшем случае query_params можно получить, просто преобразовав ключи query_kwargs во frozenset. Обращаю ваше внимание, что это не всегда справедливо: флаги в query_params запросто могут поменять сам SQL-запрос при неизменных query_kwargs.

На всякий случай предупреждаю: не стоит слепо копировать код. Разберитесь с ним, адаптируйте под свой проект. Даже у меня данный код на самом деле выглядит немного иначе, он намеренно упрощён, из него выкинуты некоторые несущественные детали.

Сколько же памяти я заплатил за это? А немного. На все вариации запросов я расходую не более мегабайта.

future_select

В отличие от дубового первого варианта, future_select кэширует куски SQL-запросов, из которых итоговый запрос собирается очень быстро. Всем хорош вариант: и высокая производительность, и низкое потребление памяти. Читать такой код сложно, сопровождать дико:

stmt = lambdas.lambda_stmt(lambda: future_select(Customer))stmt += lambda s: s.where(Customer.id == id_)

Этот вариант я обязательно задействую, когда дело будет пахнуть комбинаторным взрывом.

Наброски фасада, решающего проблему дикого синтаксиса

По идее, future_select через FutureSelectWrapper можно пользоваться почти как старым select, что нивелирует дикий синтаксис:

class FutureSelectWrapper:    def __init__(self, clause):        self.stmt = lambdas.lambda_stmt(            lambda: future_select(clause)        )        def __getattribute__(self, name):        def outer(clause):            def inner(s):                callback = getattr(s, name)                return callback(clause)                        self.stmt += inner            return self        return outer

Я обращаю ваше внимание, что это лишь наброски. Я их ни разу не запускал. Необходимы дополнительные исследования.

Промежуточный вывод: низкую производительность query builder в SQLAlchemy можно нивелировать кэшем запросов. Дикий синтаксис future_select можно спрятать за фасадом.

А ещё я не уделил должного внимания prepared statements. Эти исследования я проведу чуть позже.

Как я открывал для себя ORM заново

Мы добрались главного -- ради этого раздела я писал статью. В этом разделе я поделюсь своими откровениями, посетившими меня в процессе работы.

Модульность

Когда я реализовывал на SQL дикую аналитику, старой болью отозвалось отсутствие модульности и интроспекции. При последующем переносе на ORM у меня уже была возможность выкинуть весь подзапрос поля FROM в отдельную функцию (по факту метод класса), а в последующем эти функции было легко комбинировать и на основании флагов реализовывать паттерн Стратегия, а также исключать дублирование одинакового функционала через наследование.

Собственные типы

Если данные обладают хитрым поведением, или же хитро преобразуются, совершенно очевидно, что их надо выкинуть на уровень модели. Я столкнулся с двумя вопросами: хранение цвета и работа с ENUM. Погнали по порядку.

Создание собственных простых типов рассмотрено в документации:

class ColorType(TypeDecorator):    impl = Integer    cache_ok = True    def process_result_value(self, value, dialect):        if value is None:            return        return color(value)    def process_bind_param(self, value, dialect):        if value is None:            return        value = color(value)        return value.value

Сыр-бор тут только в том, что мне стрельнуло хранить цвета не строками, а интами. Это исключает некорректность данных, но усложняет их сериализацию и десериализацию.

Теперь про ENUM. Меня категорически не устроило, что документация предлагает хранить ENUM в базе в виде VARCHAR. Особенно уникальные целочисленные Enum хотелось хранить интами. Очевидно, объявлять этот тип мы должны, передавая аргументом целевой Enum. Ну раз String при объявлении требует указать длину -- задача, очевидно, уже решена. Штудирование исходников вывело меня на TypeEngine -- и тут вместо примеров использования вас встречает "our source code is open 24/7". Но тут всё просто:

class IntEnumField(TypeEngine):    def __init__(self, target_enum):        self.target_enum = target_enum        self.value2member_map = target_enum._value2member_map_        self.member_map = target_enum._member_map_    def get_dbapi_type(self, dbapi):        return dbapi.NUMBER    def result_processor(self, dialect, coltype):        def process(value):            if value is None:                return            member = self.value2member_map[value]            return member.name        return process    def bind_processor(self, dialect):        def process(value):            if value is None:                return            member = self.member_map[value]            return member.value        return process

Обратите внимание: обе функции -- result_processor и bind_processor -- должны вернуть функцию.

Собственные функции, тайп-хинты и вывод типов

Дальше больше. Я столкнулся со странностями реализации json_arrayagg в mariadb: в случае пустого множества вместо NULL возвращается строка "[NULL]" -- что ни под каким соусом не айс. Как временное решение я накостылил связку из group_concat, coalesce и concat. В принципе неплохо, но:

  1. При вычитывании результата хочется нативного преобразования строки в JSON.

  2. Если делать что-то универсальное, то оказывается, что строки надо экранировать. Благо, есть встроенная функция json_quote. Про которую SQLAlchemy не знает.

  3. А ещё хочется найти workaround-функции в объекте sqlalchemy.func

Оказывается, в SQLAlchemy эти проблемы решаются совсем влёгкую. И если тайп-хинты мне показались просто удобными, то вывод типов поверг меня в восторг: типозависимое поведение можно инкапсулировать в саму функцию, что сгенерирует правильный код на SQL.

Мне заказчик разрешил опубликовать код целого модуля!
from sqlalchemy.sql.functions import GenericFunction, register_functionfrom sqlalchemy.sql import sqltypesfrom sqlalchemy import func, literal_columndef register(target):    name = target.__name__    register_function(name, target)    return target# === Database functions ===class json_quote(GenericFunction):    type = sqltypes.String    inherit_cache = Trueclass json_object(GenericFunction):    type = sqltypes.JSON    inherit_cache = True# === Macro ===empty_string = literal_column("''", type_=sqltypes.String)json_array_open = literal_column("'['", type_=sqltypes.String)json_array_close = literal_column("']'", type_=sqltypes.String)@registerdef json_arrayagg_workaround(clause):    clause_type = clause.type    if isinstance(clause_type, sqltypes.String):        clause = func.json_quote(clause)    clause = func.group_concat(clause)    clause = func.coalesce(clause, empty_string)    return func.concat(        json_array_open,        clause,        json_array_close,        type_=sqltypes.JSON,    )def __json_pairs_iter(clauses):    for clause in clauses:        clause_name = clause.name        clause_name = "'%s'" % clause_name        yield literal_column(clause_name, type_=sqltypes.String)        yield clause@registerdef json_object_wrapper(*clauses):    json_pairs = __json_pairs_iter(clauses)    return func.json_object(*json_pairs)

В рамках эксперимента я также написал функцию json_object_wrapper, которая из переданных полей собирает json, где ключи -- это имена полей. Буду использовать или нет -- ХЗ. Причём тот факт, что эти макроподстановки не просто работают, а даже правильно, меня немного пугает.

Примеры того, что генерирует ORM
SELECT concat(  '[',  coalesce(group_concat(product.tag_id), ''),  ']') AS product_tags
SELECT json_object(  'name', product.name,  'price', product.price) AS product,

PS: Да, в случае json_object_wrapper я изначально допустил ошибку. Я человек простой: вижу константу -- вношу её в код. Что привело к ненужным bindparam на месте ключей этого json_object. Мораль -- держите ORM в ежовых рукавицах. Упустите что-то -- и она вам такого нагенерит! Только literal_column позволяет надёжно захардкодить константу в тело SQL-запроса.

Такие макроподстановки позволяют сгенерировать огромную кучу SQL кода, который будет выполнять логику формирования представлений. И что меня восхищает -- эта куча кода работает эффективно. Ещё интересный момент -- эти макроподстановки позволят прозрачно реализовать паттерн Стратегия -- я надеюсь, поведение json_arrayagg пофиксят в следующих релизах MariaDB, и тогда я смогу своё костылище заменить на связку json_arrayagg+coalesce незаметно для клиентского кода.

Выводы

SQLAlchemy позволяет использовать преимущества наследования и полиморфизма (и даже немного иннкапсуляции. Флеш-рояль, однако) в SQL. При этом она не загоняет вас в рамки задач уровня Hello, World! архитектурными ограничениями, а наоборот даёт вам максимум возможностей.

Субъективно это прорыв. Я обожаю реляционные базочки, и наконец-то я получаю удовольствие от реализации хитрозакрученной аналитики. У меня в руках все преимущества ООП и все возможности SQL.

Подробнее..

Recovery mode Пишем простую ORM с возможностью смены БД на лету

23.08.2020 20:09:46 | Автор: admin
image

Привет, Хабр! Карма слита из-за неосторожного комента под холиварной статьей, а значит нужно написать интересный (я надеюсь) пост и реабилитироваться.

Я несколько лет пользуюсь серверным telegram клиентом на php. И как многие пользователи устал от постоянного роста потребления памяти. Некоторые сессии могут занимать от 1 до 8 гигабайт RAM! Поддержка баз данных была уже давно обещана, но подвижек в этом направлении не было. Пришлось решать проблему самому :) Популярность open source проекта, накладывала интересные требования на pull request:

  1. Обратная совместимость. Все существующие сессии должны продолжить работать в новой версии (сессия это сериализованный инстанс приложения в файле);
  2. Свобода выбора БД. Возможность менять тип хранилища без потери данных и в любой момент, так как у пользователей разные конфигурации окружения;
  3. Расширяемость. Простота добавления новых типов баз данных;
  4. Сохранить интерфейс. Код приложения, работающий с данными, не должен меняться;
  5. Асинхронность. Проект использует amphp, поэтому все операции с базами должны быть неблокирующими;

За подробностями приглашаю всех под кат.


Что будем переносить



Большую часть памяти в MadelineProto занимают данные о чатах, пользователях и файлах. Например, в кэше пиров (peer), у меня насчитывается более 20 тысяч записей. Это все пользователи, которых когда либо видел аккаунт (включая участников всех групп), а так же каналы, боты и группы. Чем старше и активнее аккаунт, тем больше данных в будет в памяти. Это десятки и сотни мегабайт, и большая часть из них не используется. Но очищать кэш целиком нельзя, потому что телеграмм сразу будет жестко ограничивать аккаунт при попытках многократно получать одни и те же данные. Например, после пересоздания сессии на моем публичном демо сервере, телеграм в течении недели на большинство запросов отвечал ошибкой FLOOD_WAIT и ничего толком не работало. После того как кеш прогрелся все пришло в норму.

С точки зрения кода эти данные хранятся в виде массивов в свойствах пары классов.

Архитектура


Исходя из требований родилась схема:
  • Все тяжелые массивы заменяем на объекты реализующие ArrayAccess;
  • Для каждого типа базы данных создаем свои классы, наследующие базовый;
  • Обьекты создаются и записываются в свойства во время __consrtuct и __awake;
  • Абстрактная фабрика выбирает нужный класс для объекта, в зависимости от выбранной базы данных в настройках приложения;
  • Если в приложении уже есть другой тип хранилища, то считываем оттуда все данные и записываем массив в новое хранилище.


Проблемы асинхронного мира


Перым делом я создал интерфейсы и класс для хранения массивов в памяти. Это был вариант по умолчанию, идентичный по поведению старой версии программы. В первый вечер я был очень воодушевлен успехами прототипа. Код получался красивый и простой. Пока не обнаружилось, что нельзя использовать генераторы внутри методов интерфейса Iterator и внутри методов отвечающих за unset и isset.

Тут нужно пояснить, что amphp использует синтаксис генераторов для реализации асинхронности в php. Yield становится аналогом async await из js. Если какой-то метод использует асинхронность, то что бы получить из него результат, нужно дождаться этого результата в коде с помощью yield. Например:

<?phpinclude 'vendor/autoload.php';$MadelineProto = new \danog\MadelineProto\API('session.madeline');$MadelineProto->async(true);$MadelineProto->loop(function() use($MadelineProto) {    $myAsyncFunction = function() use($MadelineProto): \Generator {        $me = yield $MadelineProto->start();        yield $MadelineProto->echo(json_encode($me, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE));    };    yield $myAsyncFunction();});


Если из строки
yield $myAsyncFunction();
убрать yield, то приложение будет завершено до того, как этот код будет выполнен. Мы не получим результат.

Добавить yield перед вызовом методов и функций не очень сложно. Но так как используется интерфейс ArrayAccess, то методы не вызываются напрямую. Например, unset() вызывает метод offsetUnset(), а isset() offsetIsset(). Аналогичная ситуация с итераторами типа foreach при использовании интерфейса Iterator.

Добавление yield перед встроенными методами вызывает ошибку, так как эти методы не предназначены для работы с генераторами. Пришлось пойти на компромис и переписать код на использование собственных методов. К счастью таких мест было очень мало. В большинстве случаев массивы использовались для чтения или записи по ключу. Этот функционал с генераторами отлично подружился.

Получился такой интерфейс:

<?phpuse Amp\Producer;use Amp\Promise;interface DbArray extends DbType, \ArrayAccess, \Countable{    public function getArrayCopy(): Promise;    public function isset($key): Promise;    public function offsetGet($offset): Promise;    public function offsetSet($offset, $value);    public function offsetUnset($offset): Promise;    public function count(): Promise;    public function getIterator(): Producer;    /**     * @deprecated     * @internal     * @see DbArray::isset();     *     * @param mixed $offset     *     * @return bool     */    public function offsetExists($offset);}


Примеры работы с данными

<?php...//Чтение$existingChat = yield $this->chats[$user['id']];//Запись. yield $this->chats[$user['id']] = $user;//Можно использовать без yield, тогда мы не ждем записи в базу и код выполняется дальше.$this->chats[$user['id']] = $user;//unsetyield $this->chats->offsetUnset($id);//foreach$iterator = $this->chats->getIterator();while (yield $iterator->advance()) {    [$key, $value] = $iterator->getCurrent();    //обрабатываем элементы массива}


Хранение данных



Самый простой способ хранить данные в сериализованном виде. От использования json пришлось отказаться ради поддержки объектов. У таблицы две основных колонки: ключ и значение.

Пример sql запроса на создание таблицы:
            CREATE TABLE IF NOT EXISTS `{$this->table}`            (                `key` VARCHAR(255) NOT NULL,                `value` MEDIUMBLOB NULL,                `ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,                PRIMARY KEY (`key`)            )            ENGINE = InnoDB            CHARACTER SET 'utf8mb4'             COLLATE 'utf8mb4_general_ci'


При каждом старте приложения мы пытаемся создать таблицу для каждого свойства. Telegram клиенты не рекомендуется рестартить чаще чем раз в несколько часов, так что у нас не будет нескольких запросов на создание таблиц в секунду :)

Так как primary ключ без автоинкремента, то и вставку и обновление данных можно делать одним запросом, как в обычном массиве:

INSERT INTO `{$this->table}`             SET `key` = :index, `value` = :value             ON DUPLICATE KEY UPDATE `value` = :value


На каждую переменную создается таблица с именем в формате %id_аккаунта%_%класс%_%имя_переменной%. Но при первом старте приложения никакого аккаунта еще нет. В таком случае приходится генерировать случайный временный id с префиксом tmp. При каждом запуске класс каждой переменной проверяет не появился ли id аккаунта. Если id есть, то таблицы будут переименованы.

Индексы


Структура базы данных максимально простая, что бы добавление новых свойств в будущем происходило автоматически. Нет никаких связей. Используются только PRIMARY индексы по ключу. Но бывают ситуации, когда нужно искать по другим полям.

Например, есть массив/таблица chats. Ключ в ней это id чата. Но часто приходится искать по username. Когда приложение хранило данные в массивах, то поиск по username осуществлялся обычным перебором массива в foreach. Такой перебор работал с приемлемой скоростью в памяти, но не в базе. Поэтому была создана еще одна таблица/массив и соответствющее свойство в классе. В ключе username, в значении id чата. Единственный минус такого подхода приходится писать дополнительный код для синхронизации двух таблиц.

Кэширование


Локальная mysql работает быстро, но немного кеширования никогда не помешает. Особенно если одно и тоже значение используется несколько раз подряд. Например, сначала проверяем наличие чата в базе, а потом достаем из него какие-нибудь данные.
Был написан простенький велосипед trait.

<?phpnamespace danog\MadelineProto\Db;use Amp\Loop;use danog\MadelineProto\Logger;trait ArrayCacheTrait{    /**     * Values stored in this format:     * [     *      [     *          'value' => mixed,     *          'ttl' => int     *      ],     *      ...     * ].     * @var array     */    protected array $cache = [];    protected string $ttl = '+5 minutes';    private string $ttlCheckInterval = '+1 minute';    protected function getCache(string $key, $default = null)    {        $cacheItem = $this->cache[$key] ?? null;        $result = $default;        if (\is_array($cacheItem)) {            $result = $cacheItem['value'];            $this->cache[$key]['ttl'] = \strtotime($this->ttl);        }        return $result;    }    /**     * Save item in cache.     *     * @param string $key     * @param $value     */    protected function setCache(string $key, $value): void    {        $this->cache[$key] = [            'value' => $value,            'ttl' => \strtotime($this->ttl),        ];    }    /**     * Remove key from cache.     *     * @param string $key     */    protected function unsetCache(string $key): void    {        unset($this->cache[$key]);    }    protected function startCacheCleanupLoop(): void    {        Loop::repeat(\strtotime($this->ttlCheckInterval, 0) * 1000, fn () => $this->cleanupCache());    }    /**     * Remove all keys from cache.     */    protected function cleanupCache(): void    {        $now = \time();        $oldKeys = [];        foreach ($this->cache as $cacheKey => $cacheValue) {            if ($cacheValue['ttl'] < $now) {                $oldKeys[] = $cacheKey;            }        }        foreach ($oldKeys as $oldKey) {            $this->unsetCache($oldKey);        }        Logger::log(            \sprintf(                "cache for table:%s; keys left: %s; keys removed: %s",                $this->table,                \count($this->cache),                \count($oldKeys)            ),            Logger::VERBOSE        );    }}


Особое внимание хочется обратить на startCacheCleanupLoop. Благодаря магии amphp инвалидация кеша максимально простая. Коллбэк запускается по указанному интервалу, проходит по всем значениям и сморит на поле ts, в котором хранится timestamp последнего обращения к этому элементу. Если обращение было более 5 минут назад (конфигурируется в настройках), то элемент удаляется. C помощью amphp очень легко реализовать аналог ttl из redis или memcache. Все это происходит в фоне и не блокирует основной поток.

C помощью кеша и асинхронности ускоряется не только чтение, но и запись.

Вот исходный код метода, записывающего данные в базу.
/**     * Set value for an offset.     *     * @link https://php.net/manual/en/arrayiterator.offsetset.php     *     * @param string $index <p>     * The index to set for.     * </p>     * @param $value     *     * @throws \Throwable     */    public function offsetSet($index, $value): Promise    {        if ($this->getCache($index) === $value) {            return call(fn () =>null);        }        $this->setCache($index, $value);        $request = $this->request(            "            INSERT INTO `{$this->table}`             SET `key` = :index, `value` = :value             ON DUPLICATE KEY UPDATE `value` = :value        ",            [                'index' => $index,                'value' => \serialize($value),            ]        );        //Ensure that cache is synced with latest insert in case of concurrent requests.        $request->onResolve(fn () => $this->setCache($index, $value));        return $request;    }


$this->request создает Promise, который записывает данные асинхронно. А операции с кешом происходят синхронно. То есть можно не дожидаться записи в базу и при этом быть уверенным, что операции чтения сразу начнут возвращать новые данные.

Очень полезным оказался метод onResolve из amphp. После завершения вставки данные будут еще раз записаны в кэш. Если какая-то операция записи запоздает и кэш и база начнут отличаться, то кэш обновится значением записанным в базу последним. Т.е. наш кэш снова станет консистентен с базой.

Исходный код



Ссылка на pull request: github.com/danog/MadelineProto/pull/808

А вот так просто другой пользователь добавил поддержку postgre: github.com/danog/MadelineProto/pull/846.
Потребовалось всего 5 минут, что бы написать инструкцию для него: github.com/danog/MadelineProto/pull/808#issuecomment-666344285

Количество кода можно было бы сократить, если вынести дублирующиеся методы в общий абстрактный класс SqlArray.

One more thing



Было замечено, что во время скачивания медиафайлов из telegram стандартный garbage collector php не справляется с работой и куски файла остаются в памяти. Обычно размер утечек совпадал с размером файла. Возможная причина: garbage collector автоматически срабатывает, когда накапливается 10 000 ссылок. В нашем случае ссылок было мало (десятки), но каждая могла ссылаться на мегабайты данных в памяти. Изучать тысячи строк кода с реализацией mtproto было очень лень. Почему бы сначала не попробовать элегантный костыль с \gc_collect_cycles();?

Удивительно, но он решал проблему. Значит, достаточно настроить периодический запуск очистки. К счастью, amphp дает простые инструменты для фонового выполнения через указанный интервал.

Очищать память каждую секунду показалось слишком просто и не очень эффективно. Остановился на алгоритме, который проверяет прирост памяти после последнего очищения. Очистка происходит, если прирост больше порогового значения.

<?phpnamespace danog\MadelineProto\MTProtoTools;use Amp\Loop;use danog\MadelineProto\Logger;class GarbageCollector{    /**     * Ensure only one instance of GarbageCollector     * when multiple instances of MadelineProto running.     * @var bool     */    public static bool $lock = false;    /**     * How often will check memory.     * @var int     */    public static int $checkIntervalMs = 1000;    /**     * Next cleanup will be triggered when memory consumption will increase by this amount.     * @var int     */    public static int $memoryDiffMb = 1;    /**     * Memory consumption after last cleanup.     * @var int     */    private static int $memoryConsumption = 0;    public static function start(): void    {        if (static::$lock) {            return;        }        static::$lock = true;        Loop::repeat(static::$checkIntervalMs, static function () {            $currentMemory = static::getMemoryConsumption();            if ($currentMemory > static::$memoryConsumption + static::$memoryDiffMb) {                \gc_collect_cycles();                static::$memoryConsumption = static::getMemoryConsumption();                $cleanedMemory = $currentMemory - static::$memoryConsumption;                Logger::log("gc_collect_cycles done. Cleaned memory: $cleanedMemory Mb", Logger::VERBOSE);            }        });    }    private static function getMemoryConsumption(): int    {        $memory = \round(\memory_get_usage()/1024/1024, 1);        Logger::log("Memory consumption: $memory Mb", Logger::ULTRA_VERBOSE);        return (int) $memory;    }}
Подробнее..

Swift и CoreData. Или как построить Swift ORM на основе Objective-C ORM

04.06.2021 16:20:58 | Автор: admin

Хабр, здравствуй! Меня зовут Геор, и я развиваю iOS проекты в компании Prisma Labs. Как вы наверняка поняли, речь сегодня пойдет про кордату и многим из вас стало скучно уже на этом моменте. Но не спешите отчаиваться, так как говорить мы будет по большей части о магии свифта и про метал. Шутка - про метал в другой раз. Рассказ будет про то, как мы победили NSManaged-бойлерплейт, переизобрели миграции и сделали кордату great again.

Разработчики, пройдемте.

Пару слов о мотивации

Работать с кордатой сложно. Особенно в наше свифт-время. Это очень старый фреймворк, который был создан в качестве дата-слоя с акцентом на оптимизацию I/O, что по умолчанию сделало его сложнее других способов хранения данных. Но производительность железа со временем перестала быть узким горлышком, а сложность кордаты, увы, никуда не делась. В современных приложениях многие предпочитают кордате другие фреймворки: Realm, GRDB (топ), etc. Или просто используют файлы (почему бы и нет). Даже Apple в новых туториалах использует Codable сериализацию/десериализацию для персистенса.

Несмотря на то, что АПИ кордаты периодически пополнялся различными удобными абстракциями (напр. NSPersistentContainer), разработчики по-прежнему должны следить за жизненным циклом NSManaged объектов, не забывать выполнять чтение/запись на очереди контекста, к которому эти объекты привязаны и разумеется ругаться каждый раз, когда что-то пойдет не так. И наверняка во многих проектах есть дублирующий набор моделей доменного уровня и код для конвертации между ними и их NSManaged-парами.

Но у кордаты есть и немало плюсов - мощный визуальный редактор схемы данных, автоматические миграции, упрощенная (по сравнению с SQL) система запросов, безопасный мультипоточный доступ к данным и так далее.

У себя в Призме мы написали простой и мощный фреймворк, который позволяет забыть о недостатках кордаты и при этом использовать всю силу ее светлой стороны.

Встречайте - Sworm.

Я не буду рассказывать полностью про устройство фреймворка - для этого есть репозиторий, а сосредоточусь на основных моментах и хитростях имплементации.

Отказ от NSManagedObject-наследования и встроенной CoreData-кодогенерации

Вместо этого NSManagedObject'ы используются напрямую как key-value контейнеры. Идея не нова, а сложность заключается в том, как автоматизировать конвертацию между KV-контейнером и доменной моделью. Чтобы решить эту задачу нужно навести 3 моста:

  1. название

  2. аттрибуты

  3. отношения

С названием все просто - указав в типе вашей модели строчку с названием можно однозначно связать ее с сущностью в модели:

struct Foo {    static let entityName: String = "FooEntity"}

"Мост" отношений - уже более сложная техническая конструкция. В случае названия, статическое поле указанное внутри типа автоматически с ним связано:

Foo.entityName

Но чтобы определить отношение, помимо названия этого отношения нам нужен еще тип destination-модели, внутри которой так же должно быть название соответствующей сущности. Это наводит на две мысли. Во-первых, все модели, конвертируемые в NSManageObject должны следовать единому набору правил, то есть пришло время протокола, и, во-вторых, нам нужен дополнительный тип данных Relation<T: протокол>(name: String), который будет связывать название отношения в модели данных с типом, соответствующей ей доменной модели. Пока опустим детали, что отношения бывают разные - это на данном этапе неважно. Итак, протокол версия 1:

protocol ManagedObjectConvertible {    static var entityName: String { get }}

и тип для отношений:

Relation<T: ManageObjectConvertible>(name: String)

Применяем:

struct Foo: ManageObjectConvertible {    static var entityName: String = "FooEntity"    static let relation1 = Relation<Bar1>(name: "bar1")    static let relation2 = Relation<Bar2>(name: "bar2")}

Сразу напрашивается идея зафиксировать наличие связей (отношений) в нашем протоколе, но как это сделать, если количество связей всегда разное? Сделать коллекцию отношений не получится по нескольким причинам. Во-первых, в свифте дженерики инвариантны, во-вторых, рано или поздно нам придется вспомнить, что Relation распадется на несколько типов - one/many/orderedmany, и это автоматически приведет к мысли о гомогенности через стирание типов, что нас не устраивает. Но на самом деле, нас не интересуют сами отношения и мы можем даже не думать об их количестве. Поэтому мы добавим в протокол не конкретный тип отношений, а ассоциацию с типом отношений. Звучит странно и на первый взгляд непонятно, но подержите мое пиво - протокол версия 2:

protocol ManagedObjectConvertible {    associatedtype Relations    static var entityName: String { get }    static var relations: Relations { get }}

Все еще странно, продолжаем держать пиво:

struct Foo: ManageObjectConvertible {    static let entityName: String = "FooEntity"    struct Relations {        let relation1 = Relation<Bar1>(name: "bar1")        let relation2 = Relation<Bar2>(name: "bar2")    }    static let relations = Relations()}

И вот сейчас станет понятно - с помощью такой имплементации можно легко достать название отношения:

extension ManagedObjectConvertible {    func relationName<T: ManagedObjectConvertible>(        keyPath: KeyPath<Self.Relations, Relation<T>>    ) -> String {        Self.relations[keyPath: keyPath].name    }}

Пиво-то верни, что стоишь :)

Финальный мост - атрибуты

Как известно у любого босса есть слабые места и этот не исключение.

На первый взгляд, задача выглядит аналогичной "мосту" отношений, но в отличие от них, нам необходимо знать о всех доступных атрибутах, и обойтись ассоциированным типом не получится. Нужна полноценная коллекция атрибутов, каждый из которых должен уметь делать две вещи: заэнкодить значение из модели в контейнер и задекодить из контейнера обратно в модель. Очевидно, что это связь WritableKeyPath + String key. Но, как и в случае отношений, нам понадобится решить задачу - как сохранить информацию о типах, учитывая инвариантность дженериков и необходимость иметь гомогенную коллекцию атрибутов.

Пусть в роли атрибута выступит специальный объект типа Attribute<T>, где T - доменная модель. Тогда коллекцией атрибутов будет `[Attribute<T>]` и для нашего протокола заменим T на Self. Итак, протокол - версия 3:

public protocol ManagedObjectConvertible {    associatedtype Relations    static var entityName: String { get }    static var attributes: [Attribute<Self>] { get }    static var relations: Relations { get }}

И теперь попробуем реализовать непосредственно класс Attribute. Напомню, что в зону его ответственности входит сериализация/десериализация поля между моделью и KV-контейнером. Сперва, попробуем ненадолго забыть про ограничения на гомогенность типов и сделаем в лоб:

final class Attribute<T: ManagedObjectConvertible, V> {    let keyPath: WritableKeyPath<T, V>    let key: String    ...    func update(container: NSManagedObject, model: T) {        container.setValue(model[keyPath: keyPath], forKey: key)    }    func update(model: inout T, container: NSManagedObject) {        model[keyPath: keyPath] = container.value(forKey: key) as! V    }}

Имплементация атрибута могла бы выглядеть так, но [Attribute<T, V>] - не наш случай. Как можно избавиться от V в сигнатуре класса, сохранив информацию об этом типе? Не все знают, но в свифте можно добавлять дженерики в сигнатуру инициализатора:

final class Attribute<T: ManagedObjectConvertible> {    ...    init<V>(        keyPath: WritableKeyPath<T, V>,        key: String    ) { ... }    ...}

Теперь у нас есть информация о V в момент инициализации атрибута. А для того, чтобы не потерять ее и дальше, расчехлим свифт аналог BFG - кложуры:

final class Attribute<T: ManagedObjectConvertible> {    let encode: (T, NSManagedObject) -> Void    let decode: (inout T, NSManagedObject) -> Void    init<V>(keyPath: WritableKeyPath<T, V>, key: String) {        self.encode = {            $1.setValue($0[keyPath: keyPath], forKey: key)        }        self.decode = {            $0[keyPath: keyPath] = $1.value(forKey: key) as! V        }    }}

В нашем протоколе осталось еще одно пустое место. Мы знаем как создать NSManagedObject и заполнить его данными из модели, знаем как заполнить модель из NSManagedObject'а, но НЕ знаем, как создать инстанс нашей модели при необходимости.

Протокол - версия 4, финальная:

protocol ManagedObjectConvertible {    associatedtype Relations    static var entityName: String { get }    static var attributes: Set<Attribute<Self>> { get }    static var relations: Relations { get }    init()}

Все - мы победили наследование от NSManagedObject'ов, заменив его на имплементацию протокола.

Далее рассмотрим как можно сделать систему атрибутов гибче и шире.

Гибкая система атрибутов

Кордата поддерживает набор примитивных аттрибутов - bool, int, double, string, data, etc. Но помимо них есть малоиспользуемый Transformable, который позволяет сохранять в кордате данные различных типов. Идея отличная и мы решили вдохнуть в нее новую жизнь с помощью системы типов свифта.

Определим следующий набор атрибутов-примитивов:

Bool, Int, Int16, Int32, Int64, Float, Double, Decimal, Date, String, Data, UUID, URL

И утвердим правило: тип атрибута валиден, если данные можно сериализовать в один из примитивов и десериализовать обратно.

Это легко выразить в виде двух протоколов:

protocol PrimitiveAttributeType {}protocol SupportedAttributeType {    associatedtype P: PrimitiveAttributeType    func encodePrimitive() -> P    static func decode(primitive: P) -> Self}

Применив SupportedAttributeType в нашей имплементации Attribute

final class Attribute<T: ManagedObjectConvertible> {    let encode: (T, NSManagedObject) -> Void    let decode: (inout T, NSManagedObject) -> Void    init<V: SupportedAttributeType>(keyPath: WritableKeyPath<T, V>, key: String) {        self.encode = {            $1.setValue($0[keyPath: keyPath].encodePrimitive(), forKey: key)        }        self.decode = {            $0[keyPath: keyPath] = V.decode(primitive: $1.value(forKey: key) as! V.P)        }    }}

мы получим возможность хранить в кордате данные любых типов по аналогии с Transformable, но без objc-легаси.

Комбинируя гибкие атрибуты и заменяя NSManagedObject-наследование имплементацией протокола можно очень сильно сократить кодовую базу - убрать много бойлерплейта, связанного с дублированием моделей, копированием кода сериализации композитных атрибутов и так далее.

Благодаря ManagedObjectConvertible мы однозначно связали типы наших моделей и информмацию о схеме данных. Но для того, чтобы на основе этой информации мы могли выполнять операции с данными нам потребуется слой data access объектов или DAO, поскольку доменные модели обычно выступают в роли DTO - data transfer объектов.

Скрываем NSManaged под капот

Если рассматривать NSManaged-слой в терминах DAO и DTO, то контекст+объекты это DAO+DTO, причем равны суммы, но не компоненты по отдельности, так как NSManagedObject помимо трансфера данных еще может их обновлять, но с участием контекста. Попробуем перераспределить функциональность между NSManaged-сущностями и нашими доменными моделями. Наши модели это DTO + метаинформация о схеме данных (имплементация ManagedObjectConvertible). Составим псевдоуравнение:

доменные модели + raw NSManaged- объекты + X = DAO+DTO

я пометил NSManaged как raw - так как с точки зрения компилятора мы забрали от них информацию о схеме данных и передали ее во владение доменным моделям.

А X - это тот недостающий фрагмент, который свяжет информацию о схеме данных, информацию о типах моделей с NSManaged-слоем.

Решением нашего псевдоуравнения будет выступать новая сущность:

final class ManagedObject<T: ManagedObjectConvertible> {    let instance: NSManagedObject    ...}

Этот класс будет служить фасадом для NSManaged слоя, используя дженерик в сигнатуре типа для доступа к схеме данных.

Я не буду вдаваться в подробности конечной имплементации из-за масштабности фреймворка, но хотел бы на примере отношений между моделями продемонстрировать мощь dynamicMemberLookup в Swift.

Если мы вспомним ManagedObjectConvertible предоставляет информацию о названи сущности в схеме данных, о атрибутах-конвертерах и отношениях между моделями. Я специально заострил тогда внимание на том, как с помощью Keypaths можно получить название отношения. Адаптируем тот код под нужды ManagedObject:

final class ManagedObject<T: ManagedObjectConvertible> {    ...    subscript<D: ManagedObjectConvertible>(        keyPath: KeyPath<T.Relations, Relation<D>>    ) -> ManagedObject<D> {        let destinationName = T.relations[keyPath: keyPath]        // получаем объект отношения через NSManaged API        return .init(instance: ...)    }}

И, соответственно, использование:

managedObject[keyPath: \.someRelation]

Достаточно просто, но мы можем применить спец заклинание в свифте - dynamicMemberLookup:

@dynamicMemberLookupfinal class ManagedObject<T: ManagedObjectConvertible> {    ...    subscript<D: ManagedObjectConvertible>(        dynamicMember keyPath: KeyPath<T.Relations, Relation<D>>    ) -> ManagedObject<D> { ... }}

и сделать наш код проще и более читаемым:

managedObject.someRelation

Последний момент, который я бы хотел вам сегодня показать - это как с помощью наших атрибутов можно сделать удобную систему предикатов в запросах.

Типизированные предикаты

Идея заключается в том, чтобы заменить строковые запросы кордаты типизированными свифт выражениями:

Вместо "foo.x > 9 AND foo.y = 10" написать \Foo.x > 9 && \Foo.y == 10 и из этого выражения получить обратно "foo.x > 9 AND foo.y = 10"

Сделать это имея на руках информацию из сущности Attribute и протоколов Equatable и Comparable достаточно просто. От нас понадобится заимплементировать набор операторов сравнения и логических операторов.

Разберем для примера логический оператор >. В левой части у него находится KeyPath нужного атрибута, а в правой - значение соответствующего типа. Наша задача превратить выражение \Foo.x > 9 в строку "x > 9". Самое простое - это знак оператора. Просто в имплементации функции оператора зашиваем литерал ">". Чтобы из кипаса получить название обратимся к имплементации нашего протокола ManagedObjectConvertible у сущности Foo и попытаемся найти в списке атрибутов тот, что соответствует нашему кипасу. Сейчас мы не храним кипас и ключ контейнера внутри объекта атрибута, но ничего не мешает нам это сделать:

final class Attribute<T: ManagedObjectConvertible> {    let key: String    let keyPath: PartialKeyPath<T>    let encode: (T, NSManagedObject) -> Void    let decode: (inout T, NSManagedObject) -> Void    ...}

Обратите внимание, что WritableKeyPath стал PartialKeyPath. И самое важное, что мы можем в рантайме сравнивать кипасы межды собой, так как они имплементируют Hashable. Это крайне интересный момент, который говорит о том, что кипасы играют важную роль не только в комплайл тайме, но и в рантайме.

Исходя из новой имплементации, обращайся к списку атрибутов мы можем зная кипас, легко вытащить соответствующий ему ключ в KV-контейнере.

Также нам нужно понимать, к каким атрибутам можно применять операции сравнения. Очевидно, что не все типы имплементируют Equatable и/или Comparable. Но на самом деле, нас интересует не сам тип атрибута, а тип его конечного примитива (см. SupportedAttributeType).

Поскольку в кордате мы оперируем именно примитивами, нам будут подходить те атрибуты, чьи примитивы имплементируют Equatable и/или Comparable:

func == <T: ManagedObjectConvertible, V: SupportedAttributeType>(    keyPath: KeyPath<T, V>,    value: V) -> Predicate where V.PrimitiveAttributeType: Equatable {    return .init(        key: T.attributes.first(where: { $0.keyPath == keyPath })!.key,        value: value.encodePrimitiveValue(),        operator: "="    )}

где Predicate - это специальный тип, который нужен для абстракции отдельных фрагментов целого выражения.

И для полноты картины не хватает логического оператора. Например AND. Его имплементация по сути дела является склейкой двух фрагментов в выражении и верхнеуровнево его можно представить как "(\(left)) AND (\(right))".

Таким образом основная идея заключается в том, что зная связь между типами и схемой данных, можно из типизированных swift выражений составить правильную строку запроса и за счет этого меньше ошибаться.

Заключение

Я постарался осветить основные и наиболее интересеные моменты в имплементации, не пытаясь рассказать про кажду строчку нашего фреймворка. Я не затронул некоторые важные темы, например - прогрессивные миграции, но про это и многое другое подробно написано в репозитории.

Надеюсь, что Sworm сделает вашу жизнь чуточку проще, как помогает нам уже на протяжении года.

Всем добра!

Подробнее..

Заказать отзывы и не пожалеть об этом

28.08.2020 08:19:31 | Автор: admin

Один из комментариев под моей прошлой статьей поставил вполне логичный и закономерный вопрос: Разве еще кто-то доверяет таким сайтам с отзывами?. Думаю, что это отличая отправная точка и идея для рассказа на тему того, доверяют ли пользователи в интернете отзывам, насколько они сейчас актуальны и как заказать отзывы, если уж на то пошло?


Отзывам доверяют?


Да.
image


Ладно, если разобраться серьезно в этом вопросе, то любой человек, который хотя бы немного беспокоится на что же он тратит деньги, будет искать информацию о товаре, который его интересует. Ясно, что если речь идет о фруктах, овощах или печеньках Любятово (хотя кто знает), то вы просто возьмете и приобретете это. Когда вы хотите купить более-менее дорогую вещь, то, скорее всего, будете смотреть, какие у неё есть плюсы, минусы, нюансы, чем она лучше или хуже, чем те же вещи той же категории другого бренда.
Происходит так именно потому, что человек сам по себе воспринимает информацию от таких же людей, имеющих опыт пользования продуктом или сервисом гораздо более достоверной, чем описание товара на сайте производителя или прочих официальных источниках. То же самое касается и разных обзоров на YouTube, начиная от обзоров разнообразных девайсов, заканчивая службами доставки и ресторанами. Стоит учитывать, что в поисках того самого обзора никто не будет шерстить весь интернет, но первые 3-4 сайта из поиска он вполне может посмотреть.
На эту тему как раз было исследование у Nielsen, согласно которому около 55-60% пользователей просматривают отзывы перед покупкой.


Зачем компании пытаются заказать отзывы, а люди вообще их размещают?


В первую очередь важно понимать, что отзыв это мнение человека о каком-либо товаре, магазине, продукте, компании и так далее. Это мнение у каждого индивидуально и складывается из личного опыта взаимодействия с объектом, на который отзыв размещается. Например: мне в одном ресторане понравилось качество блюд, атмосфера и качество обслуживания, а Вам, читателю этой статьи, в нем не понравилось ничего из этого. Если сложить сотню другую подобных отзывов, то получается некое подобие общественного мнения, только на каком-либо конкретном отзовике в интернете. Само собой, оно влияет на восприятие потенциальными клиентами этого ресторана, это именно та причина, почему такие компании решаются заказать отзывы.
Но почему люди в принципе тратят свое время на то, чтобы оставить свой отзыв о чем-либо? Ответ совсем прост: они хотят высказаться. Логично предположить, что если все понравилось, то высказываться не хочется да и не нужно, поскольку, если ты платишь деньги за сервис и еду, то ты ожидаешь, что все будет выполнено на должном уровне, а свою благодарность оставили в виде денег, списанных с вашей карты в пользу ресторана и официанта в виде чаевых. Будете ли вы тратить время на это? Я думаю, что нет. А если у вас попался таракан в блюде и нахамила официантка, а управляющий отказался менять блюдо и выгнал из заведения? Само собой, у вас будет желание разбомбить их везде, где только можно: TripAdvisor, otzovik, Irecommend, Яндекс. В единичном случае для условного ресторана ничего не будет, а если таких будет десятки? Сотни? А самое главное, пошли бы вы в такой ресторан, о котором пишут практически один лишь негатив? Ответ очевиден.
Исходя из этого можно понять, что большинство брендов испытывают жесткий дефицит положительного контента от их рядовых клиентов, но стоит случиться одному негативному инфоповоду, например, выпустить одну бракованную партию товара, то репутации компании будет нанесён существенный ущерб, который придется исправлять довольно долго. Следовательно, чтобы эту ситуацию исправить можно заказать отзывы.


С чего начать размещение отзывов?


Думаю, большинство считает, что размещение подобного контента не слишком честно для потребителя, но, с другой стороны, где вы видели рекламу, которая честно и открыто указывала на все недостатки и нюансы какого-либо продукта? Вот и мне кажется, что таких вы не видели. То же самое и в этой ситуации.
Вы хотите заказать отзывы у подрядчика или размещать самому, сначала совесть может вас немного помучить, если, конечно, вы очень честный человек.
Но когда вы уже решились попробовать этот вид продвижения, то первое, что стоит сделать это подобрать список ресурсов. Немного изучив выдачу по запросу отзывы, вы уже получите представление о том, какие ресурсы считаются общенаправленными, какие посвящены каким-либо конкретным категориям товаров или услуг. Или же, как вариант, поищите отзывы о наиболее популярном вашем конкуренте, большинство ресурсов с отзывами о нем могут пригодится и вам. Однако, есть и такие отзовики, которые будут актуальны во всех случаях, например: otzovik, irecommend, а также гео-сервисы могут пригодиться практически в любой ситуации.


Возможные проблемы


Стоит сразу отметить, что на этих ресурсах размещение самое сложное, поскольку модерация проходит далеко не всегда гладко, а фильтры могут повесить за любые подозрительные действия, поэтому если вы решили заказать отзывы, то делайте это либо через проверенные агентства, либо вовсе стимулируйте отзывы от своих реальных клиентов.
Очень много компаний начинают подобное размещение на отзовиках, но из-за незнания процесса ловят фильтры на свои карточки. Как правило, происходит это после работы через биржи или автоматизированные системы по подмене IP (Нет, это не обычный VPN, а у ряда агентств свои системы, так что сложно точно описать принцип работы каждой из них). На подобных трастовых ресурсах всегда отслеживается, как размещен отзыв, поэтому бан абсолютно неизбежен, только при использовании бирж он настанет на порядок раньше.


Размещение здорового человека


Уже много слов сказано о том, как не стоит работать с отзовиками, но как делать это правильно? Давайте разбираться.
Само собой, что если вы решили заказать отзывы и уже понимаете, как это в теории должно работать, то почему не можете начать сразу? Во-первых, нужна очень большая база людей, которая будет отзывы размещать, а если она уже есть, то еще больший опыт в коммуникации с ними. Ведь если людей много, то ими еще сложнее управлять, тем более если это не штатные сотрудники, а найденные где-то люди, которые хотят подзаработать. Во-вторых, нужно хорошо знать требования ресурсов и формат отзывов, которые хорошо проходят на него, какие нужны материалы для лучшей проходимости. В-третьих, нужна обычная упертость, поскольку если вы уже схлопотали фильтр на отзовике, то будут проходить далеко не все отзывы, и это еще при условии, что вы размещаетесь через реальных людей, потому как в обратном случае не пройдет ни одного.
Это лишь малая часть из тех нюансов, которые берутся в учет при работе с отзовиками, а как они налаживаются в любом нормальном агентстве и почему зачастую проще и лучше именно заказать отзывы, а не размещать самому, разберем далее.


Почему большинство выбирают заказать отзывы через агентство?


В первую очередь здесь важен факт того, что можно заказать отзывы под ключ (почти). Внутри компаний люди попросту не хотят этим заниматься, поскольку хлопот в этом деле выше крыши, начиная от сбора базы людей, которые будут заниматься непосредственно публикацией отзыва, заканчивая коммуникацией с администрацией ресурсов. Знаете ли вы, сколько нужно времени, чтобы все эти задачи решить и настроить коммуникацию должным образом? Отвечу просто очень много (хотя здесь должно было быть одно более емкое слово). Само собой, у нас были лиды, которые решали сделать всё сами и у них все отлично получилось, но их крайне мало, если быть точнее, то около 10 процентов. В основном же, если хотели поступить таким образом, то через пару месяцев возвращались, чтобы заказать отзывы, но с собой тащили фильтры на ресурсах.
В работе с подрядчиком вам стоит беспокоиться только на этапе его подбора, когда вы принимаете решение, кому же доверить эту работу. В остальном же, придется только принимать отчетность и оплачивать счета, попутно смотреть на то, как рейтинги растут, если, конечно, вы обратились к кому надо.


Заказать отзывы или стимулировать реальные?


Эта дилемма открывается перед всеми, кто еще только планировал работу с репутацией. У каждого из методов есть свои плюсы и минусы, давайте же разберем, какие они:


Разместить отзывы самому
Из плюсов явно можно выделить качество и нативность контента, который размещают сами реальные пользователи. Это даст больше доверия к самим отзывам от лица потенциальных клиентов, которые выбирают между вами и конкурентом.
В минусы смело записываем стоимость привлечения одного такого отзыва. Её в принципе контролировать крайне сложно, поскольку это работает по довольно нестандартной схеме, схожей с CPA, поскольку для работы в таком ключе используется множество инструментов, от email-маркетинга до контекстной рекламы и обзвона клиентов. Следом сразу идет отсутствие контроля количества размещаемых отзывов, ведь показатели могут являться непостоянными, а некий потолок все-таки имеется. Последним, по моему мнению, можно смело назвать организацию всего этого процесса, которые связан именно с количеством задействуемых инструментов. Ведь перед стартом основных работ придется тестировать, что же лучше себя зарекомендует, какие нужно составлять тексты для рассылок, корректировать скрипт для обзвона. Для всего этого нужны люди и время, а всего этого может и не быть.


Заказать отзывы
Плюсов здесь будет несколько больше, чем у стимуляции реальных отзывов. Самый основной плюс это минимальные затраты времени. Для того, чтобы заказать отзывы, необходимо провести выбор подрядчика (как это сделать расскажу далее), дать ему вводную информацию, на которую он сможет опираться в рамках написания контента, ну и, конечно же, оплатить работу. Что касается стоимости, то она на порядок ниже, чем когда вы стимулируете отзывы реальных клиентов. Для этого не нужно проводить отдельную рекламную кампанию, будь то SMS-рассылка или таргетинг, показатели которых могут плавать все время. При заказе отзывов стоимость остается фиксированной на протяжении всего периода работ, да и сама она, как показывает практика, намного меньше привлечения реального отзыва.
К минусам можно отнести две вещи это вероятность попасть не на того подрядчика, ведь агентств расплодилось целое море и даже приличная, с первого взгляда, организация может оказаться шарашкиной конторой. Ни для кого не секрет, что в агентском бизнесе очень многое решает позиционирование перед заказчиком, причем именно на этапе презентации коммерческого предложения. Стоит показать пару лишних графиков, которые не будут влиять на ход работ, как некоторые клиенты могут расплыться и остановить свой выбор на этом подрядчике.
Вторым же минусом является обман своего клиента. С одной стороны, отзыв не настоящий и может выстроить ложные ожидания потенциального клиента, а с другой, ни одна рекламная активность не обходится без такого. Где вы в последний раз видели, чтобы в рекламной кампании прямо говорили, что у продукта есть такие-то преимущества, такие-то минусы? Склоняюсь к тому, что подобного в принципе практически не бывает, а может и вовсе.


У кого заказать отзывы?


Как правило, практически все, кто занимается этим это репутационные агентства. Как и писал выше, таких сейчас уже очень много и они продолжают расти как грибы, прямо как SEO агентства в свое время. Занимаются они не только размещением самих отзывов (хотя есть и такие), но и полным комплексом управления репутацией в интернете. Само собой, что есть и фрилансеры, берущие на себя такие работы, но если речь идет о качественном проекте с немаленьким объемом, то к ним если и обращаться, только на свой страх и риск. Наиболее надежным вариантом будет заказать отзывы через агентство как минимум потому, что вы заключаете договор, который юридически обязывает выполнить все работы должным образом. Однако, если клиенту что-то не понравилось, то он очень редко идет в суд для возвращения денег, чаще подрядчик просто дорабатывает очередной месяц по договору и сворачивается. В подобном случае всегда лучше иметь собственный подобный отдел, но далеко не у всех есть для этого ресурсы.


Выбираем подрядчика на размещение отзывов.


В первую очередь, при выборе подрядчика, стоит обратить внимание на его репутацию в деловой среде и в общем в интернете. Вроде бы все звучит логично: агентство репутационное, репутация у них хорошая, но иногда даже такие ребята не могут наладить свои рейтинги, а уже предлагают сделать это потенциальным клиентам. У таких людей заказать отзывы будет уж слишком отчаянным поступком.
Допустим, вы отобрали 10 агентств, у которых можете заказать отзывы. Не так важно каким именно образом, поскольку до этапа консультации и презентации коммерческого предложения сложно делать какие-то выводы, не считая репутацию самого агентства и рекомендации. Не нужно запрашивать предложение у всех 10 претендентов, но со всеми стоит проконсультироваться. Вы должны выделить сразу несколько вопросов, которые нужно задать, а именно:
Каким способом размещаются отзывы?
Кто будет задействован в рамках проекта?
На каких ресурсах могут размещать отзывы?
Понятно, что исходя из конкретного запроса этот список может пополняться и меняться, однако по этим вопросам можно довольно быстро понять, кто с вами общается.
Из этих десяти останется примерно 3-5, и уже от них можно ожидать предложения. Во время его просмотра будет складываться уже более полное впечатление об агентстве. Обычно в рамках КП предоставляется анализ текущей ситуации, смотрят видимую зоны выдачи, какие там есть ресурсы, какие на них рейтинги, как обстоят дела с репутацией компании в общих чертах. Если вы небезразличны к этому, то примерно уже знаете, как обстоят дела с отзывами и рейтингами вашей компании, и поэтому можете судить, в каком аудите информация преподнесена корректно, а где инфополе компании показали в слишком негативном свете. Кого отсеивать на этом этапе, я думаю, вы поняли сами.
Итак, у вас осталось примерно 3 претендента из 10. Если анализ у них был примерно одинаковый, то из среднего арифметического более ясна картина, которая на самом деле есть. Теперь уже речь идет о стоимости. Говоря наиболее коротко средняя стоимость за обычный отзыв составляет от 450 до 600 рублей. При сильно меньшем показателе можно с уверенностью сказать, что когда вам говорили, что размещение проходит через реальных людей, вас обманули, поскольку это будет очень близко к себестоиомсти отзыва, а сотрудникам агентства кушать что-то нужно. При сильно большей цене это попросту будет невыгодно для вас, здесь уже комментарии излишни.


Финал


Вот и остались два финалиста, у которых вы можете заказать отзывы, кого же выбрать? На таком этапе, при прочих равных, будут решать два фактора само качество консультации и впечатление, полученное в ходе переговоров, а также наличие и качество кейсов. С первым все понятно, но есть большое количество подрядчиков, которые прикрываются NDA, как щитом от демонстрации и доказательства своей компетентности. NDA у нас в стране не то, что бы очень хорошо работал, но нарушать его действительно побоятся. С другой стороны, любое приличное агентство сможет договориться хотя бы на один показательный проект с действующим клиентом, который сможет показывать потенциальным заказчикам. Речь идет именно о хорошем кейсе здорового человека с названием и логотипом бренда, полным описанием работ, может это будет просто рекомендательное письмо от клиента, такие материалы могут подтвердить квалификацию специалистов агентства. Обаяет ли вас сейлз менеджер или впечатлит кейс зависит уже от вас.


Заключение


Если вы уже приняли решение заказать отзывы или начать работу в этом направлении самому трезво взвесьте все плюсы, минусы и нюансы. Какие факторы будут решающими и какой бы ни был сложный выбор он всегда за вами.
Надеюсь, что эта статья, несмотря на объем, была интересна, а для кого-то и полезна. Буду рад ознакомиться с вашим мнениями и опытом. До новых встреч.


Иван Клейменов, Puppet Agency

Подробнее..

Как я на собеседование готовился 1

14.12.2020 06:13:47 | Автор: admin
Иногда так случается, что хочется сменить работу. В таком случае, у нас есть несколько вариантов:

  1. 1) Потребовать прибавки, чтобы получить дополнительную мотивацию работать
  2. 2) Потребовать отпуск, чтобы отдохнуть от надоевших задач и набраться сил.
  3. 3) Сменить работу, выложив резюме, в надежде, что Вы получите предложение, которое будет Вас удовлетворять.


Если Вы все таки выбрали 3й вариант, то Вы поступили, как я. И я получил предложение мечты: родной стек(последнее время, я был вынужден сменить любимый C#, на php, так ещё и bitrix framework), финтех и с прибавкой к зарплате. Конечно же, первый делом, я решил подновить знания. Этот текст это в первую очередь, моя шпаргалка, составленная с текста вакансии на должность Middle .NET Engineer моей мечты.


ORM Entity Framework / NHibernate


Entity framework и NHibernate это фреймворки, которые используют технологию ORM(объектно-ориентировочного отображения(маппинга)для сопоставления объектов(классов) с таблицами в БД. То есть ORM, условно, прослойка между кодом и базой данных, которая позволяет созданые в программе объекты складывать/получать в/из бд.

Плюсы EF


  • Позволяет создать таблицу кодом или же используя EF Designer и сгенерировать новую базу данных
  • Вы можете автоматизировать создание объектов, а так же отслеживать изменения этих объектов, чем упростить процес обновления базы данных
  • Использование единого синтаксиса(LINQ) для любых коллекций объектов, будь то данные из БД, или просто список; он достаточно быстр, при правильном использовании, а так же в меру лаконичен.
  • EF может заменить огромные куски кода, которые Вы бы писали самостоятельно.
  • Сокращает время разработки.
  • Поддерживает асинхронные операции с базами данных.

Минусы EF


  • Использование нетрадиционного подхода обработки данных, доступного не с каждой базой данных.
  • При любом изменении в схеме БД, EF откажется работать; потребуются изменения в коде.
  • SQL-код генерируются в некотролируемом виде, мы должны доверять разработчикам самого EF.
  • Слабо подходит для больших доменных моделей.


Плюсы NHibernate


  • Большие возможности маппинга.
  • Поддержка кэша второго уровня.
  • Отличная реализация Unit Of Work.
  • Вы можете использовать почти любую базу данных.
  • Очень популярный в корпоративной среде.
  • Поддерживает различные стратегии генерации идентификаторов из базы данных.

Минусы NHibernate


  • Большое время подготовки метаданных.
  • Написание сопоставления, может быть очень утомительным, ведь XML не самый дружелюбный язык.
  • Сложен в освоении, так как сложен сам процес поиска документации.


Writing unit tests frameworks


Юнит-тесты позволяют быстро и автоматически протестировать отдельные компоненты приложения независимо от остальной его части. Не всегда юнит-тесты могут покрыть весь код приложения, но тем не менее они позволяют существенно уменьшить количество ошибок уже на этапе разработки.

Популярные фрейморки:
  • xUnit.net
  • MS Test
  • NUnit

Начиная с Visual Studio 2019 специально для тестов были добавлены три типа проектов: xUnit Test Project(.NET Core), MSTest Test Project(.NET Core) и NUnit Test Project(.NET Core)

Я буду рассматривать xUnit
Тесты в xUnit определяются в виде методов, к которым применяются атрибуты Fact, Theory.
Fact- это отдельный тест, у которого нет параметров. Theory тест, принимающий параметры, может иметь несколько сценариев.
Пример
[Fact]    public void Should_do_somthing(){...}    [Theory]    [InlineData(20, 180, 80, good)]    [InlineData(20, 180, 50, bad)]    public void Should_measure_weight(int age, int height, decimal weight, string expected){...}



Существует целая парадиграма тестирования, которую xUnit реализует в полной мере: Arrange-Act-Assert.
  • Arrange: устанавливает начальные условия для выполнения теста
  • Act: выполняет тест (обычно представляет одну строку кода)
  • Assert: верифицирует результат теста


Arrange и Act это обычный код на C#, а Assert это отдельный класс с набором статических методов для проверки результатов.

Основные методы
  • All(collection, action): метод подтверждает, что все элементы коллекции collection соответствуют действию action
  • Contains(expectedSubString, actualString): метод подтверждает, что строка actualString содержит expectedSubString
  • DoesNotContain(expectedSubString, actualString): метод подтверждает, что строка actualString не содержит строку expectedSubString
  • DoesNotMatch(expectedRegexPattern, actualString): метод подтверждает, что строка actualString не соответствует регулярному выражению expectedRegexPattern
  • Matches(expectedRegexPattern, actualString): метод подтверждает, что строка actualString соответствует регулярному выражению expectedRegexPattern
  • Equal(expected, result): метод сравнивает результат теста в виде значения result и ожидаемое значение expected и подтверждает их равенство
  • NotEqual(expected, result): метод сравнивает результат теста в виде значения result и ожидаемое значение expected и подтверждает их неравенство
  • Empty(collection): метод подтверждает, что коллекция collection пустая
  • NotEmpty(collection): метод подтверждает, что коллекция collection не пустая
  • True(result): метод подтверждает, что результат теста равен true
  • False(result): метод подтверждает, что результат теста равен false
  • IsType(expected, result): метод подтверждает, что результат теста имеет тип expected
  • IsNotType(expected, result): метод подтверждает, что результат теста не представляет тип expected
  • IsNull(result): метод подтверждает, что результат теста имеет значение null
  • IsNotNull(result): метод подтверждает, что результат теста не равен null
  • InRange(result, low, high): метод подтверждает, что результат теста находится в диапазоне между low и high
  • NotInRange(result, low, high): метод подтверждает, что результат теста не принадлежит диапазону от low до high
  • Same(expected, actual): метод подтверждает, что ссылки expected и actual указывают на один и тот же объект в памяти
  • NotSame(expected, actual): метод подтверждает, что ссылки expected и actual указывают на разные объекты в памяти
  • Throws(exception, expression): метод подтверждает, что выражение expression генерирует исключение exception



Таким образом, мы прошли 2 из 7 пунктов резюме.
В следующей части:
DI frameworks
Application designs understanding (n-Tier, Onion).

Спасибо, что дочитали, таким образом, я понимаю, что не один я готовлюсь к чему-либо ;)

Материалы: раз, два, три
Подробнее..
Категории: Net , Orm , Unit-testing

О репозиториях замолвите слово

26.10.2020 14:14:03 | Автор: admin
image

В последнее время на хабре, и не только, можно наблюдать интерес GO сообщества к луковой/чистой архитектуре, энтерпрайз паттернам и прочему DDD. Читая статьи на данную тему и разбирая примеры кода, постоянно замечаю один момент когда дело доходит до хранения сущностей предметной области начинается изобретение своих велосипедов, которые зачастую еле едут. Код вроде бы состоит из набора паттернов: сущности, репозитории, value objectы и так далее, но кажется, что они для того там чтобы были, а не для решения поставленных задач.
В данной статье я бы хотел не только показать, что, по моему мнению, не так с типичными DDD-примерами на GO, но также продемонстрировать собственную ORM для реализации персистентности доменных сущностей.


Дисклеймер.


Прежде чем приступить к теме статьи, есть несколько моментов, которые необходимо осветить:


  • Данная статья о том, как писать приложения с богатой бизнес логикой. Сервисы на GO зачастую такими не являются, не нужно применять к ним DDDшные подходы.
  • Исходя из того, что я не являюсь ярым фанатом ORM, считаю, что зачастую использование этой технологии попросту излишне. Кроме того, необходимо брать ее лишь в том случае, когда вы отдаете себе отчет в ее целесообразном использовании в проекте, иначе вы попросту используете инструмент для галочки, для того, чтоб был.
  • Оппонировать я буду подходам из этой статьи и (раз, два) примерам проектов.
  • Я буду иллюстрировать свои мысли на примере типичного приложения wish list.

А теперь можно начинать.


Энтерпрайз паттерны в GO и что с ними не так.


Речь здесь пойдет о таких паттернах как: репозиторий, сущность, агрегат и способах их приготовления. Для начала, давайте разберемся, что же это за паттерны такие. Я не буду придумывать определения в стиле от себя, а буду использовать слова признанных мастеров: Ерика Эванса и Мартина Фаулера.


Сущность.


Начнем с сущности. По Эвансу:


Entity: Objects that have a distinct identity that runs through time and different representations. You also hear these called "reference objects".

Ну тут вроде бы ничего сложного, сущности в GO можно реализовать, используя структуры. Вот типичный пример:


type Wish struct {    id       sql.NullInt64    content  string    createAt time.Time}

Агрегат.


А вот про этот шаблон как то незаслуженно забывают, особенно в контексте GO. А забывают, между прочим, абсолютно зря. Чуть позже мы разберем почему агрегаты намеренно не используются в различных примерах DDD проектов на GO. Итак, определение по Эвансу:


Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate and control all access to the objects inside the boundary through the root

Рассмотрим пример aggregate root:


type User struct {    id      sql.NullInt64         name    string                email   Email                  wishes  []*Wish     friends []*User }

Тут у нас агрегат пользователь, который включает в себя сущность User, а также набор желаний этого пользователя и набор друзей.
Ну пока все ок, скажете вы, разве есть какие-то проблемы с реализацией? Я считаю есть, перейдем к репозиториям.


Репозиторий.


A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.

Определение емкое, поэтому выделю основные моменты:


  • Репозиторий абстрагирует конкретное хранилище ну обычно на этом в GO проектах все и заканчивается. Да, конечно, это важно, например, при написании юнит тестов, но это далеко не вся суть репозиториев.
  • Репозитории создаются только для aggregate root. Это исходит из определения агрегата, потому как все, что мы делаем в доменном слое, должно быть сделано через корень агрегата.
  • Репозиторий предоставляет интерфейс схожий с интерфейсом коллекции.

Давайте рассмотрим типичный для GO пример репозитория и как он используется:


type UserRepository interface {    Save(*User)    Update(*User)    FindById(*User, error)}user1 := &User{}userRepo.Save(user1) // Saveuser2, _ := userRepo.FindById(1) // FindByIduser2.Name = new useruserRepo.Update(user2 ) // Update

Вопросы, которые сразу же возникают для таких репозиториев:


  • должен ли FindById загружать коллекции друзей и желаний (что ведет к расходам на дополнительные запросы)? Что, если для решения конкретной бизнес задачи эти коллекции мне не нужны?
  • Должен ли Update каждый раз проверять список друзей и желаний не изменилось ли там что-то? Как мне отслеживать эти изменения?
  • Как быть с транзакционностью? В одном кейсе я хочу сделать Save одного пользователя, а в другом кейсе я хочу, чтобы в транзакции было два Savea. Очевидно, в таком случае управление транзакцией должно быть вне метода Save. Как в данном случае избежать протечки инфраструктурной логики в домен?

Обычно в примерах GO кода такие вопросы принято обходить всеми возможными способами:


  • делаем репозитории и для агрегатов и для сущностей, ведь чем меньше (и проще) структура тем проще ее сохранить
  • сознательно избегаем кейсов, где наша реализация начинает протекать в бизнес логику, так же поступаем с кейсами, реализация которых будет выглядеть крайне громоздкой
  • берем базу данных в которую можно положить агрегат целиком (например mongo) и делаем вид, что других хранилищ не бывает и транзакции нам не нужны

А как насчет схожести интерфейса репозитория к интерфейсу GO-коллекций? Ниже представлен пример работы с коллекцией пользователей реализованной через slice:


var users []*Useruser1 := &User{}users = append(users, user1) // Saveuser2 = users[1] // FindByIduser2.Name = new user // Update

Как видите, эквивалент методу Update для слайса users просто не требуется, потому, что изменения внесенные в агрегат User применяются сразу же.


Обобщим проблемы, которые не дают DDD-like GO коду быть достаточно выразительным, тестируемым и вообще классным:


  • Типичные GO-репозитории создаются для всего подряд, агрегат, сущность может value object who cares? Причина нет ORM или других инструментов позволяющая грамотно работать сразу с графом объектов.
  • Типичные GO-репозитории не стараются походить на коллекции. В результате страдает выразительность и тестируемость кода. Знание о базе данных может протечь в бизнес логику. Причина вновь упираемся в отсутствие подходящей ORM. Можно, опять же, все делать руками, но как показывает практика это слишком неудобно.

D3 ORM. Зачем оно мне?


Хм, похоже что написать свою ORM не самая плохая идея, что я и сделал. Рассмотрим как же она помогает решить описанные выше проблемы. Для начала, как выглядит сущность Wish и агрегат User:


//d3:entity//d3_table:lw_wishtype Wish struct {    id       sql.NullInt64 `d3:"pk:auto"`    content  string    createAt time.Time}//d3:entity//d3_table:lw_usertype User struct {    id      sql.NullInt64      `d3:"pk:auto"`    name    string             `d3:"column:name"`    email   Email              `d3:"column:email"`    wishes  *entity.Collection `d3:"one_to_many:<target_entity:Wish,join_on:user_id,delete:cascade>"`    friends *entity.Collection `d3:"many_to_many:<target_entity:User,join_on:u1_id,reference_on:u2_id,join_table:lw_friend>"`}

Как видите изменений не много, но они есть. Во первых появились аннотации, с помощью которых описывается мета-информация (имя таблицы в БД, маппинг полей структуры на поля в БД, индексы). Во вторых вместо обычных для GO коллекций sliceов D3 ORM накладывает требования на использование своих коллекций. Данное требование исходит из желания иметь фичу lazy/eager loading. Можно сказать, что, если не брать в расчет кастомные коллекции, то описание бизнес сущностей делается полностью нативными средствами.


Ну что ж, а теперь перейдем непосредственно к тому, как выглядят работа с репозиториями в D3ORM:


userRepo, _:= d3orm.MakeRepository(&domain.User{})userRepo.Persists(ctx, user1) // Saveuser2, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // FindByIduser2.Name = new user // Update

Итого получаем решение которое, на сколько это возможно, повторяет интерфейс встроенных в GO коллекций. С одной маленькой ремаркой: после того, как мы выполнили все манипуляции, необходимо синхронизировать изменения с базой данных:


orm.Session(ctx).Flush()

Если вы работали с такими инструментами как: hybernate или doctrine то, для вас это не будет неожиданностью. Так же для вас не должно быть неожиданностью то, что вся работа выполняется в рамках логических транзакций сессий. Для удобства работы с сессиями в D3 ORM есть ряд функций, которые позволяют положить и вынуть их из контекста.


Разберем еще некоторые примеры кода для демонстрации тех или иных фич:


  • lazy loading, в данном примере запрос на извлечение из БД желаний пользователя будет создан и выполнен в момент непосредственного обращения к коллекции (в последней строке)

u, _ := userRepo.FindOne(ctx, userRepo.Select().AndWhere("id", "=", 1)) // будет сгенерирован запрос только для таблицы lw_userwishes := u.wishes.ToSlice() // cгенерируется запрос для таблицы lw_wish

  • transactions D3 ORM использует концепцию UnitOfWork или другими словами транзакции на уровне приложения. Все изменения накапливаются пока не будет вызван Flush(). Кроме того транзакцией можно управлять вручную, объединяя несколько Flushей в одну транзакцию

userRepo.Persists(ctx, user1)userRepo.Persists(ctx, user2)orm.Session(ctx).Flush() // стандартное поведение - при вызове Flush создается физическая транзакция, в рамках которой выполняется два insertasession := orm.Session(ctx)session.BeginTx() // переводим в ручной режим управления транзакциейuserRepo.Persists(ctx, user1)userRepo.Persists(ctx, user2)session.Flush() // в ручном режиме тут не будет сгенерировано запросов к базеuserRepo.Persists(ctx, user3)session.Flush()session.CommitTx() // на этой строчке будет сгенерирована транзакция в рамках которой выполняется три inserta 

  • при вызове Persists сохраняются все объекты от корневого (то есть граф объектов). При этом запросы в базу данных на вставку/обновление генерируются только для тех, которые действительно изменились

Подробно о том, как работать с ORM, есть документация, а также демо проект. Краткий список фич:


  • кодогенерация вместо рефлексии
  • автогенерация схемы базы данных на основе сущностей
  • один к одному, один ко многим и многие ко многим связи между сущностями
  • lazy/eager загрузка связей
  • query builder
  • загрузка связей в одном запросе к базе (используется join)
  • кэш сущностей
  • каскадное удаление и обновление связанных сущностей
  • application-level transactions (UnitOfWork)
  • DB transactions
  • поддерживается UUID

А зачем оно вам?


Резюмируя, чем вам может быть полезна D3 ORM:


  • у вас много бизнес логики и вы хотите: чтобы ваш код был как можно ближе к языку доменной области и чтобы все инварианты и вообще весь доменный слой был покрыт юнит тестами

В противном случае не могу советовать использовать D3 ORM.
А еще бы хотел описать случаи, где, по моему мнению, использовать любую ORM плохая идея:


  • если вам действительно важна производительность и вы боритесь за каждую аллокацию
  • если ваше приложение выполняет в основном READ операции. Ну право дело, для этого у нас есть отличный инструмент SQL, зачем нам что-то другое?
  • если ваше приложение тонкий клиент к базе данных. Зачем вводить ненужные абстракции?

Заключение.


Надеюсь данной статьей мне удалось хотя бы немного поставить под сомнение типичный GO-style написания бизнес логики. Кроме того, я постарался показать и альтернативу этому подходу. В любом случае решать, как писать код, вам, ну что ж, удачи в этом нелегком деле!

Подробнее..

Из песочницы Заказной черный пиар и другие грязные приемы в рамках управления репутацией в интернете

07.08.2020 16:22:19 | Автор: admin

Сейчас пул инструментов в рамках интернет-маркетинга у любой компании или интернет агентства довольно широк, от стандартных и уже устоявшихся, вроде SEO, до сравнительно новых, таких, как SMM и ORM. Если про первые два практически всё уже изучено и новые фишки появляются редко то с ORM все обстоит несколько иначе. Большинство даже не знают, что означает эта аббревиатура. ORM, или же управление репутацией, сравнительно новый способ продвижения, по крайнем мере в том виде, который есть сейчас.


В этом блоге я хотел бы разобрать базовые (и не совсем) явления в рамках управления репутацией в интернете и темой для моей первой статьи здесь станет заказной чёрный пиар.


О чем пойдет речь?


Под управлением репутацией чаще всего понимают именно исправление плохого инфополя своей компании. Однако в бизнесе нередко бывает, что помимо улучшения показателей своей компании, хотят и насолить конкурентам. В такой момент, как правило, решаются на заказной черный пиар. Стоит уточнить, что речь идет не о черном пиаре, в котором присутствует некая война в СМИ, масштабные видео-разоблачения в интернете, где цель подломить общий имидж компании или вовсе отвернуть от неё инвесторов и профессиональное сообщество, а тот вид полу-партизанского черного пиара, где нужно подгадить конкуренту и испортить репутацию в лице клиента.


То, что это не совсем честный способ ведения борьбы со своими конкурентами, всем предельно ясно, но и желающих выигрывать свое место под солнцем таким образом немало. Как происходит и какие способы чаще всего используют в рамках черного PR? Давайте разбираться.


Немного об опыте работ


Перед изложением своих мыслей на тему хотелось бы подчеркнуть, что лично я оказывался только на той стороне баррикад, когда приходилось отмазывать человека или компанию от репутационной грязи, если так можно выразиться. Цель статьи рассказать про действие и противодействие в рамках ORM, как стоит поступать в той или иной ситуации против заказного черного PR.Понятное дело, что таких способов гораздо больше и все время всплывают новые, но базовые вещи мы все-таки разберем.


С чего начинают запуск черного пиара?


Для начала необходимо понять одно главное, что потребуется для подогрева негативного интереса это инфоповод. Реальность такова, что такой, в большинстве случаев, найдется и сам, без дополнительной инициации. Практически у всех компаний или публичных личностей в биографии найдется то, в чем их можно упрекнуть и попытаться вывести на чистую воду на каком-нибудь ресурсе. Если же такого повода нет, то кто мешает попросту выдумать его? Люди не проверяют такую информацию, они склонны верить в негатив. Часто ведь можно встретить мнение, что все негативные отзывы правдивые, а положительные купленные. С инфоповодами ситуация та же самая, негатив вряд ли кто-то будет ставить под сомнение, чем очень часто пользуются, когда в дело пускают заказной черный пиар.


Самое старое и простое для черного пиара


Пожалуй, самый простой и популярный способ, когда кто-то решил заказать черный пиар вылить побольше грязи на разнообразных отзовиках и форумах. Наиболее актуально это, конечно же, для компаний, потому что вряд ли отзывы о персоне будут смотреться органично, причем не только негативные, но и положительные. С форумами все несколько интереснее: там уже можно работать с обоими типами объектов, да и развить любое обсуждение там гораздо легче. Да что уж там, большинство заказчиков такого рода услуг просто задействуют типичную бото-ферму, где можно заказать комментарии на форум или же отзывы, дают исполнителю полную свободу, а тот уже сам думает, что бы такого написать. В этом случае у автора есть куча вещей, к которым нужно прицепиться, ведь выдумывать какую-то клевету на цель, может оказаться даже весело для него и он может немного поглумиться над ней, высмеять какой-либо недостаток компании, ее продукции, личности, оторваться, если так можно выразиться. А дальше будь что будет. Скорее всего это подхватит кто-то из реальных пользователей, и некую хейт-машину будет уже довольно сложно остановить.


Как могут использовать петиции для заказного черного пиара


Один из не совсем стандартных методов ведения черного пиара это создание и поддержка разнообразных петиций, направленных против объекта черного пиара и его деятельности. Делается все предельно просто: регистрируется петиция на условном change.org, неким образом негативно влияющая на репутацию объекта, накручиваются подписи и вуаля благодаря большому поведенческому фактору на этом ресурсе и хорошему совпадению с брендовым запросом в поисковике, эта петиция быстро выстреливает в видимую зону выдачи, а убрать ее практически невозможно, тем более удалить.
Также отмечу, что петиции на данном ресурсе очень просто раскручиваются и ничего не стоит сделать 5 или 10 тысяч подписчиков под данной петицией, а учитывая фейковое количество людей, данная петиция может набрать обороты и довольно серьезно подкосить репутацию.


Очерняющие статьи


Помимо вышесказанного, довольно часто используются статьи, компрометирующие объект черного пиара, причем наиболее широкий охват получат те статьи, которые были опубликованы не в СМИ, поскольку чаще всего на такое соглашаются только желтые или полу-желтые издания, которые готовы цепляться за любой инфоповод ради поддержания интереса аудитории, а на всё тех же форумах. Наиболее популярной среди таких площадок является Пикабу там пользователи охотно подхватывают подобный материал и могут даже развить дискуссию до такой степени, что статьи в СМИ могут появиться органически. Отмыться от подобного будет очень сложно, но возможно, если делать все правильно.


Что делать, если вы стали жертвой заказного черного пиара?


Итак, допустим, что вы кому-то насолили, у вас есть злостный и не совсем честный конкурент, или же вы просто настолько успешный, что вам кто-то завидует и решил спустить вас с небес на землю. Все хорошо, но в один прекрасный день (если его можно таковым назвать) ваш специалист по связям с общественностью докладывает, что о вас вышла разгромная статья, которая может серьезно навредить вашей репутации, и, как следствие, прибыли. А после статьи еще и на форумах начали писать, какой вы плохой человек, а компания и вовсе обманывает своих клиентов. Что же делать в таком случае?


Быть на чеку


Первое, что нужно сделать для нивелирования подобного негатива это быстро найти его. Конечно, если ваши специалисты все мониторят сами и быстро находят подобный материал это хорошо, если вы пользуетесь мониторингом еще лучше, так у вас есть хорошее преимущество и некий запас времени в один-два дня, когда вы можете начать действовать.


Не пытаться договориться с плохими парнями


Главное правило, у которого может быть исключение в 5% случаев максимум это никогда, слышите, никогда не пробуйте договориться с автором статьи или петиции, либо другим лицом негативной волны. Это даст ему еще один повод написать про вас, как вы пытаетесь откупиться. Смотрите сами: если человеку или компании настолько сильно хочется удалить что-то плохое о себе, будь то статья в СМИ, на Пикабу или петиция, то может там пишут правду, которую очень уж хочется скрыть? В любом случае, удалять подобный материал лучше всего в рамках правового поля в большинстве случаев закон предусматривает удаление того или иного материала из интернет-источников, да и сами ресурсы не хотят лишних проблем с правоохранительными органами, поэтому могут удалить при правильном подходе в диалоге. Если у вас не получилось договориться с площадкой, то уже есть смысл заняться смещением ресурса в выдаче Яндекс или Google, поскольку пытаться размещать позитив в комментариях к негативной статье такой себе вариант.


Отзывы


А вот с отзывами и комментариями на форумах все куда проще просто размещаете как можно больше позитива, но важно и чем-то его подкрепить, каким-то позитивным инфоповодом, или же делать упор на заслуги объекта заказного черного пиара.


Небольшое дополнение


Стоит отметить и еще один, вполне легальный, но немного читерский способ. Да, им пользуются многие, и он вполне простой, но вдруг кто-то о нем не знает. Он включает в себя комбинацию из мониторинга и реакции на упоминания, вот только мониторинг настраивается на ваших конкурентов, одного, двух, не важно. Лучше всего будет настроить его на негативные упоминания, на которые ваши агенты влияния смогут подкрепить негатив в отношении конкурента, невзначай упомянув ваш бренд. Однако, нужно быть максимально аккуратными и не пихать подобную рекламу везде, где душе угодно, ведь массовая аудитория точно не глупая, она точно заметит подвох. Крайне важно, чтобы аккаунты, с которых вы делаете комментарии, были живыми, чтобы хотя бы пара записей и групп на нем были по тематике, на которую идет ответ на упоминание о конкуренте. Конечно, если вы хотите, чтобы про ваш бренд, несколько очерняя конкурента, написал свое мнение Ванька из 8Б, то можете этим правилом пренебречь, но чего я вам крайне не советую.


Вывод


Подытожить эту статью можно одной очень простым советом:"Храни свою репутацию, пока она еще есть" (Может что-то подобное говорил один из великих, но в этом я не шарю). Звучит настолько легко и логично, что все это понимают и без данной статьи, однако следуют этой истине очень мало людей и компаний, и обращаются за помощью, когда ситуацию исправить уже очень сложно, если вовсе возможно.


Иван Клейменов, Puppet Agency

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru