在Rails中使用SQL

首先,本文不是关于我如何喜欢铁轨,其次,本文不是关于我如何讨厌铁轨。 可以用完全不同的方式来对待它们,并且只有更改它们才能变得更好。 而且只有开始改变,它们才会变得更糟。 好吧,总的来说,我警告过你,但你理解我。


ActiveRecord的主要概念之一是数据库非常实用,可以更改。 好吧,您坐在那里,使用MySQL编写模型,突然间您读到可以代替MySQL并用MongoDB代替MySQL的地方。 好吧,并不是那么激进,但是,例如,在PostgreSQL上,您可能有理由替换MySQL。 反之亦然,我对MySQL一无所知。 在这里ActiveRecord声称可以使您(在范围内)更容易,在过滤器和关联足够抽象之前/之后,不必担心生成数据库查询和处理应用程序逻辑。 可以不用写WHERE is_archived = TRUE而是编写where(is_archived: true)而ActiveRecord将为您做所有事情。 所有示例都将针对PostgreSQL而不是MySQL进行翻译,因此MySQL用户将必须发明自己的自行车。



但是无论如何! 实际上,事实证明,这一抽象层完全充满了漏洞,就像“金鱼”故事中的低谷一样。 而且许多基本功能无法使用,例如比较日期或使用数组。 并where("#{quoted_table_name}.finished_at >= ?", Date.current)where("#{quoted_table_name}.other_ids <@ ARRAY[?]", ids)强制作用where("#{quoted_table_name}.finished_at >= ?", Date.current)作用域。 ActiveRecord会给它一个完全有意识和合乎逻辑的答案:不要使用它。 使用habtm连接代替数组,如果需要比较日期,请使用它。 是的,上帝禁止您在这种范围内错过quoted_table_name第一个includesjoins所有内容放到原处。 到处都容易写,并且总是写,以免打倒你的手。


好吧,当然,一旦您决定在此处干扰ActiveRecord,就不会再回头了。 并不是没有机会,甚至是向无忧无虑地过渡到另一个数据库的虚幻希望。 打印和刻录此源代码会容易得多。 当然,没有其他理由不在您的应用程序中使用额外的数据库功能。 在健康上使用并强迫他人!


而且事实证明,您在models文件夹中的作用域包含了这些额外功能的一半以上,很明显,ActiveRecord只是将一个带有标签的代码与另一个代码集成在一起的便捷外壳。 范围,例如where(is_archived: true).joins(:sprint).merge(Sprint.archived) ,可以很好地工作,将它们组合起来不会比打乱鸡蛋更困难,对吗?



下一阶段将是非规范化。 不,非规范化总是不会消失,但是对它的关心落在了Rails和ActiveRecord的强大肩膀上,而且您知道这两个家伙在资源需求上的敏捷性和禁欲主义并没有不同。 假设counter_cache: true实现非规范化的第一步是counter_cache: true ,因为COUNT(*) AS sprints_count不允许您创建ActiveRecord(您不想更改select() ,对吧?)。 而且counter_cache并非完美无缺,在某些情况下,实际数量可能与所缓存的数量不同步。 当然不挑剔,但令人不愉快。 而且这只是第一个进入数据库且不加载已加载的红宝石机器磁头的候选对象。 只需几个触发器,您就完成了! 首先,在删除并向A板添加新记录时,您需要计算B板中的记录数,对吧? 好吧,当然,在编辑时,如果foreign_key更改,因为UPDATE B SET a_id = $1 WHERE id = $2会破坏旧A和新A的counter_cache。


  CREATE OR REPLACE FUNCTION update_#{parent_table}_#{child_table}_counter_on_insert() RETURNS TRIGGER AS $$ BEGIN UPDATE #{parent_table} SET #{counter_column} = COALESCE((SELECT COUNT(id) FROM #{child_table} GROUP BY #{foreign_column} HAVING #{foreign_column} = NEW.#{foreign_column}), 0) WHERE (#{parent_table}.id = NEW.#{foreign_column}); RETURN NULL; END; $$ LANGUAGE plpgsql; 

下一个数据库路径将与日期时间相关。 对于初学者来说,让我们只为数据库中的created_atupdated_at字段提供服务,幸运的是,这要简单得多。 首先设置默认值:


  change_column_default :table_name, :created_at, -> { 'CURRENT_TIMESTAMP' } change_column_default :table_name, :updated_at, -> { 'CURRENT_TIMESTAMP' } 

为了立即在任何地方执行此操作,您可以为这些字段所在的所有板块组织一个循环。 当然,除了schema_migrationsar_internal_metadata之外:


  (tables - %w(schema_migrations ar_internal_metadata)).each { ... } 

就是这样,现在这些表的默认值正是我们需要的值。 现在是时候确保滑轨不会碰到这些区域了。 在正确的位置用两个螺栓完成此操作。 是的,可以选择设置负责此工作的框架:


 Rails.application.config.active_record.record_timestamps = false 

因此,下一步是在记录更新时更新updated_at字段。 很简单:


  CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_update() RETURNS TRIGGER AS $$ BEGIN SELECT CURRENT_TIMESTAMP INTO NEW.updated_at; RETURN NEW; END; $$ LANGUAGE plpgsql; 

现在,您需要完全摆脱touch: true在模型中为touch: true 。 这东西与破折号中的目标非常相似-也完全有孔。 我什至不解释原因,因为您已经知道所有这些情况。 这并不复杂,您所需要做的就是不仅更新自己的updated_at:


  CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_update() RETURNS TRIGGER AS $$ BEGIN UPDATE foreign_table_name SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.foreign_column_name; SELECT CURRENT_TIMESTAMP INTO NEW.updated_at; RETURN NEW; END; $$ LANGUAGE plpgsql; 

当然,此类触发器的调用链将执行不必要的操作,但是在理智的机制中,在不更改记录本身的情况下调用触发器。 您可以尝试执行SET title = title ,但结果并不比SET updated_at = CURRENT_TIMESTAMP


插入上将完全相同的触发器,只需要更新updated_at


  CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_insert() RETURNS TRIGGER AS $$ BEGIN UPDATE foreign_table_name SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.foreign_column_name; RETURN NEW; END; $$ LANGUAGE plpgsql; 

当然,您可以尝试使用一个函数编写该函数,并在触发器内部添加对当前事件的检查,类似于IF TG_OP = 'UPDATE' THEN ,但最好使所有触发器尽可能简单以减少发生错误的可能性。


您可能希望以某种方式使此类触发器的生成自动化,然后可能需要查找当前表与其余表之间的所有外部关系。 您可以使用以下查询轻松完成此操作:


  SELECT ccu.table_name AS foreign_table_name, kcu.column_name AS column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '#{table_name}' ORDER BY ccu.table_name; 

另一个非常有用的提示。 以相同的方式调用触发器,以能够验证一个请求所需要的内容是否存在,例如,此请求将找到所有触摸插入触发器:


  SELECT routine_name AS name FROM information_schema.routines WHERE routine_name LIKE 'touch_for_%_on_insert' AND routine_type ='FUNCTION' AND specific_schema='public'; 

最后,最坏的情况仍然存在。 事实是,rails并不是为至少某种智能数据库而设计的,并且它们实际上并不在乎数据库中除id字段以外的其他内容可能会更改,并且只有在插入时才更改。 因此,没有健全的机制来添加RETURNING updated_at以更新请求。



事实证明Mankipatch并不是很整洁,但首先的目标是尽可能少地破坏框架的当前工作。


我会把他全部带走
 module ActiveRecord module Persistence # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L729-L741 def _create_record(attribute_names = self.attribute_names) attribute_names &= self.class.column_names attributes_values = attributes_with_values_for_create(attribute_names) an_id, *affected_rows = self.class._insert_record(attributes_values).dup self.id ||= an_id if self.class.primary_key Hash[ApplicationRecord.custom_returning_columns(self.class.quoted_table_name, :create).take(affected_rows.size).zip(affected_rows)].each do |column_name, value| public_send("#{column_name}=", self.class.attribute_types[column_name.to_s].deserialize(value)) if value end @new_record = false yield(self) if block_given? id end private :_create_record # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L710-L725 def _update_record(attribute_names = self.attribute_names) attribute_names &= self.class.column_names attribute_names = attributes_for_update(attribute_names) if attribute_names.empty? affected_rows = [] @_trigger_update_callback = true else affected_rows = _update_row(attribute_names) @_trigger_update_callback = affected_rows.any? end Hash[ApplicationRecord.custom_returning_columns(self.class.quoted_table_name, :update).take(affected_rows.size).zip(affected_rows)].each do |column_name, value| public_send("#{column_name}=", self.class.attribute_types[column_name.to_s].deserialize(value)) end yield(self) if block_given? affected_rows.none? ? 0 : 1 end private :_update_record end module ConnectionAdapters module PostgreSQL module DatabaseStatements # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L93-L96 def exec_update(sql, name = nil, binds = []) execute_and_clear(sql_with_returning(sql), name, binds) { |result| Array.wrap(result.values.first) } end # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L147-L152 def insert(arel, name = nil, pk = nil, _id_value = nil, sequence_name = nil, binds = []) sql, binds = to_sql_and_binds(arel, binds) exec_insert(sql, name, binds, pk, sequence_name).rows.first end alias create insert # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L98-L111 def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: table_ref = extract_table_ref_from_insert_sql(sql) if pk.nil? # Extract the table from the insert sql. Yuck. pk = primary_key(table_ref) if table_ref end returning_columns = quote_returning_column_names(table_ref, pk, :create) if returning_columns.any? sql = "#{sql} RETURNING #{returning_columns.join(', ')}" end super end # No source in original repo def quote_returning_column_names(table_ref, pk, action) returning_columns = [] returning_columns << pk if suppress_composite_primary_key(pk) returning_columns += ApplicationRecord.custom_returning_columns(table_ref, action) returning_columns.map { |column| quote_column_name(column) } end # No source in original repo def sql_with_returning(sql) table_ref = extract_table_ref_from_update_sql(sql) returning_columns = quote_returning_column_names(table_ref, nil, :update) return sql if returning_columns.blank? "#{sql} RETURNING #{returning_columns.join(', ')}" end # No source in original repo def extract_table_ref_from_update_sql(sql) sql[/update\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*set/im] Regexp.last_match(1)&.strip end end end end end 

最重要的是,这里我们转到ApplicationRecord.custom_returning_columns来找出ID以外的哪些列对我们很有趣。 这个方法看起来像这样:


  class << self def custom_returning_columns(table_ref, action) return [] if ['"schema_migrations"', '"ar_internal_metadata"'].include?(table_ref) res = [] res << :created_at if action == :create res << :updated_at res += case table_ref when '"user_applications"' [:api_token] when '"users"' [:session_salt, :password_changed_at] # ... else [] end res end end 



不用说结论,我们可以说,Rails的头疼已经变得有点不那么疼了。 诸如counter_cachetouch类的counter_cache过程将被遗忘,在下一篇文章中,我们可以想到一些更全局的事物,例如删除悬挂空间,数据验证,级联数据删除或偏执删除。 当然,如果您喜欢这篇文章。

Source: https://habr.com/ru/post/zh-CN421939/


All Articles