Relacionamentos
Relacionamentos são a espinha dorsal de aplicativos orientados a dados, vinculando um tipo de modelo a outro.
Por exemplo, um Usuário
pode ter muitas relações Post
, e cada Post
pode ter muitas relações Comentário
.
A API expressiva do Lucid torna o processo de associação e busca de relações de modelo simples e intuitivo, sem a necessidade de tocar em uma instrução SQL ou mesmo editar um esquema SQL.
Exemplo básico
Vamos examinar um cenário contendo dois modelos: Usuário
e Perfil
.
Em nosso exemplo, cada instância de Usuário
pode ter um único Perfil
.
Chamamos isso de relacionamento um para um.
Definindo Relacionamento
Para definir esse relacionamento, adicione o seguinte método ao seu modelo User
:
// .app/Models/User.js
const Model = use('Model')
class User extends Model {
profile () {
return this.hasOne('App/Models/Profile')
}
}
module.exports = User
No exemplo acima, adicionamos um método profile
ao modelo User
retornando um relacionamento hasOne
digitado para o modelo Profile
.
Se o modelo Profile
não existir, gere-o:
adonis make:model Profile
// .app/Models/Profile.js
const Model = use('Model')
class Profile extends Model {
}
module.exports = Profile
OBSERVAÇÃO
Não há necessidade de definir um relacionamento em ambos os modelos. Definir um caminho único em um único modelo é tudo o que é necessário.
Buscando Perfil de Usuário
Agora que definimos o relacionamento entre Usuário
e Perfil
, podemos executar o seguinte código para buscar o perfil de um usuário:
const User = use('App/Models/User')
const user = await User.find(1)
const userProfile = await user.profile().fetch()
Tem Um
Uma relação hasOne
define um relacionamento um para um usando uma chave estrangeira para o modelo relacionado.
API
hasOne(relatedModel, primaryKey, foreignKey)
relatedModel
Uma referência de contêiner IoC para o modelo que o modelo atual tem um.
primaryKey
O padrão é a chave primária do modelo atual (ou seja, id
).
foreignKey
O padrão é tableName_primaryKey
do modelo atual. A forma singular do nome da tabela é usada (por exemplo, a chave estrangeira user_id
faz referência à coluna id
na tabela users
).
Tabelas de banco de dados
Definindo relação
// .app/Models/User.js
const Model = use('Model')
class User extends Model {
profile () {
return this.hasOne('App/Models/Profile')
}
}
module.exports = User
Tem muitos
Uma relação hasMany
define um relacionamento um para muitos usando uma chave estrangeira para os outros modelos relacionados.
API
hasMany(relatedModel, primaryKey, foreignKey)
relatedModel
Uma referência de contêiner IoC para o modelo do qual o modelo atual tem muitos.
primaryKey
O padrão é a chave primária do modelo atual (por exemplo, id
).
foreignKey
O padrão é tableName_primaryKey
do modelo atual. A forma singular do nome da tabela é usada (por exemplo, a chave estrangeira user_id
faz referência à coluna id
na tabela users
).
Tabelas de banco de dados
Definindo relação
// .app/Models/User.js
const Model = use('Model')
class User extends Model {
posts () {
return this.hasMany('App/Models/Post')
}
}
module.exports = User
Pertence a
O relacionamento belongsTo
é o inverso do relacionamento hasOne e é aplicado na outra extremidade do relacionamento.
Continuando com nosso exemplo User
e Profile
, o modelo Profile
pertence ao modelo User
e, portanto, tem o relacionamento belongsTo
definido nele.
API
belongsTo(relatedModel, primaryKey, foreignKey)
relatedModel
Uma referência de contêiner IoC para o modelo ao qual o modelo atual pertence.
primaryKey
O padrão é a chave estrangeira do modelo relacionado (em nosso exemplo Profile
pertence a User
, seria user_id
).
foreignKey
O padrão é a chave primária do modelo relacionado.
Tabelas de banco de dados
Definindo relação
.app/Models/Profile.js
const Model = use('Model')
class Profile extends Model {
user () {
return this.belongsTo('App/Models/User')
}
}
module.exports = Profile
Pertence a muitos
O relacionamento belongsToMany
permite que você defina relacionamentos muitos para muitos em ambos os modelos.
Por exemplo:
- Um
Usuário
pode ter muitos modelosCarro
. - Um
Carro
pode ter muitos modelosUsuário
(ou seja, proprietários) durante sua vida útil.
Como Usuário
e Carro
podem ter muitas relações do outro modelo, dizemos que cada modelo pertence a muitos do outro modelo.
Ao definir um relacionamento belongsToMany
, não armazenamos uma chave estrangeira em nenhuma de nossas tabelas de modelo como fizemos para os relacionamentos hasOne
e hasMany
.
Em vez disso, devemos confiar em uma terceira tabela intermediária chamada de tabela dinâmica.
OBSERVAÇÃO
Você pode criar tabelas dinâmicas usando arquivos de migração.
API
belongsToMany(
relatedModel,
foreignKey,
relatedForeignKey,
primaryKey,
relatedPrimaryKey
)
relatedModel
Uma referência de contêiner IoC para o modelo do qual o modelo atual tem muitos.
foreignKey
Padrão para a chave estrangeira do modelo atual (em nosso exemplo User
pertence a muitos Car
, isso seria user_id
).
relatedForeignKey
O padrão é a chave estrangeira do modelo relacionado (em nosso exemplo User
pertence a muitos Car
, isso seria car_id
).
primaryKey
O padrão é a chave primária do modelo atual (por exemplo, id
).
relatedPrimaryKey
O padrão é a chave primária do modelo relacionado (por exemplo, id
).
Tabelas de banco de dados
Definindo relação
// .app/Models/Car.js
const Model = use('Model')
class User extends Model {
cars () {
return this.belongsToMany('App/Models/Car')
}
}
module.exports = User
No exemplo acima, a tabela chamada car_user
é a tabela dinâmica que armazena o relacionamento exclusivo entre as chaves primárias do modelo Car
e User
.
pivotTable
Por padrão, os nomes das tabelas dinâmicas são derivados pela classificação de nomes de modelos relacionados em minúsculas em ordem alfabética e unindo-os com um caractere _
(por exemplo, User
+ Car
= car_user
).
Para definir um nome de tabela dinâmica personalizado, chame pivotTable
na definição de relacionamento:
cars () {
return this
.belongsToMany('App/Models/Car')
.pivotTable('user_cars')
}
withTimestamps
Por padrão, tabelas dinâmicas não são consideradas como tendo timestamps.
Para habilitar timestamps, chame withTimestamps
na definição de relacionamento:
cars () {
return this
.belongsToMany('App/Models/Car')
.withTimestamps()
}
withPivot
Por padrão, somente chaves estrangeiras são retornadas de uma tabela dinâmica.
Para retornar outros campos da tabela dinâmica, chame withPivot
na definição do relacionamento:
cars () {
return this
.belongsToMany('App/Models/Car')
.withPivot(['is_current_owner'])
}
pivotModel
Para mais controle sobre consultas feitas a uma tabela dinâmica, você pode vincular um modelo dinâmico:
cars () {
return this
.belongsToMany('App/Models/Car')
.pivotModel('App/Models/UserCar')
}
// .app/Models/UserCar.js
const Model = use('Model')
class UserCar extends Model {
static boot () {
super.boot()
this.addHook('beforeCreate', (userCar) => {
userCar.is_current_owner = true
})
}
}
module.exports = UserCar
No exemplo acima, UserCar
é um modelo Lucid regular.
Com um modelo dinâmico atribuído, você pode usar hooks, getters/setters do ciclo de vida, etc.
OBSERVAÇÃO
Depois de chamar pivotModel
, você não pode chamar os métodos pivotTable
e withTimestamps
. Em vez disso, você precisa definir esses valores no próprio modelo de pivô.
Muitos por meio
O relacionamento manyThrough
é uma maneira conveniente de definir um relacionamento indireto.
Por exemplo:
- Um
Usuário
pertence a umPaís
. - Um
Usuário
tem muitos modelosPost
.
Usando manyThrough
, você pode buscar todos os modelos Post
para um determinado País
.
API
manyThrough(
relatedModel,
relatedMethod,
primaryKey,
foreignKey
)
relatedModel
Uma referência de contêiner IoC para o modelo que o modelo atual precisa acessar por meio para alcançar o modelo indiretamente relacionado.
relatedMethod
O método de relacionamento chamado em relatedModel
para buscar os resultados do modelo indiretamente relacionado.
primaryKey
Padrão para a chave primária do modelo atual (por exemplo, id
).
foreignKey
Padrão para a chave estrangeira para o modelo atual (em nosso exemplo de Posts
por Country
, seria country_id
).
Tabelas de banco de dados
Definindo relações
Os relacionamentos precisam ser definidos nos modelos primário e intermediário.
Continuando com nosso exemplo Posts
por Country
, vamos definir o relacionamento hasMany
necessário no modelo intermediário User
:
// .app/Models/User.js
const Model = use('Model')
class User extends Model {
posts () {
return this.hasMany('App/Models/Post')
}
}
Finalmente, defina o relacionamento manyThrough
no modelo primário Country
:
// .app/Models/Country.js
const Model = use('Model')
class Country extends Model {
posts () {
return this.manyThrough('App/Models/User', 'posts')
}
}
No exemplo acima, o segundo parâmetro é uma referência ao método posts
no modelo User
.
OBSERVAÇÃO
O parâmetro relatedMethod
deve sempre ser passado para o método manyThrough
para que um relacionamento many through funcione.
Consultando dados
A consulta de dados relacionados é bastante simplificada pela API intuitiva do Lucid, fornecendo uma interface consistente para todos os tipos de relacionamentos de modelo.
Se um Usuário
tiver muitos modelos Post
, podemos buscar todas as postagens para o usuário id=1
assim:
const User = use('App/Models/User')
const user = await User.find(1)
const posts = await user.posts().fetch()
Adicione restrições de tempo de execução chamando os métodos Query Builder como uma consulta típica:
const user = await User.find(1)
// published posts
const posts = await user
.posts()
.where('is_published', true)
.fetch()
O exemplo acima busca todas as postagens publicadas para o usuário id=1
.
Consultando Tabela Dinâmica
Você pode adicionar cláusulas where
para tabelas dinâmicas belongsToMany
assim:
const user = await User.find(1)
const cars = await user
.cars()
.wherePivot('is_current_owner', true)
.fetch()
O exemplo acima busca todos os carros cujo proprietário atual é o usuário id=1
.
Os métodos whereInPivot
e orWherePivot
também estão disponíveis.
Carregamento rápido
Quando você deseja buscar relações para mais de uma relação base (por exemplo, postagens para mais de um usuário), o carregamento rápido é a maneira preferida de fazer isso.
Carregamento rápido é o conceito de buscar relações com o mínimo de consultas de banco de dados possível em uma tentativa de evitar o problema n+1
.
Sem carregamento rápido, usando as técnicas discutidas anteriormente nesta seção:
// Not Recommended
const User = use('App/Models/User')
const users = await User.all()
const posts = []
for (let user of users) {
const userPosts = await user.posts().fetch()
posts.push(userPosts)
}
O exemplo acima faz n+1
consultas ao banco de dados, onde n
é o número de usuários. Fazer um loop por um grande número de usuários resultaria em uma grande sequência de consultas feitas ao banco de dados, o que dificilmente é o ideal!
Com carregamento rápido, apenas 2 consultas são necessárias para buscar todos os usuários e suas postagens:
// Recommended
const User = use('App/Models/User')
const users = await User
.query()
.with('posts')
.fetch()
O método with
carrega a relação passada como parte da carga original, então executar users.toJSON()
agora retornará uma saída como esta:
// JSON Output
[
{
id: 1,
username: 'virk',
posts: [{
id: 1,
user_id: 1,
title: '...'
}]
}
]
Na saída JSON acima, cada objeto User
agora tem uma propriedade de relacionamento posts
, facilitando a identificação rápida de qual Post
pertence a qual User
.
Adicionando Restrições de Tempo de Execução
Adicione restrições de tempo de execução a relacionamentos carregados como esta:
const users = await User
.query()
.with('posts', (builder) => {
builder.where('is_published', true)
})
.fetch()
Carregando Múltiplas Relações
Múltiplas relações podem ser carregadas encadeando o método with
:
const users = await User
.query()
.with('posts')
.with('profile')
.fetch()
Carregando Relações Aninhadas
Relações aninhadas são carregadas via notação de ponto.
A consulta a seguir carrega todas as postagens de Usuário
e seus comentários relacionados:
const users = await User
.query()
.with('posts.comments')
.fetch()
Os retornos de chamada de restrição de relação aninhada se aplicam somente à última relação:
const users = await User
.query()
.with('posts.comments', (builder) => {
builder.where('approved', true)
})
.fetch()
No exemplo acima, a cláusula builder.where
é aplicada somente ao relacionamento comments
(não ao relacionamento posts
).
Para adicionar uma restrição à primeira relação, use a seguinte abordagem:
const users = await User
.query()
.with('posts', (builder) => {
builder.where('is_published', true)
.with('comments')
})
.fetch()
No exemplo acima, uma restrição where
é adicionada à relação posts
enquanto posts.comments
é carregado ao mesmo tempo.
Recuperando dados de modelos carregados
Para recuperar os dados carregados, você deve chamar o método getRelated
:
const user = await User
.query()
.with('posts')
.fetch()
const posts = user.getRelated('posts')
Lazy Eager Loading
Para carregar relacionamentos depois de já ter buscado dados, use o método load
.
Por exemplo, para carregar posts
relacionados depois de já buscar um User
:
const user = await User.find(1)
await user.load('posts')
Você pode carregar vários relacionamentos preguiçosamente usando o método loadMany
:
const user = await User.find(1)
await user.loadMany(['posts', 'profiles'])
Para definir restrições de consulta via loadMany
, você deve passar um objeto:
const user = await User.find(1)
await user.loadMany({
posts: (builder) => builder.where('is_published', true),
profiles: null
})
Recuperando dados de modelos carregados
Para recuperar os dados carregados, você deve chamar o método getRelated
:
const user = await User.find(1)
await user.loadMany(['posts', 'profiles'])
const posts = user.getRelated('posts')
const profiles = user.getRelated('profiles')
Filtrando dados
A API do Lucid simplifica a filtragem de dados dependendo da existência de um relacionamento.
Vamos usar o exemplo clássico de encontrar todos os posts com comentários.
Aqui está nosso modelo Post
e sua definição de relacionamento comments
:
// .app/Models/Post.js
const Model = use('Model')
class Post extends Model {
comments () {
return this.hasMany('App/Models/Comments')
}
}
has
Para recuperar apenas posts com pelo menos um Comment
, encadeie o método has
:
const posts = await Post
.query()
.has('comments')
.fetch()
É simples assim! 😲
Adicione uma restrição de expressão/valor ao método has
assim:
const posts = await Post
.query()
.has('comments', '>', 2)
.fetch()
O exemplo acima recuperará apenas posts com mais de 2 comentários.
whereHas
O método whereHas
é semelhante ao has
, mas permite restrições mais específicas.
Por exemplo, para buscar todas as postagens com pelo menos 2 comentários publicados:
const posts = await Post
.query()
.whereHas('comments', (builder) => {
builder.where('is_published', true)
}, '>', 2)
.fetch()
doesntHave
O oposto da cláusula has
:
const posts = await Post
.query()
.doesntHave('comments')
.fetch()
NOTA
Este método não aceita uma restrição de expressão/valor.
whereDoesntHave
O oposto da cláusula whereHas
:
const posts = await Post
.query()
.whereDoesntHave('comments', (builder) => {
builder.where('is_published', false)
})
.fetch()
NOTA
Este método não aceita uma restrição de expressão/valor.
Você pode adicionar uma cláusula or
chamando os métodos orHas
, orWhereHas
, orDoesntHave
e orWhereDoesntHave
.
Contagens
Recupere contagens de relacionamento chamando o método withCount
:
const posts = await Post
.query()
.withCount('comments')
.fetch()
posts.toJSON()
// JSON Output
{
title: 'Adonis 101',
__meta__: {
comments_count: 2
}
}
Defina um alias para uma contagem como:
const posts = await Post
.query()
.withCount('comments as total_comments')
.fetch()
// JSON Output
__meta__: {
total_comments: 2
}
Restrições de contagem
Por exemplo, para recuperar apenas a contagem de comentários que foram aprovados:
const posts = await Post
.query()
.withCount('comments', (builder) => {
builder.where('is_approved', true)
})
.fetch()
Inserções, atualizações e exclusões
Adicionar, atualizar e excluir registros relacionados é tão simples quanto consultar dados.
save
O método save
espera uma instância do modelo relacionado.
save
pode ser aplicado aos seguintes tipos de relacionamento:
hasOne
hasMany
belongsToMany
const User = use('App/Models/User')
const Post = use('App/Models/Post')
const user = await User.find(1)
const post = new Post()
post.title = 'Adonis 101'
await user.posts().save(post)
create
O método create
é semelhante a save
, mas espera um objeto JavaScript simples, retornando a instância do modelo relacionado.
create
pode ser aplicado aos seguintes tipos de relacionamento:
hasOne
hasMany
belongsToMany
const User = use('App/Models/User')
const user = await User.find(1)
const post = await user
.posts()
.create({ title: 'Adonis 101' })
createMany
Salve muitas linhas relacionadas no banco de dados.
createMany
pode ser aplicado aos seguintes tipos de relacionamento:
hasMany
belongsToMany
const User = use('App/Models/User')
const user = await User.find(1)
const post = await user
.posts()
.createMany([
{ title: 'Adonis 101' },
{ title: 'Lucid 101' }
])
saveMany
Semelhante a save
, mas salva várias instâncias do modelo relacionado:
saveMany
pode ser aplicado aos seguintes tipos de relacionamento:
hasMany
belongsToMany
const User = use('App/Models/User')
const Post = use('App/Models/Post')
const user = await User.find(1)
const adonisPost = new Post()
adonisPost.title = 'Adonis 101'
const lucidPost = new Post()
lucidPost.title = 'Lucid 101'
await user
.posts()
.saveMany([adonisPost, lucidPost])
associate
O método associate
é exclusivo do relacionamento belongsTo
, associando duas instâncias de modelo entre si.
Supondo que um Perfil
pertença a um Usuário
, para associar um Usuário
a um Perfil
:
const Profile = use('App/Models/Profile')
const User = use('App/Models/User')
const user = await User.find(1)
const profile = await Profile.find(1)
await profile.user().associate(user)
dissociate
O método dissociate
é o oposto de associate
.
Para remover um relacionamento associado:
const Profile = use('App/Models/Profile')
const profile = await Profile.find(1)
await profile.user().dissociate()
attach
O método attach
é chamado em um relacionamento belongsToMany
para anexar um modelo relacionado via tabela dinâmica:
const User = use('App/Models/User')
const Car = use('App/Models/Car')
const mercedes = await Car.findBy('reg_no', '39020103')
const user = await User.find(1)
await user.cars().attach([mercedes.id])
O método attach
aceita um retorno de chamada opcional que recebe a instância pivotModel
, permitindo que você defina propriedades extras em uma tabela dinâmica, se necessário:
const mercedes = await Car.findBy('reg_no', '39020103')
const audi = await Car.findBy('reg_no', '99001020')
const user = await User.find(1)
const cars = [mercedes.id, audi.id]
await user.cars().attach(cars, (row) => {
if (row.car_id === mercedes.id) {
row.is_current_owner = true
}
})
OBSERVAÇÃO
Os métodos create
e save
para relacionamentos belongsToMany
também aceitam um retorno de chamada que permite que você defina propriedades extras em uma tabela dinâmica, se necessário.
detach
O método detach
é o oposto do método attach
, removendo todos os relacionamentos de tabela dinâmica existentes:
const user = await User.find(1)
await user.cars().detach()
Para desanexar apenas as relações selecionadas, passe uma matriz de ids:
const user = await User.find(1)
const mercedes = await Car.findBy('reg_no', '39020103')
await user.cars().detach([mercedes.id])
sync
O método sync
fornece um atalho conveniente para detach
e então attach
:
const mercedes = await Car.findBy('reg_no', '39020103')
const user = await User.find(1)
// Behave the same way as:
// await user.cars().detach()
// await user.cars().attach([mercedes.id])
await user.cars().sync([mercedes.id])
update
O método update
atualiza em massa as linhas consultadas.
Você pode usar os métodos Query Builder para atualizar apenas campos específicos:
const user = await User.find(1)
await user
.posts()
.where('title', 'Adonis 101')
.update({ is_published: true })
Para atualizar uma tabela dinâmica, chame pivotQuery
antes de update
:
const user = await User.find(1)
await user
.cars()
.pivotQuery()
.where('name', 'mercedes')
.update({ is_current_owner: true })
delete
O método delete
remove linhas relacionadas do banco de dados:
const user = await User.find(1)
await user
.cars()
.where('name', 'mercedes')
.delete()
OBSERVAÇÃO
No caso de belongsToMany
, esse método também remove o relacionamento da tabela dinâmica.