I need to call an upsert
to my Category table in PostgreSQL. My current solution uses the exec_query
API in ActiveRecord, using binding.
What I got so far looks like this:
VERIFY_CATEGORY_SQL = <<SQL
WITH sel AS (SELECT id FROM categories WHERE name=$1 AND source=$2),
ins AS (INSERT INTO categories (name, source, created_at, updated_at) SELECT
$3, $4, 'now', 'now' WHERE NOT EXISTS (SELECT * from sel) RETURNING id)
select id from ins union all select id from sel
SQL
VERIFY_CATEGORY_SQL = <<SQL
WITH sel AS (SELECT id FROM categories WHERE name=$1 AND source=$2),
ins AS (INSERT INTO categories (name, source, created_at, updated_at) SELECT
$1, $2, now(), now() WHERE NOT EXISTS (SELECT * from sel) RETURNING id)
select id from ins union all select id from sel
SQL
def verify_category_existence(name, source)
ActiveRecord::Base.connection.exec_query(VERIFY_CATEGORY_SQL, 'SQL',
[[nil, name], [nil, source]]).first['id'].to_i
end
Notes
- In order to make this work I needed to add
prepared_statements: true
attribute to mydatabase.yml
file. Although I need to reuse my(Apparently this works fine...)name
andsource
parameters, I can't and have to duplicate them as parameters$1, $3
and$2, $4
respectively- The method call seems a bit hackish, and I believe maybe it was not meant as a public method, and only intended for internal uses (hence the awkward
[[nil, x]...]
syntax and the'SQL'
parameter, which apparently is for logging purposes)
What do you think of this solution?
Do you know of a better way to do this?
Edit:
I've received great responses to the SQL/Postgresql side of my solution, but I would love to also hear from our rubyists regarding the Ruby/ActiveRecord part of the solution - is this way correct? is there a better solution?
Edit:
Just found out that using 'now'
produced unexpected results - timestamp values were re-used rather than calculated on each insert. Changed it to now()
, and it works better.
PostgreSQL
you could just re-use$1
and$2
; try it withPREPARE
thenEXECUTE
inpsql
. So if you can't here, it's a Rails or Pg gem limitation. My main concern is that your insert-if-not-exists implementation is subject to race conditions; concurrently running statements will both insert rows. You'll needSERIALIZABLE
isolation to prevent that (test and make sure that's enough, I think it is), or you'll need toLOCK TABLE ... IN EXCLUSIVE MODE
first. See stackoverflow.com/q/17267417/398670 \$\endgroup\$SERIALIZABLE
? Could you show me an example? \$\endgroup\$SERIALIZABLE
, see postgresql.org/docs/current/static/transaction-iso.html . It's important to understand that if you use serializable isolation, it'll cause an error when you encounter a race condition, so your code must handle errors and retry the transaction. \$\endgroup\$SERIALIZABLE
if I have a unique constraint on the relevant columns? Won't it be enough? If so, I should still expect a unique exception, and retry, right? \$\endgroup\$