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

Обзор Prisma ORM



Это статья-обзор о 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% скидку на первый месяц аренды сервера любой конфигурации!

Источник: habr.com
К списку статей
Опубликовано: 29.04.2021 10:23:13
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании маклауд

Javascript

Программирование

Go

Typescript

Prisma

Orm

Vps

Vds

Категории

Последние комментарии

  • Имя: Макс
    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