Learn Rails5.2- ActiveRecord: sqlite3的用法, Query查询语法。乐观锁和悲观锁案例,查询语法includes(), 多态关联,destory和delete, Scope, Validats, Migrations

 

rails generate model photo title:string album:references

这会产生一个album_id列,当建立belongs_to关联时,需要用到。

references算时一种结构,会产生integer.

 

For integer, string, text and binary fields, an integer in curly braces will be set as the limit:  `rails generate model user pseudo:string{30}`
For decimal, two integers separated by a comma in curly braces will be used for precision and scale: rails generate model product 'price:decimal{10,2}'
You can add a `:uniq` or `:index` suffix for unique  
        `rails generate model user pseudo:string:uniq`
        `rails generate model user pseudo:string:index`
You can combine any single curly brace option with the index options:
        `rails generate model user username:string{30}:uniq`

 

Database Configuration 

config/database.yml中可以看到默认的数据库配置。

 

development:
  <<: *default
  database: db/development.sqlite3

 

使用sqlite3+路径, 进入控制台。

$ sqlite3 db/development.sqlite3
SQLite version 3.14.0 2016-07-26 15:17:14
Enter ".help" for usage hints.
sqlite> .tables #显示所有表格
ar_internal_metadata  products
countries             schema_migrations
sqlite> .schema countries #打印创建表格的命令。
CREATE TABLE "countries" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "population" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);

 


 

在rails console建立一条记录,可以通过底层数据库sqlite3查看: 

$ sqlite3 db/development.sqlite3

> select * from countries;

1|Cermany|81831000|2018-06-15 07:39:00.370425|2018-06-15 

表示成功。

 


 

一次创建多条数据需要使用一个数组的hashes。

Country.create([{name: "China"}, {name:"France"}]) 

 

Country.all返回得到一个数组的Country。它是一个ActiveRecord::Relation scope object

因此,Country.all可以使用each方法。

 


 

rails db:drop 删除数据库。rails db:create, rails db:migrate三剑客。

rails db:seed建立种子文件。

 


 

find , where 

 

使用查询方法的使用,如果是多条,必然返回一个Array。

Country.find(1)返回的是一个单独的object,他的类是Country

Country.find([1])返回的则是一个集合[],他的类是 Array

 

这里用到了between..and.. 

> Album.where(release_year:1960..1966).count
   (0.2ms)  SELECT COUNT(*) FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ?  [["release_year", 1960], ["release_year", 1966]]
=> 5

 

这里使用了and连接了2个between..and.. 

> Album.where(release_year:1960..1966,  id: 1..5).count
   (0.2ms)  SELECT COUNT(*) FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ? AND "albums"."id" BETWEEN ? AND ?  [["release_year", 1960], ["release_year", 1966], ["id", 1], ["id", 5]]
=> 4

 

这里使用了in, 用于精确的指定查询条件

> Album.where(release_year:[1966, 1968]).count
   (0.1ms)  SELECT COUNT(*) FROM "albums" WHERE "albums"."release_year" IN (?, ?)  [["release_year", 1966], ["release_year", 1968]]
=> 4

 

这里order by 和 ASC, limit

> Album.where(release_year:[1966, 1968]).first   
  Album Load (0.2ms)  SELECT  "albums".* FROM "albums" WHERE "albums"."release_year" IN (?, ?) ORDER BY "albums"."id" ASC LIMIT ?  [["release_year", 1966], ["release_year", 1968], ["LIMIT", 1]]
=> #<Album:0x00007ffd8cb9b388...>

 

所有查询语法,条件都可以使用?, (?, ?)代替,在语法最后使用Array,按顺序列出具体条件。  

 


 

not ,or 

 

这里使用了!= 

> Album.where.not(release_year: 1968).count
   (0.1ms)  SELECT COUNT(*) FROM "albums" WHERE "albums"."release_year" != ?  [["release_year", 1968]]
=> 9

 

这里使用了数据库的or,另外rails语法,or()方法连接的应该是同一个数据库

> Album.where(release_year:1967).or(Album.where(name: "The Beathles")).count
   (0.2ms)  SELECT COUNT(*) FROM "albums" WHERE ("albums"."release_year" = ? OR "albums"."name" = ?)  [["release_year", 1967], ["name", "The Beathles"]]

 

⚠️和 Album.where(release_year:1960..1966, id: 1..5).count   的区别

 

 


 

自定义SQL,使用find_by_sql()方法。

 


 

模糊查找like, >= , <=

 

> Album.where("name like ?", "%on%").count

 

   (0.1ms)  SELECT COUNT(*) FROM "albums" WHERE (name like '%on%')
=> 5

 

> Album.where('release_year > ?', 1964).count
   (0.2ms)  SELECT COUNT(*) FROM "albums" WHERE (release_year > 1964)
=> 10

 


 

使用了AND 

> Album.where("name like ? AND release_year > ?", '%on%', 1970).count
   (0.1ms)  SELECT COUNT(*) FROM "albums" WHERE (name like '%on%' AND release_year > 1970)
=> 3

 

使用了local variable在语法内部用了#{}插入符号 

> Album.where("name like ?", "%#{search_string}%").count
   (0.2ms)  SELECT COUNT(*) FROM "albums" WHERE (name like '%ing%')
=> 2

 

 


 

 链式查询:limit

 

这里用到limit方法限制查询记录数量。 

Ablum.where(release_year: 1965..1968).order(:release_year).limit(3)

 =>SELECT  "albums".* FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ? ORDER BY "albums"."release_year" ASC LIMIT ?  [["release_year", 1965], ["release_year", 1968], ["LIMIT", 3]]

 


 

自动的优化查询:

 

因为order对sum来说是无关紧要的查询条件。所以SQL没有使用order查询。

> Album.where(release_year: 1970..1979).order(:name).sum(:release_year)
 =>  (42.5ms)  SELECT SUM("albums"."release_year") FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ?  [["release_year", 1970], ["release_year", 1979]]
=> 5922

 


 

reverse_order和 order

 

reverse_order反转查询顺序。

 Album.where(release_year: 1960..1969).order(:name).reverse_order

 


 

Pluck()方法 

从检索的记录中挑出需要的字段。

这里返回一个Array,包含了所以符合查询条件的记录的名字。

可以挑出多个字段,返回嵌套数组。

 

> Album.where(release_year: 1960..1969).pluck(:name)
    SELECT "albums"."name" FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ?  [["release_year", 1960], ["release_year", 1969]]
=> ["Sgt. Pepper's Lonely Hearts Club Band",
 "Pet Sounds",
 "Revolver",
 "Highway 61 Revisited",
 "Rubber Soul",
 "Blonde on Blonde",
 "The Beatles"]

 

⚠️Album.pluck(:id)等同于Album.ids 

 

 


 

Select()方法 类似pluck()

 

返回的是一个ActiveRecord::Relation。 

> Album.where(release_year: 1960..1969).select(:name)
   SELECT "albums"."name" FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ?  [["release_year", 1960], ["release_year", 1969]]
=> [#<Album:0x00007ffd8bb808e0 id: nil, name: "Sgt. Pepper's Lonely Hearts Club Band">,
 #<Album:0x00007ffd8bb805c0 id: nil, name: "Pet Sounds">,
 #<Album:0x00007ffd8bb80480 id: nil, name: "Revolver">,
 #<Album:0x00007ffd8bb80340 id: nil, name: "Highway 61 Revisited">,
 #<Album:0x00007ffd8bb80188 id: nil, name: "Rubber Soul">,
 #<Album:0x00007ffd8bb7bed0 id: nil, name: "Blonde on Blonde">,
 #<Album:0x00007ffd8bb7bbb0 id: nil, name: "The Beatles">]

  


 

Calculations

 

average()方法。根据检索的条件,计算平均值,返回BigDecimal.

 

 > Album.where(release_year: 1960..1969).average(:release_year)
    SELECT AVG("albums"."release_year") FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ?  [["release_year", 1960], ["release_year", 1969]]
=> 0.196614285714286e4

 

maximum(), minimum(), sum(),用法一样。

> Album.maximum(:release_year)
   (0.2ms)  SELECT MAX("albums"."release_year") FROM "albums"
=> 1979

 


 

SQL EXPLAIN

 

大型的数据库,EXPLAIN是一个很好的debugging 方法。可以显示详细的信息。 

> Album.where(release_year: 1960..1969).explain
  SELECT "albums".* FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ?  [["release_year", 1960], ["release_year", 1969]]
=> EXPLAIN for: SELECT "albums".* FROM "albums" WHERE "albums"."release_year" BETWEEN ? AND ? [["release_year", 1960], ["release_year", 1969]]
2|0|0|SCAN TABLE albums

 

批处理:batches

find_each,

find_in_batches: yields batches to 块,作为一个模型的数组。

见其他博客,或guide。 

 

 


 

lock_version 乐观锁optimistic

 

备注:之前看rails的guide以及API, 因为是英文同时没有step to step的案例,一直没有弄明白。因此,如果某个特点在一篇教材上不懂,标记下来,可以通过查找不同的资料,如教材。一个step to step的案例是最方便理解知识点的地方了。

 

通过乐观锁可以锁定一个数据库的数据。

如果有多个用户同时修改同一条记录,就可能会产生冲突。 加上🔒,一旦冲突就会报告❌,同时更新会被忽视掉。

 

激活乐观🔒,一个model需要一个属性,名字是lock_version,类型是integer.

 

rails g model Car name "price:decimal{8,2}" lock_version:integer 

生产:

class CreateCars < ActiveRecord::Migration[5.2]
  def change
    create_table :cars do |t|
      t.string :name
      t.decimal :price, precision: 8, scale: 2
      t.integer :lock_version
      t.timestamps
    end
  end
end

 

如果建立了2个实例变量a,  b都指向同一条记录,如果在a上进行了属性修改并save。那么b就不能再用于修改记录了,也不能删除记录,  b会过期,会报告❌,然后忽略掉b的操作。

ActiveRecord::StaleObjectError: Attempted to update a stale object: Car. 

之后,你需要处理这个冲突,进行营救并回滚,合并或者其他方法来解决这个冲突。

如果要关掉🔒:

class Car < ApplicationRecord
  self.locking_column = :lock_car
end

 


 

 Pessimistic lock 悲观🔒

和乐观锁锁定整个数据库不同,悲观锁用于锁定单个的记录。

> Car.lock.find(1)

为了防止死锁,一般在事物中使用,transaction do..end 

  


 

joins()方法,连接两个有关联的表。1对n

 

> Category.joins(:products).where(products:{name:"Strawberry"})
   SELECT "categories".* FROM "categories" INNER JOIN "products" ON "products"."category_id" = "categories"."id" WHERE "products"."name" = ?  [["name", "Strawberry"]]

 =》一个数组集合。

 

反过来也可以,⚠️需要主要的是这里category是单数,因为是一对多的关系。

 

> Product.joins(:category).where(categories:{id:1})
   SELECT "products".* FROM "products" INNER JOIN "categories" ON "categories"."id" = "products"."category_id" WHERE "categories"."id" = ?  [["id", 1]]

 

joins(),也叫内连接是返回所有关联的记录。如果一个目录和一个产品关联,那么就是一条记录。

 


left_joins() 

Category.left_joins(:products) 

左连接。返回所有内连接的记录,也返回没有建立连接的category。

 

 

 


 

includes()方法,已经理解。

 

Category.includes(:products)会使用2条查询语法,一个是查询所有的categories,另一个是根据category_id,查询products。

返回的是Category中的对象集合。同时,所有的products信息被储存到缓存中。

这样通过对象集合中的对象,比如对象fruit.products,就可以从缓存中拿出相关的记录。

 

这样就无需再调用数据库查询了,更快一点。但是会占用更多的缓存资源。 


> Category.includes(:products).where(:products => {name: "Strawberry"})
   SELECT ..一大坨... FROM "categories" LEFT OUTER JOIN "products" ON "products"."category_id" = "categories"."id" WHERE "products"."name" = ?  [["name", "Strawberry"]]


区别:joins和includes,left_joins

 

joins(),也叫内连接是返回所有关联的记录。如果一个目录和一个产品关联,那么就是一条记录。

> Category.joins(:products).size

  SELECT COUNT(*) FROM "categories" INNER JOIN "products" ON "products"."category_id" = "categories"."id"

=> 11

 

因为有一个Category的记录,name是"bad food",没有关联任何products,所以左连接会返回12条记录。

> Category.left_joins(:products).size

   (0.2ms)  SELECT COUNT(*) FROM "categories" LEFT OUTER JOIN "products" ON "products"."category_id" = "categories"."id"
=> 12

 

includes,返回的数组集合中包含4个对象记录,每个记录都可能关联着products的记录。 如果你知道之后会用到所有的product的数据。使用includes()就得到了。之后需要什么,直接会从缓存里取。无需再进行数据库的互动了。

> Category.includes(:products).size
     SELECT COUNT(*) FROM "categories"
=> 4

 

 


 

destroy方法 

ActiveRecord::Persistence

destroy(),在数据库中删除数据,并冻结缓存中的这个实例。 只能读,不能改。

可以使用frozen? 方法来判断是否是已经删除的 。

可以新建一个实例对象,把删除的对象的属性的值赋予新的对象,然后再保存。 变相回复数据。


如果Book和Author2个表建立了1对n关联,设置has_many :author, dependent: :destroy 

book.destroy, 会从数据库删除关联的authors

但book.delete,不会从数据库删除关联的authors 

book.authors.delete(2)可以从数据库删除对应的author

 

在has_many :author不加上dependent: :destroy的情况下:

book.authors.delete(2)只会把author的外键book_id:nil掉。

book.authors.destroy(2)则可以从数据库删除author。

 

book.destroy和book.delete,都会从数据库删除book, 不会删除相关的authors ,也不会改变它的外键。

 

 


 

belongs_to中最重要的选项touch:true 

 

用于俄罗斯套娃缓存cache。它会同步更新关联的记录的updated_at属性到当前时间。

 


 

 

has_many(name, scope = nil, **options, &extension)

 

scope作为第二个参数。可以取回记录,或者当你存取关联连接时客制化generated query

has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }

 

 

extension参数是一个block, 可以增加新的查询finders, creators,其他方法作为关联的一部分。

has_many :employees do
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

 


 

 

多对多关联 

 


多态关联: Polymorphic Associations

 

一种更方便的关联结构。一种使用技巧。tricky.

用途:car和bike是两个不同类型,它们都有颜色标签这个属性。 此时可以增加2个表CarTag, BikeTag。但更方便的是只使用Tag,让Tag和Car, Bike都建立关联。

 

方法: 给Tag增加2个field,一个string field用于记录关联表的名字/类名, 一个integer field用于记录关联表的记录的id号。 并给这两个字段附上索引。

$ rails generate model Car name
$rails generate model Bike name 
$ rails generate model Tag name taggable:references{polymorphic}

建立tags表格:

会生成taggable_index, taggable_index两个字段:

同时,这2个field会添加上index索引。 

create_table "tags", force: :cascade do |t|

t.string "name"

t.string "taggable_type"

t.integer "taggable_id"

t.datetime "created_at", null: false

t.datetime "updated_at", null: false

t.index ["taggable_type", "taggable_id"], name: "index_tags_on_taggable_type_and_taggable_id"

end

建立关联:

class Tag < ApplicationRecord

belongs_to :taggable, polymorphic: true

end 

解释:

不同于传统的关联,这里taggable指向的是Tag内部的taggable_type字段。

因为这个字段储存了关联表的名字/类名。

本案例相当于belongs_to :car和belongs_to :bike


class Bike < ApplicationRecord

has_many :tags, as: :taggable

end  

解释: as用来指定一个多态接口interface
class Car < ApplicationRecord  
  has_many :tags, as: :taggable 
end	

 >  beetle = Car.create(name: 'Beetle')

 >  beetle.tags.create(name: "blue")

 INSERT INTO "tags" ("name", "taggable_type", "taggable_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "blue"], ["taggable_type", "Car"], ["taggable_id", 1],...  

 > beetle.tags.create(name: 'Automatic')

 INSERT INTO "tags" ("name", "taggable_type", "taggable_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "Automatic"], ["taggable_type", "Car"], ["taggable_id", 1],...

 解释:

给beetle对象增加了2个tags,可以看到taggable_type和taggable_id能准确定位到beetle对象。

 

> Tag.last.taggable
   SELECT  "tags".* FROM "tags" ORDER BY "tags"."id" DESC LIMIT ?  [["LIMIT", 1]]
   SELECT  "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<Car:0x00007ffe6e95e9e8
 id: 1,
 name: "Beetle",...]

 

 


 

 

Scope

在model层写scope,就是便捷的查询语法, 本质是类方法。

⚠️在rails console, 增改一个类的类方法后,需要从新进入控制台才能生效。

 

scope可以带参数;使用有名字的scope可以便捷的建立数据。

class Product < ApplicationRecord

  scope :available, -> {where(in_stock: true)}

end 

 觉得比较诡异的使用方法,新建时会自动带上available的属性in_stock:true

> product = Product.available.build
=> #<Product:0x00007fd095ed4208
 id: nil,
 name: nil,
 price: nil,
 weight: nil,
 in_stock: true,
 expiration_date: nil,
 created_at: nil,
 updated_at: nil>

 

model可以带一个defult_scope,设定了default_scope, 创建新的记录的时候会自动带上default_scope的属性。 

 unscoped方法会移除默认的scope.

 

 


 


Validates 

validates有大量的选项设置,具体可以看API. 

比如length选项,可以设置is, in,  maximun,  minimum, 提示信息too_long, too_short等

 

增加了验证后,Product.builde, 可以使用valid?方法测试是否通过验证。

使用errors生成❌对象

> product.errors
=> #<ActiveModel::Errors:0x00007fd095cf68c8
 @base=
  #<Product:0x00007fd096b3fcb8
   id: nil,
   name: nil,
   price: nil,
   weight: nil,
   in_stock: nil,
   expiration_date: nil,
   created_at: nil,
   updated_at: nil>,
 @details={:name=>[{:error=>:blank}], :price=>[{:error=>:blank}]},
 @messages={:name=>["can't be blank"], :price=>["can't be blank"]}>

 

使用full_messages得到一个数组 => ["Name can't be blank", "Price can't be blank"]

 

可以使用save(validate: false)强制保存,⚠️除非真的有好的理由,否则别这么使用。

 


Numericality 

 

validates :weight, numericality: {equal_to: 100} 

⚠️存储的数据,某个字段的值如果不符合该字段的type, 同时又没有限制,该值不会存入数据库。

如果字段是integer/string 型,可以使用numericality: true来🚫。

numericality有很多🚫选项。如:

only_integer:true, greater_than: 100,甚至可以限制偶数even:true,奇数odd: true 

 


Uniqueness值需唯一

 

也有大量选项,如大小写唯一等:

:scope - One or more columns by which to limit the scope of the uniqueness constraint.  

 

在account_id字段,不能有相同的user_name。

比如account_id: 001, 它有对应的user_name: "xiaoming", 那么account_id: 002就不能有xiaoming了。 也就是说xiaoming只在account_id的列集合数据中,只出现一次。

class Person < ActiveRecord::Base
  validates_uniqueness_of :user_name, scope: :account_id
end

 

也可以多重范围scope的uniqueness:

这是一个教师表,唯一的设定是:每个学期semester_id的每个班class_id的teacher_id是唯一的,一个学期内,一个教师在这个班当班主任,别的班就不能当了。

class TeacherSchedule < ActiveRecord::Base
  validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
end

 


 

 

inclusion和exclusion 

 包含:

validates_inclusion_of :gender, in: %w( m f )
选项是in, message。

 validates :gender, inclusion: {in:["m", "f"], message: "this one is not allowed"}

 

exclusion是不包含。其他没区别。 

 


format是用来配合正则表达式的。 


 

通用的验证选项

on 

限定使用的events create, update等,也可以限定自定义方法

allow_nil: true #值可以是nil

allow_blank: true #值可以是nil,或者空字符串"" 

 

if and unless       

:if- 指定一个方法,Proc对象,或字符串来调用,当返回true时,则该验证生效

(e.g.if: :allow_validation, orif: Proc.new { |user| user.signup_step > 2 }).

例子:

validates :name, presence: true, if: :today_is_moday? 

def today_is_monday?

   Date.today.monday?

end 

 


 

自定义验证方法

Defining Validations with Your Own Methods

 

validate()方法。自定义验证条件。需要配合errors.add(attr,message) 

例子:

  validate :reservation_dates_must_make_sense
  private
  def reservation_dates_must_make_sense
    if end_date <= start_date
      errors.add(:start_date, "has to be before the end date")
    end
  end

这个方法不需要主动调用,在创建新的记录时会自动调用。

 

validates_each(*attr_names, &block) 可以接受一个block,可在块中自定义限制条件。

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
  end
end

 

更高级的方法(很少用到):validates_with(*args, &block), 参数是一个个类。

这个方法是include 这个类,然后使用这个类的方法进行验证。

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if options[:fields].any?{|field| record.send(field) == "Evil" }
      record.errors[:base] << "This person is evil"
    end
  end
end
 
class Person < ApplicationRecord
  validates_with GoodnessValidator, fields: [:first_name, :last_name]
end

解释:

注意Person是继承自ApplicationRecord, 所以已经include了ActiveModel::Validator模块。

 

 


 



 

 

 

 

 

 

 

 

 

posted @ 2018-06-15 19:51  Mr-chen  阅读(693)  评论(0编辑  收藏  举报