【问题标题】:How to validate overlapping times in Rails with postgresql如何使用 postgresql 在 Rails 中验证重叠时间
【发布时间】:2016-04-12 13:44:29
【问题描述】:

我有一个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,我想做的是显示错误,例如,13:50 - 14:30Day2 等等。

例如;

app_development=# 从事件中选择 *;

 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,但验证不起作用。

虽然我问了similar question,但有人建议我使用 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=# 从事件中选择 *;

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

【问题讨论】:

    标签: ruby-on-rails ruby postgresql validation ruby-on-rails-4


    【解决方案1】:

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

    1. SQL 的OVERLAPS operator:

    够简单,

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

    但是,这使得一个范围正好在另一个范围之后
    (换句话说,它检查 start )。

    1. Range types'&amp;&amp; (overlaps) operator:

    这也很简单,通常。但是 PostgreSQL 没有用于 time 的内置范围类型(​​但是对于其他时间类型,有 tsrangetstzrangedaterange)。

    你需要自己创建这个范围类型:

    CREATE TYPE timerange AS RANGE (subtype = time);
    

    但在此之后,您可以检查重叠

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

    范围类型的优点:

    • 你可以控制自己,你想如何处理范围边界

      f.ex.您可以使用 timerange(start_at, end_at, '[]') 来包含范围的起点和终点。默认情况下,它包括范围的起点,但不包括范围的终点。

    • 它可以被索引,f.ex。与

      CREATE INDEX events_times_idx ON events USING GIST (timerange(start_at, end_at));
      
    • Exclusion constraints:这本质上是相同的,你想要实现的,但它将在数据库级别强制执行(例如,UNIQUE 或任何其他约束):

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

    【讨论】:

    • 感谢您的回答,@pozs。虽然我投了this answer is useful,但我试图应用另一个答案。因为这是第一次使用postgresql。稍后我会尝试了解您的建议。顺便说一句,如果你能在我的问题底部给我任何建议,我将不胜感激
    • @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)
    【解决方案2】:

    您的范围正朝着正确的方向前进,但并未涵盖您的所有案例。

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

    在您的示例中,您最终检查了start_at BETWEEN 7:05 AND 7:30,但第 1 天的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:      |------------|
    

    看,可以看到前三种情况都是通过检查if来处理的

    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。我试图编辑验证,例如在我的问题底部。但是当我尝试在另一天添加event 时,此验证不起作用。我在问题的底部编辑了源代码。如果您能给我任何建议,我们将不胜感激。
    • 您的数据库中似乎没有任何表示事件发生在哪一天的任何表示。你需要加上那个,然后你也可以考虑AND new_day = existing_day
    • 感谢您的快速评论,@Kristján。仅检查 start_atend_at 是不可能的吗?因为当我尝试select sql 时,似乎只有时间(不是日期)存储在这些字段中,正如我在问题底部添加的那样。
    • 这就是我要说的。当您不存储时,您如何期望比较事物是否在同一天?如果要存储实际日期,则应将列设为timestamps。如果您的日子是一个序列(1、2、3、...),实际上并没有与一个日期相关联,那么您需要一列。
    • 感谢您的评论,@Kristján。你回答了我的第一个问题,我接受了你的回答。我稍后会考虑AND new_day = existing_day你建议的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多