如何在Rails中使用postgresql验证重叠的时间?

3

我在我的日程应用程序中拥有一个名为 Event model 的模型,其中包含 start_at 时间和 end_at 时间,并且我希望在保存之前验证重叠时间。

我是在Cloud9上创建我的Rails应用程序的。

我的视图图像如下所示:

Day1
07:00 - 07:20 event1
10:30 - 11:30 event2
15:40 - 16:10 event3
[add event button]

Day2
08:15 - 09:05 event4
12:08 - 13:04 event5
14:00 - 14:25 event6
[add event button]

[save schedule button]

start_at时间和end_at时间可以同时更改和添加。

我想要做的是,如果我尝试为Day1添加(或更改为)07:05 - 07:30,例如为Day2添加13:50 - 14:30等,则显示错误。

例如:

app_development=# select * from events;

 id | start_at |  end_at  | title  | detail | schedule_id |         created_at         |         updated_at         
----+----------+----------+--------+--------+-----------------+----------------------------+----------------------------
  1 | 07:00:00 | 07:20:00 | event1 |        |               1 | 2016-04-12 05:28:44.166827 | 2016-04-12 12:52:07.682872
  2 | 10:30:00 | 11:30:00 | event2 |        |               1 | 2016-04-12 05:28:44.17747  | 2016-04-12 12:52:07.689934
  3 | 15:40:00 | 16:10:00 | event3 |        |               1 | 2016-04-12 05:29:07.5005   | 2016-04-12 12:52:07.693477

我在表格上方添加了07:05 - 07:30,但验证不起作用。
虽然我曾经提出类似的问题,但被建议使用postgresql而不是sqlite3。
所以我成功配置了postgresql,但结果仍然相同。如果您能告诉我如何检查和显示错误,我将不胜感激。 schema.rb
  create_table "events", force: :cascade do |t|
    t.time     "start_at"
    t.time     "end_at"
    t.string   "title"
    t.integer  "room_id"
    ...

  create_table "rooms", force: :cascade do |t|
    t.string   "room"
    t.integer  "schedule_id"
    ...

  create_table "schedules", force: :cascade do |t|
    t.string   "title"
    t.integer  "user_id"
    t.date     "departure_date"
    ...

给出以下模型

class Event < ActiveRecord::Base
  belongs_to :room, inverse_of: :events
  has_one :schedule, autosave: false, through: :room
  ...
  validate :cannot_overlap_another_event

  def cannot_overlap_another_event
    range = Range.new start_at, end_at
    overlaps = Event.exclude_self(id).in_range(range)
    overlap_error unless overlaps.empty?
  end

  scope :in_range, -> range {
    where('(start_at BETWEEN ? AND ?)', range.first, range.last)
  }
  scope :exclude_self, -> id { where.not(id: id) }

  def overlap_error
    errors.add(:overlap_error, 'There is already an event scheduled in this hour!')
  end

class Schedule < ActiveRecord::Base
  belongs_to :user
  has_many :rooms, inverse_of: :schedule
  accepts_nested_attributes_for :rooms, allow_destroy: true
  ...

class Room < ActiveRecord::Base
  belongs_to :schedule, inverse_of: :rooms
  has_many :events, inverse_of: :room
  accepts_nested_attributes_for :events, allow_destroy: true
  ...

_schedule_form.html.erb

  <%= render 'shared/error_messages', object: f.object %>
  <%= f.label :title %>
  <%= f.text_field :title, class: 'form-control' %>
  <br>
    <%= f.label :departure_date %>
    <div class="input-group date" id="datetimepicker">
      <%= f.text_field :departure_date, :value => (f.object.departure_date if f.object.departure_date), class: 'form-control' %>
      <span class="input-group-addon">
        <span class="glyphicon glyphicon-calendar"></span>
      </span>
    </div>
  <script type="text/javascript">
    $(function () {
      $('#datetimepicker').datetimepicker({format:'YYYY-MM-DD'});
    });
  </script>
  <br>
  <div id="room">
    <%= f.simple_fields_for :rooms do |a| %>
  <div id="room_<%= a.object.object_id %>">
        <p class="day-number-element-selector"><b>Day&nbsp;<%= a.index.to_i + 1 %></b></p>

        <%= a.simple_fields_for :events do |e| %>
          <span class="form-inline">
            <p>
              <%= e.input :start_at, label: false %>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;
              <%= e.input :end_at, label: false %>
            </p>
          </span>
          <%= e.input :title, label: false %>
        <% end %>
      </div>

      <%= a.link_to_add "Add event", :events, data: {target: "#room_#{a.object.object_id}"}, class: "btn btn-primary" %>

      <%= a.input :room %>

    <% end %>
  </div>

如果您能告诉我如何检查和显示错误,我将不胜感激。

编辑

编辑如下:

event.rb

scope :in_range, -> range {
  where('(start_at BETWEEN ? AND ? OR end_at BETWEEN ? AND ?) OR (start_at <= ? AND end_at >= ?)', range.first, range.last, range.first, range.last, range.first, range.last)
}

尽管看起来似乎有效,但是当我添加一个不同日期的event时,这个验证就无法正常工作,如下所示id=8。(请参阅created_atupdated_at)。 app_development=# select * from events;
id | start_at |  end_at  | title  | detail | room_id |         created_at         |         updated_at         
----+----------+----------+--------+--------+-----------------+----------------------------+----------------------------
  1 | 07:00:00 | 07:20:00 | event1 |        |               1 | 2016-04-12 05:28:44.166827 | 2016-04-12 12:52:07.682872
  2 | 10:30:00 | 11:30:00 | event2 |        |               1 | 2016-04-12 05:28:44.17747  | 2016-04-12 12:52:07.689934
  3 | 15:40:00 | 16:10:00 | event3 |        |               1 | 2016-04-12 05:29:07.5005   | 2016-04-12 12:52:07.693477
  8 | 07:05:00 | 07:10:00 | event4 |        |               1 | 2016-04-15 21:37:58.569868 | 2016-04-15 21:39:27.956737
2个回答

7

在PostgreSQL中,您不需要重新发明轮子,有两种实现简单的方法可以实现重叠检查:

  1. SQL的OVERLAPS运算符

非常简单,

where("(start_at, end_at) OVERLAPS (?, ?)", range.first, range.last)

然而,这个使一个范围紧接着另一个范围 (换句话说,它检查 开始 <= 时间 < 结束)。
第二点:范围类型'&& (重叠) 操作符:
通常情况下,这也很简单。但是 PostgreSQL 没有内置的时间范围类型(然而,对于其他时间类型,有 tsrangetstzrangedaterange)。
你需要自己创建这个范围类型:
CREATE TYPE timerange AS RANGE (subtype = time);

但在此之后,您可以检查重叠区域。
where("timerange(start_at, end_at) && timerange(?, ?)", range.first, range.last)

范围类型的优点:

  • you can control yourself, how do you want to handle range boundaries

    f.ex. you could use timerange(start_at, end_at, '[]') to include both the start & the end-point of the ranges. By default it includes the start, but excludes the end-point of the ranges.

  • it can be indexed, f.ex. with

    CREATE INDEX events_times_idx ON events USING GIST (timerange(start_at, end_at));
    
  • Exclusion constraints: this is essentially the same, what you want to achieve, but it will be enforced at DB level (like, UNIQUE or any other constraints):

    ALTER TABLE events
      ADD CONSTRAINT events_exclude_overlapping
      EXCLUDE USING GIST (timerange(start_at, end_at) WITH &&);
    

感谢您的回答,@pozs。虽然我投票认为“这个答案很有用”,但我尝试了另一个答案。因为这是我第一次使用postgresql。我会稍后尝试理解您的建议。顺便说一下,如果您能在我的问题底部给我任何建议,我将不胜感激。 - SamuraiBlue
@SamuraiBlue 这是因为你没有应用你所接受的那个。它应该是 where("? BETWEEN start_at AND end_at OR ? BETWEEN start_at AND end_at OR start_at BETWEEN ? AND ?", range.first, range.last, range.first, range.last) - pozs

3
你的范围朝着正确的方向发展,但并未涵盖所有情况。
scope :in_range, -> range {
  where('(start_at BETWEEN ? AND ?)', range.first, range.last)
}

在你的例子中,你最终要检查 start_at BETWEEN 7:05 AND 7:30,但是第一天的 start_at7:00,不在此范围内。
你需要处理四种情况:
New range overlaps start
Existing:     |------------|
New:      |-------|

New range overlaps end
Existing: |------------|
New:               |-------|

New range inside existing range
Existing: |------------|
New:         |-------|

Existing range inside new range
Existing:    |-------|
New:      |------------|

我是一名有用的助手,可以为您进行文本翻译。以下是需要翻译的内容:

观察一下,您会发现前三种情况是通过检查是否

new_start BETWEEN start_at AND end_at
OR
new_end   BETWEEN start_at AND end_at

然后你只需要添加内容以捕获第四个案例。
OR
start_at BETWEEN new_start AND new_end

你可以在 end_at 上添加类似的检查以实现代码对称性,但这并不是必须的。


感谢您的回答,@Kristján。我尝试编辑验证,就像我问题底部所示。但是当我尝试在不同的日期添加“事件”时,此验证无效。我已经编辑了问题底部的源代码。如果您能给我任何建议,我将不胜感激。 - SamuraiBlue
看起来你的数据库中没有任何关于事件发生日期的表示。你需要添加这个,然后你也可以考虑 AND new_day = existing_day - Kristján
感谢您的快速评论,@Kristján。是否不可能仅检查 start_atend_at?因为当我尝试使用底部添加的 select sql 时,似乎只存储了时间(而不是日期)在这些字段中。 - SamuraiBlue
这就是我的意思。如果你没有存储日期,你怎么能比较是否在同一天?如果你想要存储实际日期,你应该将你的列设置为“时间戳”。如果你的天数是一个序列(1、2、3,...)而不是实际绑定到一个日期上的,那么你需要另外一列来存储。 - Kristján
感谢您的评论,@Kristján。您回答了我的第一个问题,我接受了您的回答。我会考虑您建议的AND new_day = existing_day - SamuraiBlue

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接