ActiveRecord-Suche nach Jahr, Tag oder Monat in einem Datumsfeld

70

Ich habe ein ActiveRecord-Modell mit einem Datumsattribut. Ist es möglich, dieses Datumsattribut zu verwenden, um nach Jahr, Tag und Monat zu suchen:

Model.find_by_year(2012)
Model.find_by_month(12)
Model.find_by_day(1)

oder ist es einfach möglich ,_by_date (2012-12-1) zu finden.

Ich hatte gehofft, ich könnte es vermeiden, Attribute für Jahr, Monat und Tag zu erstellen.

Mutuelinvestor
quelle
Ich glaube nicht, dass ich gut kommuniziert habe. Ich versuche, Datensätze / Objekte zu finden, indem ich das vollständige Datum, nur das Jahr, nur den Monat und nur den Tag verwende. Der erste Fall ist unkompliziert, die späteren drei (mit Monat, Tag und Jahr) machen mir Schwierigkeiten.
Mutuelinvestor

Antworten:

171

Angenommen, Ihr "Datumsattribut" ist ein Datum (und kein vollständiger Zeitstempel), dann erhalten Sie mit einem einfachen where"Suchen nach Datum":

Model.where(:date_column => date)

Sie wollen nicht, find_by_date_columnda dies höchstens ein Ergebnis liefert.

Für die Abfragen für Jahr, Monat und Tag möchten Sie die extractSQL-Funktion verwenden:

Model.where('extract(year  from date_column) = ?', desired_year)
Model.where('extract(month from date_column) = ?', desired_month)
Model.where('extract(day   from date_column) = ?', desired_day_of_month)

Wenn Sie jedoch SQLite verwenden, müssen Sie damit herumspielen, strftimeda es nicht weiß, was es extractist:

Model.where("cast(strftime('%Y', date_column) as int) = ?", desired_year)
Model.where("cast(strftime('%m', date_column) as int) = ?", desired_month)
Model.where("cast(strftime('%d', date_column) as int) = ?", desired_day_of_month)

Die %mund %dFormat-Spezifizierer fügen in einigen Fällen führende Nullen hinzu, was die Gleichheitstests verwirren kann, daher müssen cast(... as int)die formatierten Zeichenfolgen zu Zahlen gezwungen werden.

ActiveRecord schützt Sie nicht vor allen Unterschieden zwischen Datenbanken. Sobald Sie also etwas Nicht-Triviales tun, müssen Sie entweder eine eigene Portabilitätsschicht erstellen (hässlich, aber manchmal notwendig) und sich an eine begrenzte Anzahl von Datenbanken binden (realistisch, es sei denn Sie veröffentlichen etwas, das in einer beliebigen Datenbank ausgeführt werden muss, oder führen Ihre gesamte Logik in Ruby aus (verrückt nach nicht trivialen Datenmengen).

Die Abfragen für Jahr, Monat und Tag des Monats sind in großen Tabellen ziemlich langsam. In einigen Datenbanken können Sie Indizes für Funktionsergebnisse hinzufügen, aber ActiveRecord ist zu dumm, um sie zu verstehen, sodass es zu einem großen Durcheinander kommt, wenn Sie versuchen, sie zu verwenden. Wenn Sie also feststellen, dass diese Abfragen zu langsam sind, müssen Sie die drei zusätzlichen Spalten hinzufügen, die Sie vermeiden möchten.

Wenn Sie diese Abfragen häufig verwenden, können Sie Bereiche für sie hinzufügen. Die empfohlene Methode zum Hinzufügen eines Bereichs mit einem Argument besteht jedoch darin, nur eine Klassenmethode hinzuzufügen:

Die Verwendung einer Klassenmethode ist die bevorzugte Methode, um Argumente für Bereiche zu akzeptieren. Auf diese Methoden kann weiterhin auf die Zuordnungsobjekte zugegriffen werden ...

Sie hätten also Klassenmethoden, die so aussehen:

def self.by_year(year)
    where('extract(year from date_column) = ?', year)
end
# etc.
mu ist zu kurz
quelle
3
Danke für eine tolle Antwort. Um dies zu tun, müssen Sie sich auf eine datenbankspezifische Funktion verlassen, die einen der Vorteile von Activerecord zunichte macht. Vielleicht sehen Sie deshalb so oft Daten, die nach Monat, Tag und Jahr aufgeschlüsselt sind. Nochmals vielen Dank.
Mutuelinvestor
1
Immer beeindruckt von der Qualität Ihrer Antworten
apneadiving
1
Dies war sehr hilfreich, obwohl ich für SQLite CASTdie Datumsnummer auf eine Ganzzahl setzen musste, anstatt 0 hinzuzufügen (das Hinzufügen von 0 gab mir aus irgendeinem Grund die falsche Nummer). Meine überarbeiteten Filter sind dann: Model.where("CAST(strftime('%Y', date_column) as INT) = ?", desired_year) Model.where("CAST(strftime('%m', date_column) as INT) = ?", desired_month) Model.where("CAST(strftime('%d', date_column) as INT) = ?", desired_day_of_month)
Jay El-Kaake
@ JayEl-Kaake: Ich denke, ein expliziter cast(... as int)ist auf jeden Fall besser als ein Trick. Ich bin mir nicht sicher, warum ich ihn überhaupt nicht verwendet habe cast.
Mu ist zu kurz
Keine Notwendigkeit für Extrakt Methode in pg Sie verwenden können , TO_CHARmit ILIKEnur einer in der Linie, habe ich geschrieben das volle Beispiel am Ende dieses Threads
heriberto perez
32

Datenbankunabhängige Version ...

def self.by_year(year)
  dt = DateTime.new(year)
  boy = dt.beginning_of_year
  eoy = dt.end_of_year
  where("published_at >= ? and published_at <= ?", boy, eoy)
end
BSB
quelle
4
Es gibt auch beginning_of_day/ end_of_dayund beginning_of_month/ end_of_monthfür die anderen Implementierungen.
d_rail
Dies ist eine weit überlegene Antwort, da ein Index verwendet wird, falls verfügbar. Mu macht einen Tabellenscan.
Sixty4Bit
11

In Ihrem Modell:

scope :by_year, lambda { |year| where('extract(year from created_at) = ?', year) }

In Ihrem Controller:

@courses = Course.by_year(params[:year])
Fahrbahn
quelle
11

Probieren Sie dies für MySQL aus

Model.where("MONTH(date_column) = 12")
Model.where("YEAR(date_column) = 2012")
Model.where("DAY(date_column) = 24")
Beena Shetty
quelle
5

Ich denke, der einfachste Weg, dies zu tun, ist ein Bereich:

scope :date, lambda {|date| where('date_field > ? AND date_field < ?', DateTime.parse(date).beginning_of_day, DateTime.parse(date).end_of_day}

Verwenden Sie es so:

Model.date('2012-12-1').all

Dadurch werden alle Modelle mit einem Datumsfeld zwischen dem Beginn und dem Ende des Tages für das von Ihnen gesendete Datum zurückgegeben.

Veraticus
quelle
Ein Tag ist kein offenes Intervall, sondern ein halboffenes Intervall.
Mu ist zu kurz
2
'd >= ? and d < ?', dt.beginning_of_day, dt.tomorrow.beginning_of_day. Verwenden <und >verfehlt die Grenze zwischen den Tagen.
Mu ist zu kurz
1
Ich bin mir nicht ganz sicher, ob dies etwas ist, wonach Mutuelinvestor gefragt hat, da es den Fall nicht abdeckt, wenn Benutzer nach allen Posts ab April fragen und den Teil 'Jahr' vernachlässigen
ITmeze
4

Einfache Implementierung für nur das Jahr

app/models/your_model.rb

scope :created_in, ->( year ) { where( "YEAR( created_at ) = ?", year ) }

Verwendung

Model.created_in( 1984 )
Joshua Pinter
quelle
1

Ich mag die Antwort von BSB, aber als Umfang

scope :by_year, (lambda do |year| 
  dt = DateTime.new(year.to_i, 1, 1)
  boy = dt.beginning_of_year
  eoy = dt.end_of_year
  where("quoted_on >= ? and quoted_on <= ?", boy, eoy)
end)
idrinkpabst
quelle
1

Dies wäre eine andere:

def self.by_year(year)
  where("published_at >= ? and published_at <= ?", "#{year}-01-01", "#{year}-12-31")
end

Funktioniert ziemlich gut für mich in SQLite.

Tim und Struppi81
quelle
1

Ein weiterer kurzer Bereich wie folgt:

  class Leave
    scope :by_year, -> (year) {
      where(date:
                Date.new(year).beginning_of_year..Date.new(year).end_of_year)
    }
  end

Nennen Sie den Bereich:

Leave.by_year(2020)
Shiko
quelle
0

Keine extractMethode erforderlich , dies funktioniert wie ein Zauber in postgresql:

text_value = "1997" # or text_value = '1997-05-19' or '1997-05' or '05', etc
sql_string = "TO_CHAR(date_of_birth::timestamp, 'YYYY-MM-DD') ILIKE '%#{text_value.strip}%'"
YourModel.where(sql_string)
Heriberto Perez
quelle
0

In Postgresql v 11+ (ich habe getestet): unten funktioniert

Model.where('extract(year  from date_column::date) = ?', desired_year)
Model.where('extract(month from date_column::date) = ?', desired_month)
Model.where('extract(day   from date_column::date) = ?', desired_day_of_month)
Ravistm
quelle
0

Ich benutze normalerweise:

Model.where('extract(month from birthday_column) = ? and extract(day from birthday_column) = ?', Time.now.month, Time.now.day)
Leandro Castro
quelle
0

Wenn Sie nur nach dem Datum suchen, tun Sie einfach:

Model.where(date: Date.today.beginning_of_day..Date.today.end_of_day)
Angelo Richardson
quelle