英文:
SQL Query to combine alterative rows into a single table using Django's Models
问题
我已经创建了一个考勤服务器,允许员工记录他们的考勤。
有手动和自动两种记录考勤的方式。系统的SQL表格如下。
员工 | 本地用户 | 设备 | 生物识别记录 | 打卡记录 |
---|---|---|---|---|
Id | Id | Id | Id | Id |
姓名 | 姓名 | 地点 | 本地用户ID | 员工ID |
设备ID | API密钥 | 打卡时间 | 打卡时间 | |
员工ID | 已禁用 |
每当添加了一个生物识别记录,如果该记录中的本地用户有一个员工外键,那么就会创建一个打卡记录。我觉得这有助于将自动和手动记录结合在一起。手动记录直接添加到打卡记录中。
当用户请求考勤时,目前会获取活跃的打卡记录的交替行,并将其合并为一行。
输出数据格式
考勤记录 |
---|
Id |
打卡时间(上班) |
打卡时间(下班) |
员工ID |
使用的代码
目前我正在使用原始的SQL查询来获取数据。
基本逻辑是:
- 分成两个子查询 -> 上班打卡 & 下班打卡,用户唯一的Roll号
- 与Roll号联接
select i.id, username, i.punched_on as punch_in, min(punch_out) as punch_out
from (select *,row_number() over (partition by user_id order by user_id) as r_index from attendance_punch_log) as i
left join local_user on i.user_id = local_user.id
left join (
select o.logged_on as punch_out, username as out_user
from (select *, row_number() over (partition by user_id order by user_id) as l_index from attendance_punch_log) as o
left join local_user on o.user_id = local_user.id) as o on username = out_user and punch_out > punch_in
where mod(r_index, 2) = 1
group by username , r_index
order by punch_in, punch_out
Django模型如下:
class BiometricsDevice(models.Model):
# ... (此处省略部分代码)
pass
class LocalUser(models.Model):
# ... (此处省略部分代码)
pass
class PunchLog(models.Model):
# ... (此处省略部分代码)
pass
class BiometricsLog(models.Model):
# ... (此处省略部分代码)
pass
用于创建查询集的代码如下:
def get(self, request):
# ... (此处省略部分代码)
pass
最后的想法
- 在最后创建考勤表格时,我应该使用哪种其他方法?
- 是创建一个新表格还是使用查询集更好?
英文:
I have created an attendance server that allows staffs to log their attendance.
There is a manual and a automatic way of logging attendance. The SQL tables for the system is as follows.
Staff | Local User | Device | Biometrics Log | Punch Log |
---|---|---|---|---|
Id | Id | Id | Id | Id |
Name | Name | Location | Local User ID | Staff ID |
Device ID | API Key | Punched On | Punched On | |
Staff ID | Is Disabled |
Anytime a biometrics log is added, if the local user in that log has a staff foreign key then a punch log gets created. I felt that this helps in combining automatic and manual logs. Manual Logs are added straight to Punch Log
When the user is requesting for Attendance. This currently will take alterative rows of punch log that are active and combines it into a single row.
Output Data Format
Attendance Log |
---|
Id |
Punch In |
Punch Out |
Staff ID |
CODE IN USE
Currently I am using raw sql query to get the data
The basic logic is to
- Split into two sub queries -> Punch In & Punch Out with Roll Number Unique to User
- Join with Roll Number
select i.id, username, i.punched_on as punch_in, min(punch_out) as punch_out
from (select *,row_number() over (partition by user_id order by user_id) as r_index from attendance_punch_log) as i
left join local_user on i.user_id = local_user.id
left join (
select o.logged_on as punch_out, username as out_user
from (select *, row_number() over (partition by user_id order by user_id) as l_index from attendance_punch_log) as o
left join local_user on o.user_id = local_user.id) as o on username = out_user and punch_out > punch_in
where mod(r_index, 2) = 1
group by username , r_index
order by punch_in, punch_out
The Django models are as follows
class BiometricsDevice(models.Model):
location = models.ForeignKey(
Location, on_delete=models.CASCADE, related_name="device"
)
description = models.CharField(max_length=120)
api_key = models.CharField(max_length=20, null=False, blank=False, unique=True)
last_sync = models.DateTimeField(null=True)
sync_user = models.BooleanField(default=True)
is_active = models.BooleanField(default=True)
created_on = models.DateField(auto_now_add=True)
created_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="created_biometrics",
)
def __str__(self):
return f"{self.location.name} - {self.api_key}"
class LocalUser(models.Model):
ref = models.PositiveIntegerField()
name = models.CharField(max_length=20)
device = models.ForeignKey(
BiometricsDevice, on_delete=models.CASCADE, related_name="users"
)
# Signal on staff update -> Re-Run Biometrics Log to Punch Log Conversion
staff = models.ForeignKey(
Staff,
on_delete=models.CASCADE,
related_name="biometric_user",
blank=True,
null=True,
)
def __str__(self):
return f"{self.name} - {self.device}"
class Meta:
unique_together = (["name", "device"], ["name", "ref", "device"])
class PunchLog(models.Model):
staff = models.ForeignKey(
Staff,
on_delete=models.CASCADE,
related_name="punch_log",
)
punched_on = models.DateTimeField()
is_active = models.BooleanField(default=True)
created_on = models.DateField(auto_now_add=True)
created_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="created_punch_logs",
null=True,
blank=True,
)
def __str__(self):
return f"{self.staff.name} - {self.punched_on}"
class BiometricsLog(models.Model):
user = models.ForeignKey(
LocalUser,
on_delete=models.CASCADE,
related_name="biometrics_log",
)
punched_on = models.DateTimeField()
punch_log = models.OneToOneField(
PunchLog,
on_delete=models.CASCADE,
related_name="biometrics_log",
null=True,
blank=True,
)
class Meta:
unique_together = ["user", "punched_on"]
def __str__(self):
return f"{self.user.name} - {self.punched_on}"
The Code to create queryset is as follows
def get(self, request):
punch_in = (
BiometricsLog.objects.annotate(
index=Window(
expression=RowNumber(),
order_by=F("user_id").asc(),
partition_by=[F("user_id")],
),
)
.annotate(evenindex=F("index") % 2)
.filter(evenindex=1)
.order_by("-punched_on")
)
punch_out = (
BiometricsLog.objects.annotate(
index=Window(
expression=RowNumber(),
order_by=F("user_id").asc(),
partition_by=[F("user_id")],
),
)
.annotate(evenindex=F("index") % 2)
.filter(evenindex=0)
.order_by("-punched_on")
)
# Odd Even index join
queryset = punch_in.annotate(
punch_in=F("punched_on"),
punch_out=Subquery(
punch_out.filter(
index__gt=OuterRef("index"),
user_id=OuterRef("user_id"),
).values("punched_on")[:1]
),
staff=F("user__staff"),
)
Final Thoughts
- What other method should i be using to create attendance table in the end ?
- Is it better to create a new table insted of a querset ?
答案1
得分: 0
似乎对我来说,在打卡日志中确定“in”或“out”的方法过于复杂。这确实会有维护问题,也可能影响性能。
你可以简单地向打卡日志模型添加一个“direction”字段(甚至只是布尔值)。这将使查询和其他一切都更简单和更可读。
class PunchLog(models.Model):
...
is_in = models.BooleanField(default=True)
对另一个日志模型进行类似调整可能也是有意义的。
编辑
然而,也许你想要审查整个模型架构。我认为这两个日志非常相似,实际上可以视为同一事物。而且,在只发生一个事件时,在某些不明显的情况下制作两个日志条目在概念上是不正确的。使用更简单的架构,查询变得微不足道。
将你的日志合并怎么样?
class LogEntry(models.Model):
local_user = models.ForeignKey(...)
created_on = models.DateTimeField(auto_now_add=True)
is_in = models.BooleanField(...)
is_biometric = models.BooleanField(...)
英文:
It seems to me that the method of determining "in" or "out" in the punch log is overly complicated. This certainly has maintenance issues and could also affect performance.
You could simply add a direction
field (or even just a boolean) to your punch log model. It would make the queries and everything else much simpler and more readable.
class PunchLog(models.Model):
...
is_in = models.BooleanField(default=True)
It might make sense make a similar adjustment to the other log model as well.
EDIT
However, perhaps you want to review your entire model architecture. I think that both logs are similar enough to be the practically the same thing. Also, I think it is conceptually not correct to make two log entries in certain not obvious conditions, when only one event occurs. With a simpler architecture, the queries become trivial.
How about combining your logs?
class LogEntry(models.Model):
local_user = models.ForeignKey(...)
created_on = models.DateTimeField(auto_now_add=True)
is_in = models.BooleanField(...)
is_biometric = models.BooleanField(...)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论