22
Когда возможностей ActiveRecord недостаточно Sequel: ORM для ценителей Postgres

Когда возможностей Active record недостаточно

Embed Size (px)

Citation preview

Page 1: Когда возможностей Active record недостаточно

Когда возможностей ActiveRecord недостаточноSequel: ORM для ценителей Postgres

Page 2: Когда возможностей Active record недостаточно

О чем пойдет речь

Почему возможностей AR может не хватать

Основные отличия Sequel

Составные типы данных (hstore, jsonb, array)

Использование специальных функций Postgres и

примеры

Page 3: Когда возможностей Active record недостаточно

Есть же AR! У нас все отлично работает!

Слабая поддержка:

Составных типов данных

CTE

Join, group

Распределенных БД

Подзапросов

Master Slave

Операций на стороне бд

Sequel

Sequel это [паттерн] ActiveRecord

Во многом похож на AR

Превосходит AR по функциональности

Поддерживает большее число СУБД

Поддерживает ree 1.8, jruby

Имеет модульную архитектуру

Поддерживает то, что написано слева

Page 4: Когда возможностей Active record недостаточно

Где работает плохо и почему

Model.select(“COALESCE(ABS(SUM(amount * (courses -> users.currency)::numeric)), 0)”)

SELECT COALESCE(ABS(SUM(amount * (courses -> users.currency)::numeric)), 0) FROM "models"

Model.select { coalesce(abs(sum(:amount * :courses.hstore[:users__currency].cast(:numeric))), 0) }

SELECT coalesce(abs(sum(("amount" * CAST(("courses" -> "users"."currency") AS numeric)))), 0) FROM "models"

Page 5: Когда возможностей Active record недостаточно

ActiveRecord vs Sequel (функциональность)

1.pg_loose_count2.pg_static_cache_updater 3.single_table_inheritance4.timestamps5.association_proxies6.optimistic_locking

Feature AR Sequel

Where OR 2016 2008

Left joins 2015 2008

Concurrently indexing 2013(5) 2012

Bulk loading 2010 2009

where.not 2012 2007

rewhere 2013 2008

Page 6: Когда возможностей Active record недостаточно

Arelusers = Arel::Table.new(:users) # Users.arel_tableorders = Arel::Table.new(:orders)

User.find_by_sql(users.project(Arel.star).join(orders, Arel::Nodes::OuterJoin).on(users[:id].eq(orders[:user_id])))

User.left_join(:orders, user_id: :id).all

User.find_by_sql(users.project(Arel.star).where( orders.where(users[:id].eq(orders[:user_id])).exists.not ))

User.exclude { exists(DB[:orders].where(user_id: users__id)) }.all

Page 7: Когда возможностей Active record недостаточно

Arel сложные условия и оконные функцииusers.project(Arel.star).where(users[:id].eq(23).or(users[:status_id].gteq(42))).to_sql

User.where(id: 23).or(User.where("status_id >= ?", 42)).to_sql

User.where { { id: 23 } | (status_id >= 42) }.sql

SELECT * FROM "users" WHERE (("id" = 23) OR ("status_id" >= 42))

users.project([users[:id].sum.over(Arel::Nodes::Window.new.partition(users[:status_id])), users[:id], users[:status_id]]).to_sql

User.select{ [sum(id).over(partition: :users__status_id), status_id] }.sql

SELECT sum("id") OVER (PARTITION BY "users"."status_id"), "status_id" FROM "users"

Page 8: Когда возможностей Active record недостаточно

Sequel::Model & Sequel::Datasetclass Model < Sequel::Model(:models) many_to_one :category

subset(:expensive){ price > 500000 }

dataset_module do def by_category(category_id) where(category_id: category_id) end endEnd

Model.by_category(1).allModel.expensive.all

Model.qualify .join(:categories, id: :category_id) .select_append( :categories__title___cat_title ) .group(:models__id) .group_append(:categories__id)

SELECT "models".*, "categories"."title" AS "cat_title"FROM "models"INNER JOIN "categories" ON ("categories"."id" = "models"."category_id")GROUP BY "models"."id", "categories"."id"

Page 9: Когда возможностей Active record недостаточно

Массивы

Model.where{ :values.pg_array.contains([4, 5]) }SELECT * FROM "models" WHERE ("values" @> ARRAY[4,5])

Model.select{ [:array1.pg_array.concat(array2).overlaps(:array3).as(:result), :id] }.allSELECT (("array1" || "array2") && "array3") AS "result", "id" FROM "models"

ia.contains(:other_int_array_column) # @> ia.contained_by(:other_int_array_column) # <@ia.overlaps(:other_int_array_column) # &&ia.concat(:other_int_array_column) # ||

ia.push(1) # int_array_column || 1ia.unshift(1) # 1 || int_array_column

Page 10: Когда возможностей Active record недостаточно

HstoreModel.select{ :properties.hstore[:enabled] }SELECT ("properties" -> "enabled") FROM "models"

Model.select{ :properties.hstore.key?(:enabled) }SELECT ("properties" ? "enabled") FROM "models"

Model.where( :properties.hstore.key?(:enabled))SELECT * FROM "models" WHERE ("properties" ? "enabled")

DB[:tab].update(:h=>Sequel.hstore_op(:h).concat('c'=>3))UPDATE "tab" SET "h" = ("h" || '"c"=>"3"'::hstore)

Page 11: Когда возможностей Active record недостаточно

JSON(B) вложенные структуры

DB[:jsonb].insert(jsonb: '{"array":[1], "object": {}}' )DB[:jsonb].where( :prefs.pg_jsonb.contains('{"object":{}}') & { :prefs.pg_jsonb[['array', '0']] => '1' } ).allSELECT * FROM "jsonb" WHERE (("prefs" @> '{"object":{}}') AND (("prefs" #> ARRAY['array','0']) = '1'))

DB[:jsonb].update(prefs: :prefs.pg_jsonb.set(['array', '1'], '5'))UPDATE "jsonb" SET "prefs" = jsonb_set("prefs", ARRAY['array','1'], '5', true)

Page 12: Когда возможностей Active record недостаточно

Использование вложенных типов данныхDB.create_table(:address, temp: true) { String :street; String :city; }CREATE TEMPORARY TABLE "address" ("street" text, "city" text)

DB.create_table(:test, temp: true) { primary_key :id; address :address; }CREATE TEMPORARY TABLE "test" ("id" serial PRIMARY KEY, "address" address)

DB[:test].insert(:address=>DB.row_type(:address, street: '123 Sesame St.', city: 'Some City'))INSERT INTO "test" ("address") VALUES (ROW('123 Sesame St.', 'Some City')::"address") RETURNING "id"

DB[:test].first[:address]=> {:street=>"123 Sesame St.", :city=>"Some City"}

Page 13: Когда возможностей Active record недостаточно

Полнотекстовый поискDB.alter_table(:texts) {add_full_text_index([:text], language: 'english')CREATE INDEX "texts_text_index" ON "texts" USING gin (to_tsvector('english'::regconfig, (COALESCE("text", ''))))

DB[:texts].full_text_search(:text, %w[match tes:*], language: 'english')SELECT * FROM "texts" WHERE (to_tsvector(CAST('english' AS regconfig), (COALESCE("text", ''))) @@ to_tsquery(CAST('english' AS regconfig), 'match | tes:*'))[{:id=>1, :text=>"test test"}, {:id=>3, :text=>"exact match"}]

Page 14: Когда возможностей Active record недостаточно

CTEПеремещение данных в другую таблицу WITH deleted_items AS ( DELETE FROM #{table} WHERE #{where} RETURNING * ) INSERT INTO #{table}_archive SELECT * FROM deleted_items

admins = User.where(role: 'admin')User.with(:admins, admins).from(:admins).where(login: /a/)

WITH "admins" AS (SELECT * FROM "users" WHERE ("role" = 'admin')) SELECT * FROM "admins" WHERE ("login" ~ 'a')

DB[:"#{table}_archive"].with(:deleted_items, DB[table].returning.with_sql(:delete_sql)).insert(DB[:deleted_items])

WITH "d" AS (DELETE FROM "users" RETURNING *) INSERT INTO "users_archive" SELECT * FROM "d" RETURNING NULL

Page 15: Когда возможностей Active record недостаточно

Recursive CTEDB[:t].with_recursive(:t, DB.select(Sequel.lit('1').as(:val)), DB[:t].select(:t__val + 1).where(Sequel.expr(:t__val) < 10), :args=>[:val]).all

WITH recursive "t"("val") AS ( SELECT 1 AS "val" UNION ALL ( SELECT ("t"."val" + 1) FROM "t" WHERE ( "t"."val" < 10 ) )) SELECT * FROM "t"

Page 16: Когда возможностей Active record недостаточно

Полезные RCTE Инициализацияdb[table].where(id: [db[table].min(key), db[table].max(key)]).select( sequel_function(:min, key), sequel_function(:max, key), as(:target.cast(pg_type), :target_col), as(:target.cast(pg_type), :value_col)).group(:target_col)

Тело рекурсииdb[:t].join(table, Sequel.qualify(table, :id)=> mid) .where(expr(:max) >= :min) .select( as(Sequel.case( { (:t__target_col >= Sequel.qualify(table, column)) => mid }, :min), :min), as(Sequel.case( { (:t__target_col < Sequel.qualify(table, column)) => mid }, :max), :max), :target_col, as(Sequel.qualify(table, column), :value_col) )

def mid (:min + :max) / 2end

Page 17: Когда возможностей Active record недостаточно

RCTE binary search (Окончание)def find_nearest(table, column, target, options={}) key = options.fetch(:key, :id) pg_type = options.fetch(:pg_type, :text) db.dataset.with_recursive(:t, initial_query(table, key, target, pg_type), recursive_query(table, column), union_all: false ).from(:t)end

Page 18: Когда возможностей Active record недостаточно

Деревья

Node.first.descendants

WITH RECURSIVE "t" AS (SELECT * FROM "tree" WHERE ("parent_id" = 1) UNION ALL (SELECT "tree".* FROM "tree" INNER JOIN "t" ON ("t"."id" = "tree"."parent_id"))) SELECT * FROM "t" AS "tree"

=> [#<Node @values={:id=>2, :parent_id=>1}>, #<Node @values={:id=>4, :parent_id=>1}>, #<Node @values={:id=>3, :parent_id=>2}>, #<Node @values={:id=>5, :parent_id=>4}>]

Page 19: Когда возможностей Active record недостаточно

Insert conflict & rollupDB[:texts].insert_conflict(:target=>:id, :update=>{:text=>:excluded__text}).insert(id: 2, text: 'exact match updated')

INSERT INTO "texts" ("id", "text") VALUES (2, 'exact match updated') ON CONFLICT ("id") DO UPDATE SET "text" = "excluded"."text" RETURNING "id"

User.select { [role, sum(balance).cast(:integer)] }.group(:role).group_rollup.all

SELECT "role", CAST(sum("balance") AS integer) FROM "users" GROUP BY ROLLUP("role")

=> [#<User @values={:role=>"admin", :sum=>210933700}>, #<User @values={:role=>"client", :sum=>344305}>, #<User @values={:role=>nil, :sum=>211278005}>]

Page 20: Когда возможностей Active record недостаточно

DO statements & copyDB.do(:language=>:plpgsql) <<-SQL BEGIN raise 'exception'; END;SQL

DB.create_function(:set_updated_at, “ BEGIN NEW.updated_at := CURRENT_TIMESTAMP; RETURN NEW;END;", :language=>:plpgsql, :returns=>:trigger)

DB.copy_table(DB[:jsonb], :format=>:csv)

COPY (SELECT * FROM "jsonb") TO STDOUT (FORMAT csv)

=> 1,"{""array"": [7, ""4""], ""object"": {}}"2,"{""array"": [7, ""4""], ""object"": {}}"

Page 21: Когда возможностей Active record недостаточно

Шардинг и master/slaveDB=Sequel.connect('postgres://master_server/database', :servers=>{:read_only=>{:host=>'slave_server'}})

# Force the SELECT to run on the masterDB[:users].server(:default).all

# Force the SELECT to run on the read-only slaveDB[:users].server(:read_only).all

DB.transaction(:prepare=>'some_transaction_id_string') do DB[:foo].insert(1) # INSERT end

DB.commit_prepared_transaction('some_transaction_id_string') # or DB.rollback_prepared_transaction('some_transaction_id_string')

Page 22: Когда возможностей Active record недостаточно

Итоги

Вопросы? Q&A!

Ссылка о производительности