Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction doesn't rollback #1014

Closed
emorris00 opened this issue Oct 10, 2017 · 13 comments
Closed

Transaction doesn't rollback #1014

emorris00 opened this issue Oct 10, 2017 · 13 comments
Labels

Comments

@emorris00
Copy link

Version: 0.0.11

The "Database entity is not set for the given subject" error does not rollback transactions

// this will remove row with id 1 but not id 2
repository.transaction(async repository=> {
    repository.remove({id: 1})
    repository.remove({}) // errors out here, but also commits transaction
    repository.remove({id: 2})
})
@pleerock
Copy link
Member

You need to await pending promises:

// this will remove row with id 1 but not id 2
repository.transaction(async repository=> {
    await repository.remove({id: 1})
    await repository.remove({}) // errors out here, but also commits transaction
    await repository.remove({id: 2})
})

Also I strongly recommend you to update orm version to 0.1.0

@emorris00
Copy link
Author

emorris00 commented Oct 11, 2017

Ah sorry, apparently I didn't understand my problem. It appears that the problem is with using Promise.all. Let me know what you think. This is using version 0.1.0

import {createConnection, getConnection, Connection, Entity, PrimaryColumn} from 'typeorm'


@Entity()
class TestEntity {
    @PrimaryColumn({type: "int"})
    id: number
}

// transaction should throw and not delete the row
// passes
async function test1() {
    let connection = getConnection()
    await connection.manager.save(TestEntity, {id: 1})

    console.assert(await connection.manager.findOneById(TestEntity, 1), "row doesn't exist before transaction")
    
    let didThrow = false
    try {
        await connection.transaction(async manager => {
            await manager.remove(TestEntity, {id: 1})
            await new Promise((resolve, reject) => reject('reject'))
        })
    }
    catch(e) {
        didThrow = true
    }

    console.assert(didThrow, "transaction didn't throw")
    console.assert(await connection.manager.findOneById(TestEntity, 1), "row doesn't exist after transaction")
}

// transaction should throw and not delete the row
// fails, row gets deleted
async function test2() {
    let connection = getConnection()
    await connection.manager.save(TestEntity, {id: 1})

    console.assert(await connection.manager.findOneById(TestEntity, 1), "row doesn't exist before transaction")

    let didThrow = false
    try {
        await connection.transaction(async manager => {
            await Promise.all([
                manager.remove(TestEntity, {id: 1}),
                new Promise((resolve, reject) => reject('reject')),
            ])
        })
    }
    catch(e) {
        didThrow = true
    }

    console.assert(didThrow, "transaction didn't throw")
    console.assert(await connection.manager.findOneById(TestEntity, 1), "row doesn't exist after transaction")
}


async function runTest(func: Function) {
    try {
        await func()
        console.log(`${func.name} passed`)
    }
    catch(e) {
        console.log(`${func.name} failed,`, e.message)
    }
}


createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "",
    password: "",
    database: "test",
    entities: [TestEntity],
    synchronize: true,
}).then(async connection => {
    await runTest(test1)
    await runTest(test2)
    process.exit()
})

@pleerock
Copy link
Member

can you simplify your code and point me to exact line where your problem is?

@emorris00
Copy link
Author

// this throws an error, but the entity still gets removed
await connection.transaction(async manager => {
    await Promise.all([
        manager.remove(TestEntity, {id: 1}),
        new Promise((resolve, reject) => reject('reject')),
    ])
})

pleerock pushed a commit that referenced this issue Oct 13, 2017
@pleerock
Copy link
Member

I can reproduce this issue.

However, its how promises are working and its because of how Promise.all works.
The problem is that when promise.all executes it does not wait for all promises to be resolved and throws error once any of promise rejects but other promises are executed at this moment, after rejection has made. ORM listens to rejects and executed rollback. But since there is a promise in execution at the same moment, DELETE is executed anyway.

Solution is to combine for loop and await, or simply run promises in order without using Promise.all. You can also use PromiseUtils.runInSequence.

You need something like bluebird's settle function. I just added it into PromiseUtils class internally used in orm. Example:

    /**
     * Returns a promise that is fulfilled with an array of promise state snapshots,
     * but only after all the original promises have settled, i.e. become either fulfilled or rejected.
     */
    function settle(promises: Promise<any>[]) {
        return Promise.all(promises.map(p => Promise.resolve(p).then(v => ({
            state: "fulfilled",
            value: v,
        }), r => ({
            state: "rejected",
            reason: r,
        })))).then((results: any[]): any => {
            const rejected = results.find(result => result.state === "rejected");
            if (rejected)
                return Promise.reject(rejected.reason);

            return results.map(result => result.value);
        });
    }

    // --------------------

     await connection.transaction(manager => {
                await settle([
                    manager.remove(TestEntity, { id: 1 }),
                    new Promise((resolve, reject) => reject(new Error())),
                ]);
      });

More detailed answer to the problem and solution is here.

@teyou
Copy link
Contributor

teyou commented Oct 13, 2017

await [manager.remove(TestEntity, {id: 1}), 
       new Promise((resolve, reject) => reject('reject'))
       ].reduce((prev, cur) => prev.then(cur), Promise.resolve());

another way of running Promise in a sequence

@emorris00
Copy link
Author

emorris00 commented Oct 13, 2017

This is just an idea, but would it be possible to have the query runners store references to the promises from queries they run if its in a transaction, and then change rollbackTransaction to wait for all of them to resolve before rolling back?

@pleerock
Copy link
Member

QueryRunner does not work with promises, it just accepts a single promise. Its unlikely possible what you are saying because even if we do crazy transaction method which accepts multiple arguments - functions that return multiple promises there can be lot of nested promises inside them which will have same issue.

@emorris00
Copy link
Author

emorris00 commented Oct 13, 2017

I'm not sure we are talking about the same thing, let me give you a rough example of what I mean.

class MysqlQueryRunner {
	promises = []

	query(query: string, parameters?: any[]): Promise<any> {
		if (this.isReleased)
			throw new QueryRunnerAlreadyReleasedError();

		let promise = new Promise(async (ok, fail) => {
			// ...
		});

                // store the promise from query if runner is in a transaction
		if(this.isTransactionActive) {
			this.promises.push(promise);
		}

		return promise
	}


	async rollbackTransaction(): Promise<void> {
		if (!this.isTransactionActive)
			throw new TransactionNotStartedError();

                // wait for all stored promises to resolve before rolling back
		await settle(this.promises);
		await this.query("ROLLBACK");
		this.isTransactionActive = false;
		this.promises = [];
	}
}

@pleerock
Copy link
Member

its not scalable solution at all because you are covering only Promise.all on first level, we aren't you consider about Promise.all inside Promise.all ?

@pleerock
Copy link
Member

I think this can be closed.

f-wrobel added a commit to f-wrobel/typeorm that referenced this issue Nov 9, 2017
* added cli options to init command generated project

* renamed TableSchema in to Table

* renamed ColumnSchema in to TableColumn;
renamed ForeignKeySchema in to TableForeignKey;
renamed IndexSchema in to TableIndex;
renamed PrimaryKeySchema in to TablePrimaryKey;

* added few websql-specific options

* added failing test

* Small fix for schema update after typeorm#858

The implementation of typeorm#858 broke the schema by altering the default valie without the metadata knowing about it.

This fixed it

* fixed test

* fixed linting error

* fixes typeorm#945

* fixed test

* made query runner to await

* fix: typeorm#953

* SchemaBuilder: Table.addPrimaryKeys and removePrimaryKeys also change isPrimary on the column

* fixed docs

* fixed countquery for sqlite based drivers

* update test
1. add enabledDrivers
2. assert array exactly the same

* fixed bug with duplicate entity columns in inheritance tree

* Fix column naming for deeper embedded entities

Example from provided test would be "countersInformationDescription" which previously would get database name "countersCountersInformationDescription". Problem was that parentEmbeddedMetadata was already taken into consideration when building prefix so anything deeper than one level would add earlier prefixes multiple times.

Previously `buildParentPrefixes` for `countersInformationDescription` would be `["counters", "counters_information]` and now it is simply `["counters_information"]`.

* updated docs, getting ready for 0.1.0 release

* updated mssql docker image

* docs update

* improving docs

* improving docs

* improving docs

* added ionic example to docs

* text case for issue typeorm#966

* docs: spelling and general improvements

* In the case of .env file, it will load the default .env file name.

It will introduce a breaking change because it will not locate ormconfig.env, instead it will search for a .env file.

* docs: spelling and general improvements (part 2)

* docs: spelling and general improvements (part 3)

* Now it will try to load ormconfig.env file and if it does not exists, it will then try to load the global .env file

* Workaround fix for typeorm#983

For some reason the null default values are reported as "NULL" on some columns. This is a small workaround for this particular case
MySQL does not have this issue, works fine

* docs: spelling and general improvements (part 4)

* reverted decorator-reference changed, changed links

* fixed links

* spelling and general improvements (part 5)

* issue typeorm#992 and typeorm#989

* merged other doc changes

* WebSQL version should be a string

* docs(websql): version is a string, not a number

* test(entity): added a testcase for typeorm#1002

* driver(websql): support uuid generated primary column

* improving docs

* improving docs

* improving docs

* improving docs

* improving docs

* improving docs

* improving docs

* improving docs

* version bump

* fixed issue with relation id loader not properly working with inverse many-to-many relations

* Update RelationIdLoader.ts

* added test for typeorm#1014

* removed only

* fixes typeorm#1011

* fixes typeorm#991

* fixes typeorm#990

* fixes typeorm#975

* fixes typeorm#975

* added 1 and -1 ordering support via find options. Added tests for typeorm#970

* fixes typeorm#966

* added typeorm-model-generator to extensions section in README

* added empty line

* added export to Database type, fixes typeorm#949

* deprecated isArray in column options

* fixed bug in transaction decorator

* added test for typeorm#948; fixed issue with array not working when defined alternatively way in entity

* fixed issue with array not working when defined alternatively way in entity

* reverted change making array column types to be determined automatically

* fixed issue with array not working when defined alternatively way in entity

* re-implemented update, updateById, removeById methods; added delete and insert methods

* fixing issues with new persistence methods

* fixes issues with update, insert, delete query builder; added support of embeds in there, plus in find options. Fixes typeorm#931 typeorm#971 typeorm#999 typeorm#942

* removed only tests, remove logs, fixed issue with sqlite insert default

* fixed broken test

* fixed broken test

* fixed broken test

* fixed broken test

* implemented multi-schema support in junction tables, fixes typeorm#1030

* fixed failing tests

* fixed failing tests; updated package deps

* version bump

* updated changelog

* expression was missing mode

* fixes typeorm#716

* Fix typo in migration docs

* fixes typeorm#1037

* fixes #typeorm#798 and typeorm#799

* fixes typeorm#1042

* better support operations with regular tables in query builders

* fixed compiler errors

* fixed compiler errors

* small changes

* docs: fix select-query-builder.md link

* added test for typeorm#1048

* removed only

* fixed broken version command

* refactoring persistence - step1

* refactoring persistence - step 2

* refactoring persistence - step 3

* refactoring persistence - step 4

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* driver(cordova): enabled foreign_keys

* add missing link to slides

* th -> the

* refactoring persistence

* refactoring persistence - removed cascade removes!

* fixed broken version command

* removed test because of removed cascade remove functionality

* refactoring persistence

* Adding cache options to repository

* Removing extra empty line

* Adding missing test and util logic

* Adding cache to find-options docs

* Updating changelog

* Updating caching docs to include new repository cache options

* added the citext column in postgres

* updated changelog for typeorm#1075

cc: @pleerock

* refactoring persistence

* refactoring persistence - added many to one uni directional tests

* fix: missing export TransactionRepository in index.ts

* Added sqlite type in ormTemplate

Fixed MissingDriver error for sqlite

* Add support for MongoDB authentication

* Add ObjectId conversion to `findOneById` method

* Add missing `create` overloads in `BaseEntity`

* refactoring persistence

* refactoring persistence

* refactoring persistence - added many to one uni directional tests

* refactoring persistence

* refactoring persistence

* refactoring persistence - added many to one uni directional tests

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* Update README.md

* refactoring persistence

* refactoring persistence

* fix changelog typo error

* refactoring persistence

* Fix tree entity sample code in docs

* refactoring persistence

* refactoring persistence

* refactoring persistence

* Update custom-repository.md

Minor typo

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* Wrong column name

I think you meant `categoryName` instead of `categoryId` here.

* refactoring persistence

* fixed compiler errors caused by typescript 2.6 upgration; version bump

* fixed indentation

* explicit require() statements for the drivers that call them

This allows us to bundle into a server.js with webpack. Useful for
Isomorphic JS projects.

* Fix minor typo in relations-faq.md

* fixed merging conflict issues

* refactoring persistence

* fixed bad query when using take on entity with multiple pk

* better style

* fixed failing citext test

* refactoring persistence

* Update CHANGELOG.md

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* refactoring persistence

* removed useless "schemaCreate: true" from tests

* removed useless "dropSchema: true" from tests

* removed useless "dropSchema: true" from tests

* refactoring persistence

* removed old way of inserting/updating/deleting in query runners

* give example for how to work with webpack, fix tslint errors

* switch the webpack how to to faq rather than its own file

* refactoring persistence

* added listeners and subscribers support via insert/update/delete query builders (typeorm#1104)

* moved broadcaster from the connection into query runner

* moved broadcaster from the connection into query runner

* implemented bulk insert and remove operations support (typeorm#1025)

* optimized remove process and implemented topological sorting in remove as well

* fixed issues with topological sorting and implemented transactions support in query builders

* refactoring persistence

* Correct typo in function name

Normilize -> Normalize
( i  -> a )

* reimplemented entity updation after save method call and after query builders calls mechanizm

* version bump
@calvintwr
Copy link

Hi, just ran into this problem. Has it changed? I noticed that in other ORMs, there isn’t this problem.

Is the solution then to handle transaction manually — wrap it in try/catch, and call rollback in catch?

@emorris00
Copy link
Author

I think you should be able to create a promise that runs Promise.allSettled and rejects if any of the promises in Promise.allSettled rejected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants