Skip to content

Commit

Permalink
feat(BaseModel): add updateOrCreateMany to avoid creating duplicate rows
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 12, 2020
1 parent 45ea50e commit 7c24bdc
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 11 deletions.
31 changes: 21 additions & 10 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,16 +735,6 @@ declare module '@ioc:Adonis/Lucid/Model' {
options?: ModelAdapterOptions,
): Promise<InstanceType<T>>

/**
* Returns the first row or save it to the database
*/
updateOrCreate<T extends ModelConstructorContract> (
this: T,
search: any,
updatePayload: any,
options?: ModelAdapterOptions,
): Promise<InstanceType<T>>

/**
* Find rows or create in-memory instances of the missing
* one's.
Expand All @@ -754,6 +744,7 @@ declare module '@ioc:Adonis/Lucid/Model' {
uniqueKey: string,
payload: ModelObject[],
options?: ModelAdapterOptions,
mergeAttributes?: boolean,
): Promise<InstanceType<T>[]>

/**
Expand All @@ -767,6 +758,26 @@ declare module '@ioc:Adonis/Lucid/Model' {
options?: ModelAdapterOptions,
): Promise<InstanceType<T>[]>

/**
* Returns the first row or save it to the database
*/
updateOrCreate<T extends ModelConstructorContract> (
this: T,
search: any,
updatePayload: any,
options?: ModelAdapterOptions,
): Promise<InstanceType<T>>

/**
* Update existing rows or create new one's.
*/
updateOrCreateMany<T extends ModelConstructorContract> (
this: T,
uniqueKey: string,
payload: ModelObject[],
options?: ModelAdapterOptions,
): Promise<InstanceType<T>[]>

/**
* Fetch all rows
*/
Expand Down
22 changes: 21 additions & 1 deletion src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ export class BaseModel implements ModelContract {
uniqueKey: string,
payload: ModelObject[],
options?: ModelAdapterOptions,
mergeAttributes: boolean = false,
) {
const castKey = this.$refs[uniqueKey]
if (!castKey) {
Expand Down Expand Up @@ -512,6 +513,9 @@ export class BaseModel implements ModelContract {
/* eslint-disable-next-line eqeqeq */
const existingRow = existingRows.find((one) => one[uniqueKey] == row[uniqueKey])
if (existingRow) {
if (mergeAttributes) {
existingRow.merge(row)
}
return existingRow
}

Expand All @@ -534,7 +538,7 @@ export class BaseModel implements ModelContract {
options?: ModelAdapterOptions,
) {
const rows = await this.fetchOrNewUpMany(uniqueKey, payload, options)
await await Promise.all(rows.map((row) => {
await Promise.all(rows.map((row) => {
if (!row.$persisted) {
return row.save()
}
Expand All @@ -544,6 +548,22 @@ export class BaseModel implements ModelContract {
return rows
}

/**
* Update existing rows or create missing one's. One database call per insert
* is invoked, so that each insert and update goes through the lifecycle
* of model hooks.
*/
public static async updateOrCreateMany<T extends ModelConstructorContract> (
this: T,
uniqueKey: string,
payload: ModelObject[],
options?: ModelAdapterOptions,
) {
const rows = await this.fetchOrNewUpMany(uniqueKey, payload, options, true)
await Promise.all(rows.map((row) => row.save()))
return rows
}

/**
* Create a array of model instances from the adapter result
*/
Expand Down
210 changes: 210 additions & 0 deletions test/orm/base-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,216 @@ test.group('Base Model | fetch', (group) => {
const usersList = await db.query().from('users')
assert.lengthOf(usersList, 1)
})

test('persist records to db when find call returns zero rows', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

@column()
public email: string

@column()
public points: number
}

const users = await User.updateOrCreateMany(
'username',
[
{
username: 'virk',
email: '[email protected]',
},
{
username: 'nikk',
email: '[email protected]',
},
{
username: 'romain',
email: '[email protected]',
},
],
)

assert.lengthOf(users, 3)
assert.isTrue(users[0].$persisted)
assert.equal(users[0].username, 'virk')
assert.equal(users[0].email, '[email protected]')

assert.isTrue(users[1].$persisted)
assert.equal(users[1].username, 'nikk')
assert.equal(users[1].email, '[email protected]')

assert.isTrue(users[2].$persisted)
assert.equal(users[2].username, 'romain')
assert.equal(users[2].email, '[email protected]')

const usersList = await db.query().from('users')
assert.lengthOf(usersList, 3)
})

test('update records and avoiding duplicates', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

@column()
public email: string

@column()
public points: number
}

await db.insertQuery().table('users').insert({
username: 'virk',
email: '[email protected]',
points: 10,
})

const users = await User.updateOrCreateMany(
'username',
[
{
username: 'virk',
email: '[email protected]',
points: 4,
},
{
username: 'nikk',
email: '[email protected]',
},
{
username: 'romain',
email: '[email protected]',
},
],
)

assert.lengthOf(users, 3)
assert.isTrue(users[0].$persisted)
assert.equal(users[0].username, 'virk')
assert.equal(users[0].email, '[email protected]')
assert.equal(users[0].points, 4)

assert.isTrue(users[1].$persisted)
assert.equal(users[1].username, 'nikk')
assert.equal(users[1].email, '[email protected]')
assert.isUndefined(users[1].points)

assert.isTrue(users[2].$persisted)
assert.equal(users[2].username, 'romain')
assert.equal(users[2].email, '[email protected]')
assert.isUndefined(users[2].points)

const usersList = await db.query().from('users')
assert.lengthOf(usersList, 3)
})

test('wrap create calls inside a transaction using updateOrCreateMany', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

@column()
public email: string

@column()
public points: number
}

await db.insertQuery().table('users').insert({
username: 'virk',
email: '[email protected]',
points: 10,
})

const trx = await db.transaction()

await User.updateOrCreateMany(
'username',
[
{
username: 'virk',
email: '[email protected]',
},
{
username: 'nikk',
email: '[email protected]',
},
{
username: 'romain',
email: '[email protected]',
},
],
{
client: trx,
},
)

await trx.rollback()
const usersList = await db.query().from('users')
assert.lengthOf(usersList, 1)
})

test('wrap update calls inside a transaction using updateOrCreateMany', async (assert) => {
class User extends BaseModel {
@column({ primary: true })
public id: number

@column()
public username: string

@column()
public email: string

@column()
public points: number
}

await db.insertQuery().table('users').insert({
username: 'virk',
email: '[email protected]',
points: 10,
})

const trx = await db.transaction()

await User.updateOrCreateMany(
'username',
[
{
username: 'virk',
email: '[email protected]',
points: 4,
},
{
username: 'nikk',
email: '[email protected]',
},
{
username: 'romain',
email: '[email protected]',
},
],
{
client: trx,
},
)

await trx.rollback()
const usersList = await db.query().from('users')
assert.lengthOf(usersList, 1)
assert.equal(usersList[0].points, 10)
})
})

test.group('Base Model | hooks', (group) => {
Expand Down

0 comments on commit 7c24bdc

Please sign in to comment.